Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always challenge a user with no/bad credentials #359

Merged
merged 5 commits into from
Apr 23, 2018

Conversation

imtayadeway
Copy link
Contributor

According to RFC 2617:

The 401 (Unauthorized) response message is used by an origin server
to challenge the authorization of a user agent. This response MUST
include a WWW-Authenticate header field containing at least one
challenge applicable to the requested resource.

We currently only issue a challenge when no credentials are provided -
if a user fails authentication they are not challenged again. This
broken implementation can cause some frustration for instance when
using the web browser, which will cache a mis-typed user/password
combination and not challenge the user again.

This is somewhat problematic as we only use the Authorization header
for Basic Authentication. It seems like a safe compromise for the user
to be challenged to use Basic Authentication if authentication fails
for whatever reason.

According to RFC 2617:

> The 401 (Unauthorized) response message is used by an origin server
  to challenge the authorization of a user agent. This response MUST
  include a WWW-Authenticate header field containing at least one
  challenge applicable to the requested resource.

We currently only issue a challenge when no credentials are provided -
if a user fails authentication they are not challenged again. This
broken implementation can cause some frustration for instance when
using the web browser, which will cache a mis-typed user/password
combination and not challenge the user again.

This is somewhat problematic as we only use the `Authorization` header
for Basic Authentication. It seems like a safe compromise for the user
to be challenged to use Basic Authentication if authentication fails
for whatever reason.
rescue MiqException::MiqEVMLoginError => e
raise AuthenticationError, e.message
end
end or raise AuthenticationError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the or? that's generally a no-no

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just a Perl-ism - || raise isn't valid Ruby, so the alternative is to make this (perhaps) less readable by assigning to a variable that's only used to check that this is non-nil

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly, I find the variable more readable...or use tap (I do find tap less readable).

See also https://github.com/bbatsov/ruby-style-guide#no-and-or-or

|| is valid Ruby, so not sure why you think it's not...maybe you are using it without parenthesis?

