garrison

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:

  1. When a user logs in, a user_token (a long random string) is generated via the Accounts context
  2. Assuming “Remember me” is checked, the token is stored in a remember_me_cookie named app_user_remember_me
  3. The token is then copied from the remember_me_cookie into a user_token value stored in the Plug session
  4. That user_token value in the Plug session is used to authenticate and store the current_user via the Plug/LiveView pipelines
  5. 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.

Showing Posts 1 to 6

josevalim

josevalim

Creator of Elixir

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

garrison OP

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

josevalim

Creator of Elixir

Excellent find. Would you like to submit a pull request to the generators? Please tag me so I can review it. Thank you :heart:

garrison

garrison OP

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_session is 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 by clear_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/0 when 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

josevalim

Creator of Elixir

So just to confirm the testing you went through:

  1. You accessed the page to print the CSRF masked/unmasked values, two values were shown
  2. You logged in
  3. You accessed the page to print the CSRF masked/unmasked values, no values were shown

?

garrison

garrison OP

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:

  1. On the first page load, the token is nil because it isn’t generated until get_csrf_token/0 is called during template render.
  2. 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.
  3. After loading the login form page, the same token, "abc", is again printed.
  4. After logging in, the token printed is nil for 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.
  5. 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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews