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

allow single_access_token via http headers #728

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

sevgibson
Copy link

Acts exactly like params, but uses headers instead.

curl -H "user_credentials: 4LiXF7FiGUppIPubBPey" https://www.domain.com

@jaredbeck
Copy link
Collaborator

Hi Scott, thanks for the contribution. Before we review this, can you please remind me of some of the security considerations re: putting plaintext secrets in HTTP headers? My dim recollection is there's no difference compared to e.g. a URL param, ie. the security of either approach depends on TLS and SNI? Can you please summarize the concerns and/or point me to some reading material? Thanks.

@sevgibson
Copy link
Author

Thank you for your quick reply @jaredbeck !

I believe your recollection is exactly correct. And, in fact, OAuth generally uses a Header for the Authorization: Bearer token.

From https://en.wikipedia.org/wiki/HTTPS:

Because HTTPS piggybacks HTTP entirely on top of TLS, the entirety of the underlying
HTTP protocol can be encrypted. This includes the request URL (which particular web
page was requested), query parameters, headers, and cookies (which often contain
identifying information about the user).

I had trouble finding such a clear statement from the RFC, but it basically says that after the initial connection, ALL data must be sent as TLS "application data". This would include the URL, query parameters, headers, and payload body. https://tools.ietf.org/html/rfc2818#section-2

Screen Shot 2020-08-28 at 7 08 30 PM

Copy link
Collaborator

@jaredbeck jaredbeck left a comment

Choose a reason for hiding this comment

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

Hi Scott, I've finished my first review. It looks like you've copied all the params methods (and tests) carefully, so it all looks good at first glance. I'd like to address the level of duplication first. Once we've addressed that, I think a final review will go smoothly. Thanks!

else
%i[all any].include?(single_access_allowed_request_types)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems headers_enabled_by_allowed_request_types? and params_enabled_by_allowed_request_types? have exactly the same contents. Let's fix that duplication by having a single method. What do you think about a name like request_type_allowed?, or single_access_token_allowed_by_request_type??

lib/authlogic/session/base.rb Show resolved Hide resolved
UserSession.single_access_allowed_request_types
)
end
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

To avoid duplicating ConfigTest verbatim, perhaps we should combine params_test.rb and headers_test.rb and call it single_access_token_test.rb.

Copy link
Author

Choose a reason for hiding this comment

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

@jaredbeck Note that I don't necessarily like adding a method that isn't really a test_ method to the class SessionTest::SingleAccessTokenTest::InstanceMethodsTest but it was the best way I could see to reduce the duplication, and not hide the actual assertions off in a helper file (which would be far worse for the reader).

lib/authlogic/session/base.rb Show resolved Hide resolved
@sevgibson
Copy link
Author

Overall, I realize I totally missed DRY the first time -- not a novice programmer, but new to submitting PRs to open source. Since this was security, I was a bit afraid to make any of these slight refactors without having a review by someone much more familiar with the code. Thank you for your patience!

Copy link
Collaborator

@jaredbeck jaredbeck left a comment

Choose a reason for hiding this comment

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

Other than my suggestion about maybe using Set, this LGTM.

Please add an item to the changelog under Unreleased -> Added.

@tiegz can you also take a quick look at this PR, please?

lib/authlogic/session/base.rb Outdated Show resolved Hide resolved
Copy link
Collaborator

@tiegz tiegz left a comment

Choose a reason for hiding this comment

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

This looks like a useful addition to Authlogic, @sevgibson .

Only concern I have is: is this a breaking change if it's enabled by default?

Downstream users should already be aware that url-params can authenticate, if they have the single_access_token column. Will it be surprising then that someone could authenticate via a header suddenly too? (i.e. could single-access-token-replay attacks suddenly become easier to conceal?)

The answer might be "no", just curious what you all think.

lib/authlogic/session/base.rb Outdated Show resolved Hide resolved
test/session_test/single_access_token_test.rb Outdated Show resolved Hide resolved
@jaredbeck
Copy link
Collaborator

Thanks for the review, Tieg.

is this a breaking change if it's enabled by default? Downstream users should already be aware that url-params can authenticate, if they have the single_access_token column. Will it be surprising then that someone could authenticate via a header suddenly too? (i.e. could single-access-token-replay attacks suddenly become easier to conceal?)

It's not a breaking change as far as our API is concerned, and we haven't yet identified any meaningful impact on security.

I'm glad you mentioned replay attacks, though. Can you talk a bit more about that? An attacker intercepts the single_access_token? Perhaps we should only accept single_access_token over TLS connections. Now that would be a breaking change 😁

PS: I find the term "single access" misleading, because it sounds like "single use", but that's probably a discussion for another time.

sevgibson and others added 2 commits September 15, 2020 08:31
Co-authored-by: Tieg Zaharia <[email protected]>
Co-authored-by: Tieg Zaharia <[email protected]>
@sevgibson
Copy link
Author

sevgibson commented Sep 15, 2020

@tiegz @jaredbeck I agree that this should not be enabled by default. This makes perfect sense to me because my intent was not to make a change that would become active by default for existing apps. If you both agree, then I'll submit an update to default to nil which should be disabled. Please confirm if you both agree. Thank you!

@sevgibson
Copy link
Author

Perhaps we should only accept single_access_token over TLS connections. Now that would be a breaking change 😁

💯

@tiegz
Copy link
Collaborator

tiegz commented Sep 15, 2020

I'm glad you mentioned replay attacks, though. Can you talk a bit more about that? An attacker intercepts the single_access_token?

Scenario: a user's single access token is accidentally leaked.

  • Currently: it would be easy to track down usages of that token by looking at url params in your logs
  • After this change: most people don't log all HTTP headers, so it would be easier for an attacker to use this token and make it look like they had just used session authentication (which might appear more legit).

That's my only worry -- but it assumes things about logs and importance of params vs headers, so maybe it's NBD.

Perhaps we should only accept single_access_token over TLS connections. Now that would be a breaking change 😁

Not a bad idea TBH!

PS: I find the term "single access" misleading, because it sounds like "single use", but that's probably a discussion for another time.

Totally agreed. At a past job, I created an Authlogic extension that wrapped database-backed tokens and used the single_access? feature in Authlogic. So you can land on a url with that token, but the token is pegged to that page, and gave you lesser power, and it would eventually expire. I've always thought that'd be useful as part of Authlogic. You could even use the single_access_token in the database as a key to derive these one-time-use tokens, so you don't need to store them anywhere (similar to JWTs).

@tiegz
Copy link
Collaborator

tiegz commented Sep 15, 2020

@tiegz @jaredbeck I agree that this should not be enabled by default. This makes perfect sense to me because my intent was not to make a change that would become active by default for existing apps. If you both agree, then I'll submit an update to default to nil which should be disabled. Please confirm if you both agree. Thank you!

I personally prefer that to avoid any surprises, but it's up to you @jaredbeck (and then maybe we could even enable it by default in the next major version?)

@tiegz
Copy link
Collaborator

tiegz commented Sep 15, 2020

Also, wanted to mention an experience I've had in the past:

I was using something very similar to single_access_token for authentication in the url. We discovered that the url was being cached in Facebook's caching layer because it was triggering 2FA SMS codes spuriously for users. Given that, plus how often people forget to scrub params in logs, it's worth considering how easy it is to leak a single_access_token [in url params].

@tiegz
Copy link
Collaborator

tiegz commented Sep 15, 2020

And another experience I just remembered 😆 :

Got hit by a credential-stuffing attack where the attacker was using Basic Auth to attempt mass logins. Luckily we had disabled Authlogic's Basic Auth (which I think was disabled as a default years ago too). But since we weren't logging the Basic Auth header anywhere, the only way I could figure that out was accidentally while looking at raw requests 🤮

[Lesson from that being: headers are less obvious than params]

@sevgibson
Copy link
Author

@tiegz I appreciate how thoroughly you're considering the security implications, since of course most security holes (aside from intentional trojan horse) are due to holes that are simply overlooked. Having said that, I think that having this disabled by default leaves the assessment of the risks specifically related to using single_access_token in headers in the hands of the developer using the gem.

TBH, using single_access_token is a pretty low bar when compared with other ouath style protocols (JWT, etc). The problem of course is the level of effort when starting a new project. And once committed down that path, it becomes very difficult to transition.

Copy link
Collaborator

@jaredbeck jaredbeck left a comment

Choose a reason for hiding this comment

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

Some minor stuff. I think we're close.

lib/authlogic/session/base.rb Outdated Show resolved Hide resolved
lib/authlogic/session/base.rb Show resolved Hide resolved
lib/authlogic/session/base.rb Show resolved Hide resolved
@sevgibson
Copy link
Author

@jaredbeck I had to fix specs, and also decided to explicitly assert the defaults for both params_key and headers_key (since they are different now).

Copy link
Collaborator

@jaredbeck jaredbeck left a comment

Choose a reason for hiding this comment

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

LGTM. @tiegz please merge (squash) if you're happy

@tiegz
Copy link
Collaborator

tiegz commented Sep 21, 2020

Looks really close to me, but I can see 2 blockers:

  • we need to access controller.request.headers instead of controllers.headers (which is for response headers)
  • to comply with CGI, Rack servers will uppercase the header name, convert hyphens to underscores, and prefix with "HTTP_". Maybe we can do that in the headers_credentials method, similar to how Rails does it?

@sevgibson
Copy link
Author

Looks really close to me, but I can see 2 blockers:

  • we need to access controller.request.headers instead of controllers.headers (which is for response headers)
  • to comply with CGI, Rack servers will uppercase the header name, convert hyphens to underscores, and prefix with "HTTP_". Maybe we can do that in the headers_credentials method, similar to how Rails does it?

@tiegz Ok, makes sense -- this may take me a little time, so marking this as draft for now, and will request review again when both are done. Thanks, both for working through this PR with me!

@sevgibson sevgibson marked this pull request as draft September 21, 2020 23:24
Copy link
Collaborator

@tiegz tiegz left a comment

Choose a reason for hiding this comment

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

@sevgibson I've added PR suggestions for the 2 fixes I mentioned. I've tested this locally and it fixes the feature for me.

# Setting headers_key to nil is the accepted way to disable
# single_access_token in headers.
return nil if headers_key.nil?
controller.headers[headers_key]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
controller.headers[headers_key]
controller.request.headers[headers_key]

@@ -269,6 +269,14 @@ def unset_params
controller.params["user_credentials"] = nil
end

def set_headers_for(user)
controller.headers["user_credentials"] = user.single_access_token
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
controller.headers["user_credentials"] = user.single_access_token
controller.request.headers["user_credentials"] = user.single_access_token

end

def unset_headers
controller.headers["user_credentials"] = nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
controller.headers["user_credentials"] = nil
controller.request.headers["user_credentials"] = nil

Comment on lines +39 to +42
def headers
controller.headers
end

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
def headers
controller.headers
end

@@ -1849,6 +1894,10 @@ def params_key
build_key(self.class.params_key)
end

def headers_key
build_key(self.class.headers_key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
build_key(self.class.headers_key)
# Rack servers will uppercase header names, convert hyphens to undercsores, and prefix with "HTTP_" in order
# to comply with CGI. We translate these converted headers in a similar way to Rails:
# https://github.com/rails/rails/blob/6-0-stable/actionpack/lib/action_dispatch/http/headers.rb#L123-L126
"HTTP_" + build_key(self.class.headers_key).upcase.tr("-", "_")

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

Successfully merging this pull request may close these issues.

None yet

3 participants