[1] pry(main)> false || raise("blah")
RuntimeError: blah
from (pry):20:in `<main>'
[2] pry(main)> false || raise "blah"
SyntaxError: unexpected tSTRING_BEG, expecting keyword_do or '{' or '('
false || raise "blah"
                ^

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I must have misinterpreted that error, but I find having to know that that raise requires parentheses perhaps a bit arcane. I'll change to use the variable.

FWIW I prefer Avdi's take on the no or/and rule 😉

rescue MiqException::MiqEVMLoginError => e
raise AuthenticationError, e.message
end
end or raise AuthenticationError
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since the "guts" of the other legs of the if clause are methods, can we make this a method as well? Maybe authenticate_with_basic_auth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I considered this, but then it's not clear to me at a glance how that method (or any similarly-named method following the scheme above) differs crucially from the one from Rails' Authentication library (authenticate_with_http_basic) so I felt that might be better dealt with in a follow up?

rescue MiqException::MiqEVMLoginError => e
raise AuthenticationError, e.message
end
end or raise AuthenticationError
end
log_api_auth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strangely this only logged successful logins, but do we log unsuccessful logins anywhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have never logged an unsuccessful basic authentication login....it looks like we might lose some logging around the other cases. I'll try to rectify that

This also restores the existing logging that was present for failures
in the other strategies.
rescue AuthenticationError => e
api_log_error("AuthenticationError: #{e.message}")
response.headers["Content-Type"] = "application/json"
request_http_basic_authentication("Application", ErrorSerializer.new(:unauthorized, e).serialize.to_json)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing the logging of the 401,

before this PR, we get the following:

[----] I, [2018-04-16T17:48:16.502716 #26595:3fdeb52eb958]  INFO -- : MIQ(Api::ApiController.log_request_initiated)  
[----] I, [2018-04-16T17:48:16.502900 #26595:3fdeb52eb958]  INFO -- : MIQ(Api::ApiController.log_request) API Request:    {:requested_at=>"2018-04-16 21:48:16 UTC", :method=>"GET", :url=>"http://localhost:3000/api"}
[----] E, [2018-04-16T17:48:16.571327 #26595:3fdeb52eb958] ERROR -- : MIQ(Api::ApiController.api_error) API Error
[----] E, [2018-04-16T17:48:16.571381 #26595:3fdeb52eb958] ERROR -- : MIQ(Api::ApiController.api_error) MiqException::MiqEVMLoginError: Authentication failed
[----] I, [2018-04-16T17:48:16.571986 #26595:3fdeb52eb958]  INFO -- : MIQ(Api::ApiController.log_request) Response:       {:completed_at=>"2018-04-16 21:48:16 UTC", :size=>"0.108 KBytes", :time_taken=>"0.069 Seconds", :status=>401}

The last Response: entry is missing. This stuff is handled by api_error, so instead of adding more of that logic here, maybe the above can be as follows:

rescue AuthenticationError => e
   api_error(:unauthorized, e)
   request_http_basic_authentication("Application", ErrorSerializer.new(:unauthorized, e).serialize.to_json)
end

Or possibly get api_error to return to the response generated

rescue AuthenticationError => e
   request_http_basic_authentication("Application", api_error(:unauthorized, e).to_json)
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. It looks like request_http_basic_authentication is short-circuiting the after_action that invokes this - it's not specific to api_error. Will look into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 9fe4247

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @imtayadeway for the update.

@@ -37,8 +37,6 @@ class BaseController < ActionController::API
rescue_from(ActiveRecord::StatementInvalid) { |e| api_error(:bad_request, e) }
rescue_from(JSON::ParserError) { |e| api_error(:bad_request, e) }
rescue_from(MultiJson::LoadError) { |e| api_error(:bad_request, e) }
rescue_from(MiqException::MiqEVMLoginError) { |e| api_error(:unauthorized, e) }
rescue_from(AuthenticationError) { |e| api_error(:unauthorized, e) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see AuthenticationError raised from within this file so that's ok to remove. I'm not sure about MiqEVMLoginError in the other 2 token based auth. Should be verified. If that error can be raised, we'd need to rescue MiqException::MiqEVMLoginError for those cases too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK because:

  1. I see tests that cover the happy/unhappy paths around authenticating with tokens
  2. System token authentication does a catch-all rescue, and that specific error inherits from StandardError
  3. User token authentication doesn't do any actual logging-in as such, it just talks to the token manager

@miq-bot
Copy link
Member

miq-bot commented Apr 17, 2018

Checked commits imtayadeway/manageiq-api@4ebe167~...9fe4247 with ruby 2.3.3, rubocop 0.52.1, haml-lint 0.20.0, and yamllint 1.10.0
3 files checked, 0 offenses detected
Everything looks fine. 🍪

@abellotti abellotti added this to the Sprint 84 Ending Apr 23, 2018 milestone Apr 23, 2018
@abellotti
Copy link
Member

LGTM!! Thanks @imtayadeway for fixing this. 👍

@abellotti abellotti merged commit 97eea82 into ManageIQ:master Apr 23, 2018
@himdel
Copy link
Contributor

himdel commented Apr 24, 2018

(summarizing gitter ☝️ April 24, 2018 3:24 PM)

Turns out the change in app/controllers/api/base_controller/authentication.rb changes what happens when validate_user_identity fails.

Before, it would not prevent the user from logging in, but now that it is being run inside a authenticate_with_http_basic do ... block, it does.

Turns out logging in as a newly created user now fails with User's Group is missing (#366) because their current_group is not set yet.

@himdel
Copy link
Contributor

himdel commented Aug 21, 2018

Looks like this is a problem...

DELETE /api/auth with x-auth-token will request HTTP Basic Authentication if that token already expired.
So, trying to log out with an expired token causes the popup.

There seems to be no way of disabling the popup on the browser side, so this may need to be undone.
(Or the UI needs to keep track of the token validity, which seems too error-prone.)

@himdel
Copy link
Contributor

himdel commented Oct 10, 2018

Login dialog popup problem addressed in #488

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants