garrison
Why does phx.gen.auth use a separate "remember_me_cookie"?
I am in the process of designing my own auth system, and as a result I’ve had a close look at phx.gen.auth to get a feel for best practices.
Most of the design choices are quite straightforward and clear, which is great, but one thing I’ve been wondering about is the choice to use a separate remember_me_cookie to store the user_token outside the Plug session.
For anyone who is unfamiliar, it works like this:
- When a user logs in, a
user_token(a long random string) is generated via the Accounts context - Assuming “Remember me” is checked, the token is stored in a
remember_me_cookienamedapp_user_remember_me - The token is then copied from the
remember_me_cookieinto auser_tokenvalue stored in the Plug session - That
user_tokenvalue in the Plug session is used to authenticate and store thecurrent_uservia the Plug/LiveView pipelines - Since the Plug session is, by default, stored as a session cookie (noobs beware: this is a completely different meaning of the word session), it will be lost whenever the browser feels like it (probably on restart), at which point we jump to Step 3
What I’m wondering here is why bother with the remember_me_cookie shuffle instead of just storing the token in a cookie directly? A good chunk of the code in auth.ex is dedicated to copying the token from the cookie to the session.
Also, storing the cookie in the session then necessitates clearing the session on login to prevent fixation attacks (as helpfully explained in a comment), but if the token was just stored in its own cookie this would not be necessary. Though, from what I can tell, it’s actually not necessary anyway by default as the default session implementation does not actually have a session id (it just encodes/signs the session data directly into the cookie), meaning a fixation attack would not apply. But I presume it was written this way in case users swap in their own Plug session backend (i.e. store it in Postgres), at which point fixation attacks would apply, and it’s better to handle that from the start.
Anyway, I suspect this separate cookie business is done just because it’s idiomatic to store the token in the session, but I figured I’d inquire in case there’s something I’m missing. I suppose it also handles the case of the user unchecking “Remember me” nicely, but you could just set the expiry of the cookie instead so I don’t think that’s the reason.
Most Liked
garrison
This is exactly the sort of thing I figured I might be missing. Thanks!
Indeed, after having a closer look I see that LiveView does not expose cookies for exactly this reason, providing the session on mount instead. So I suppose it would be possible to use a Plug to inject a user_id into the session or something like that, but that seems messy and at that point the phx.gen.auth approach starts to make a lot of sense.
This is a good point, and it also makes sense to avoid fixation on the csrf token. Which actually raises another question:
Having a look at the Plug.CSRFProtection module, I see that the csrf token is actually stored in the process dictionary (an unusual choice) and then written back into the session at the end of the Plug pipeline via a callback. Doesn’t that mean that clearing the session is insufficient to prevent fixation on the csrf token, and that delete_csrf_token/0 must be called as well? Otherwise I would expect the csrf token to survive in the process dictionary and be written back into the new session after login. Perhaps I’m missing some other side-effect, though; I haven’t tested this.
Interestingly, the Plug.CSRFProtection docs actually explicitly state that you should call delete_csrf_token/0 after logging a user in.
josevalim
Excellent find. Would you like to submit a pull request to the generators? Please tag me so I can review it. Thank you ![]()
josevalim
It has been a while but I believe everything you said is correct.
The only reason we still write to the session is because the session is also shared with Channels/LiveViews. WebSockets are vulnerable to cross-site hijacking attacks, so cookies should only be read from WebSockets if you do CSRF validation, which we already do for session. Otherwise you have to re-implement that for cookies.
While this is true, I would still renew the session on login even if you write the token to a cookie. Otherwise it can be easy for someone to accidentally write something they did not intend to the fixated session id, which can now be tied to the logged in user.
Last Post!
garrison
This is close to what happens, but not quite right because get_csrf_token/0 is called on template render, so the masked token is not written into the process dictionary until then. Therefore Process.get(:plug_masked_csrf_token) was always nil when that debug Plug was invoked (because it runs before template render).
The exact sequence (for :plug_unmasked_csrf_token) is:
- On the first page load, the token is
nilbecause it isn’t generated untilget_csrf_token/0is called during template render. - After reloading the page, the unmasked token is in the process dictionary because it was generated after the first page load and then loaded from the session during the second page load. So a value, let’s say
"abc", is printed for the token. - After loading the login form page, the same token,
"abc", is again printed. - After logging in, the token printed is
nilfor the same reason as in Step 1. This is where I get confused as I can’t find the side-effect responsible for resetting this token in Plug, but it does get reset. - After reloading, a new unmasked token is printed (call it
"xyz"), having been generated on the previous page load just like in Step 2.
Logging out has the same effect (the token is nil, and regenerated before the next page load).
The behavior was exactly the same if delete_csrf_token/0 was added to the auth.ex code.
I should also note that this was tested with the Controller version of phx.gen.auth, not the LiveView version.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex










