-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Add support for sending and receiving the auth token via a server cookie #1453
Conversation
@@ -55,6 +55,16 @@ def destroy | |||
user.tokens.delete(client) | |||
user.save! | |||
|
|||
if DeviseTokenAuth.cookie_enabled | |||
if DeviseTokenAuth.cookie_attributes[:domain] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized that we might need to be smarter about determining the domain
attribute. We might need to determine it by looking at the request's domain (in which case we'd probably want to allow configuring a whitelist, like we do with redirect_whitelist
). This is because the Domain
cookie attribute only allows one value, so as the code sits now you wouldn't be able to handle multiple domains being served from the same box.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense, remember to use allowlist instead of whitelist
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@apuntovanini Another option is to just support passing to the cookie_attributes[:domain]
config either a string, which will work for most people, or a proc / lambda, which could accept the request and do whatever logic (in our case, return the domain of the request so long as it matches the allow list).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here are the changes for that second option I mentioned. And our project's config looks like:
config.cookie_attributes = {
domain: lambda { |request| request.domain if valid_cookie_domains.include?(request.domain) },
secure: Rails.env.production?,
httponly: true,
same_site: 'Strict',
expires: token_lifespan.from_now,
encrypt: true
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 I just saw this in the in the Rails docs:
To support multiple domains, provide an array, and the first domain matching request.host will be used. Make sure to specify the :domain option with :all or Array again when deleting cookies.
So I can totally gut this Proc logic now, and instead just pass along either a string or Array from the config to the cookie call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See this commit. So much cleaner! I wish I had realized that earlier, lol.
I tested in our application and works like charm. I only have issues logging out (we use a graphql endpoint to make our calls, we should probably switch to classic controllers flow for authentication instead). But sharing cookies with our subdomains is crucial to us, well done! |
Yay, thanks so much! I'm so glad this will help y'all as well! |
elsif config_value.is_a?(Proc) | ||
config_value.call(request) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See this discussion
@@ -9,6 +9,7 @@ test/dummy/.sass-cache | |||
test/dummy/config/application.yml | |||
coverage | |||
.idea | |||
.byebug_history |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive-by that popped up while debugging a test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like using cookies in APIs but your reasoning of why is a good alternative sounds good
if cookie_domain.present? | ||
# If a cookie is set with a domain specified then it must be deleted with that domain specified | ||
# See https://stackoverflow.com/a/6244724/1747491 | ||
cookies.delete(DeviseTokenAuth.cookie_name, domain: cookie_domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is unreached by specs but it could be ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This conditional will be moot now, see this recent commit.
|
||
cookie_domain = get_cookie_domain | ||
if cookie_domain.present? | ||
cookie_attributes.merge!(domain: cookie_domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is unreached by specs but it could be ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This conditional will be moot now, see this recent commit.
# more flexibility, such as to dynamically choose the domain when serving multiple domains | ||
# from a single server, you can set a Proc that will be passed the request. | ||
config_value = DeviseTokenAuth.cookie_attributes[:domain] | ||
if config_value.is_a?(String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These cases are unreached in specs but that could be ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moot now that I've gutted the unnecessary Proc logic.
@@ -35,11 +35,20 @@ def set_user_by_token(mapping = nil) | |||
access_token_name = DeviseTokenAuth.headers_names[:'access-token'] | |||
client_name = DeviseTokenAuth.headers_names[:'client'] | |||
|
|||
# gets values from cookie if configured and present |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can all this new cookie code be in a new module like set_user_by_cookie
or something like that, so we don't pollute more this big file, you can include the concern/module in this file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can, my only push back would be that hopefully in the future we'll allow turning off headers (for the security reasons mentioned in point two of the description). In that case, it might feel a bit disjointed to have the header logic in set_user_by_token
while the cookie logic is in set_user_by_cookie
, when really both logics deal with handling the token. In that future world, would we move the header logic to another concern called set_user_by_headers
? If so, wouldn't set_user_by_token
just become a skeleton file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, see my recent realization. That may clean up the amount of noise in this file considerably.
Just to understand, how would how tackle cross-subdomain authentication with api without a cookie? Thanks! |
…ty with setting domain to the Proc if the Proc returns nil. clean up unncessary present conditionals.
Yeah, we weren't too keen on it either. But it's the only way to share state across subdomains. And the general consensus seems to be that it's more secure for auth (once you're using only the HttpOnly cookie, which we'll hopefully allow in later work). |
@MaicolBen @booleanbetrayal Could one of you retry the failed Travis job? It seems that just one of the jobs failed with:
|
@theblang - running this now produces a new error:
It looks like Devise has hard-coded the omniauth dependency in a way that it's breaking it's 2.0.0 release. It would be preferable if they fixed this on their end, but we might want to constrain the Gem version in the project if they are slow with a release. (cc: @MaicolBen ) |
I'm going to wait for heartcombo/devise#5326 for a few days |
Looks like we're close! |
Can you rebase/update with last master? |
There are two main reasons to use a server cookie to send and receive the auth token.
Domain
attribute. From the MDN docs:Note that you cannot access
localStorage
across subdomains.HttpOnly
server cookie then JavaScript can never access it, which helps protect against XSS attacks. These three issues are mostly pertaining to the security aspect: Where to store token securely? #1281 Storing auth token in browser best practise #1321 Does DTA support HTTP Only Cookie Refresh tokens along site access tokens? #1371This PR introduces a server cookie as an additive opt-in feature to send and receive the auth token, which will allow for the subdomain sharing mentioned in point one. By introducing the option to use a server cookie, we lay the groundwork for addressing the security aspect in point two. I didn't want to make the changes for point two yet as that will be more impactful, because we'd need to make it possible to not expose the auth token in headers. I imagine if and when we do that work we'll probably add an analogous
headers_enabled
config to hinge off of in code to turn off sending and receiving the auth token in headers.cc @booleanbetrayal @nbrustein @apuntovanini