garrison
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.
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming











Showing Posts 1 to 6- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
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.
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_idinto 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.CSRFProtectionmodule, 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 thatdelete_csrf_token/0must 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.CSRFProtectiondocs actually explicitly state that you should calldelete_csrf_token/0after 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
garrison
So I went ahead and actually tested this, and to my surprise the CSRF token is actually cleared after login/logout. What I can’t for the life of me figure out is where it gets cleared. I thought maybe there was some weird interaction with
configure_session(:renew)being executed via callback at the end of the pipeline (it would be executed after the CSRF callback because:fetch_sessionis run first and they run in reverse order). But renewing the session doesn’t clear the data written into it after login (i.e. the user token, which obviously survives). The session is cleared byclear_session/1, which would be run before the CSRF callback. I’m curious if anyone can find the code responsible for resetting the token.I don’t use GitHub, but I went ahead and tested a one-line patch to call
delete_csrf_token/0when the session is cleared, and it seems to work fine (tests pass etc). The relevant line in the template is here.As I mentioned above, though, this change is not strictly necessary - I just have no idea why!
josevalim
So just to confirm the testing you went through:
?
garrison
This is close to what happens, but not quite right because
get_csrf_token/0is called on template render, so the masked token is not written into the process dictionary until then. ThereforeProcess.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:nilbecause it isn’t generated untilget_csrf_token/0is called during template render."abc", is printed for the token."abc", is again printed.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."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/0was added to theauth.excode.I should also note that this was tested with the Controller version of phx.gen.auth, not the LiveView version.