0

In an effort to avoid having to deal with CSRF attacks, I'm trying to implement an auth flow that completely avoids using cookies. In most cases this makes one vulnerable to XSS attacks. However, according to this auth0 blog post, it can be safely done with access and refresh tokens with refresh token rotation and automatic reuse detection.

Auth Flow

  1. user logs in with username / email and password, user receives access token and refresh token
    • access token expires within minutes, refresh token within hours, days, weeks or even months
    • access token payload contains at least a user_id
    • refresh token is also a JWT and contains at least a session_id as its payload
  2. user requests resources by supplying the access token in the authorization header
  3. when access token expires, user requests a new access and refresh token pair using the refresh token
  4. repeat from step 2: user continues to request resources using the new access token until it expires

Details

  1. upon login a new ("initial" or "root") user session (row) is stored in a database:
    • session_id = "initial-session", parent_id = null, owner_id = user_id
  2. refresh token rotation
    • when a new pair of tokens is requested using the latest refresh token, a new user session (row) is stored in a database referencing the previous session (row) as the parent session (row):
      1. session_id = "child1-session", parent_id = "initial-session", owner_id = user_id
      2. session_id = "child2-session", parent_id = "child1-session", owner_id = user_id
    • this establishes a "session family" and, by extension, a "token family" as any given session_id directly corresponds to a single refresh token (session_id is the payload in the refresh token)
  3. automatic reuse detection
    • when creating a new row in the user sessions table with a parent_id (from the refresh token payload), it is ensured that the "parent session" (row) exists (not invalidated) and does not already have any children
    • if a "child session" (row) is found the entire "session family" is deleted from the database, no new token pair is generated for the user making the request, the user is effectively logged out
    • this prevents a potentially stolen refresh token being used more than once, and (after the access token has expired) logs out both the legitimate user and the token thief

Side note: I put "(row)" in parenthesis, because I consider all rows of one "session family" to represent the same user session. Meaning a single session row in the database is not necessarily a distinct session.

Questions

  1. Should the refresh token be a JWT? (with session_id as the payload)?
  2. What should be the payload of the two tokens? Is it safe to send both user_id and session_id in both access and refresh tokens?
  3. Is there a need to store the hashed refresh token in a database?
  4. With refresh token rotation in place is really it safe to store both tokens in local storage in the frontend?
  5. Is my premise even correct, that I am inherently protected against CSRF attacks if I avoid using cookies?

Discussion

1. Should the refresh token itself be a JWT? (with session_id as the payload?)

I've read, e.g. here, that JWTs are a poor choice for long-lived refresh tokens. But I'm not convinced by the reasoning and not clear about the alternative: random bytes as the refresh token.

  1. with a JWT
    • I don't need to store the expiration time, since it is "baked" into the JWT
    • I can retrieve the session_id from it to identify the appropriate row in the database to invalidate the refresh token
  2. with a random bytes refresh token, and its SHA-256 hash stored in the database
    • I would need to store the expiration time along with the hash, unless the refresh token should never expire
    • I'm unsure about how to safely identify the stored token hash with no session id
      • I would've used bcrypt for hashing, but it does not reproduce the same exact hash
      • Is the suggested hashing (SHA-256) reasonably secure?
      • Would the hash itself be the primary key? How else could I identify the row in the database to invalidate the refresh token?
2. What should be the payload of the two tokens? Is it safe to send both user_id and session_id in both access and refresh tokens?

Assumptions: access token contains only user_id, refresh token contains only session_id

The user / frontend would need to use the refresh token (containing the session_id) to log out of only a single session. Otherwise, either all of a user's sessions need to be logged out or the single session to log out from would need to be identified by other means, e.g. either IP or User Agent or both if such information was stored with each session.

Since I can get the user_id via the session_id, I don't see a necessity to include the user_id in the refresh token payload, but it could be useful to avoid a database query.

My conclusion:

  • access token contains both user_id and session_id
  • refresh token contains only session_id
3. Is there a need to store the hashed refresh token in a database?

This was suggested in this SO comment.

Under the assumption that each user session (row) gets a unique id, either incremental integers or uuids and since the JWTs are signed and thus tamper-proof, I don't see a need to store the hashed refresh token.
The implication is that one would ensure that the stored hashed refresh token matches the sent refresh token, which, in my case, contains the session_id to quickly find that row in the database. An attacker would need to change the session_id in the JWT, forge the signature, and - additionally - make sure the hash of the forged JWT matches the stored hash of the original JWT.

To me, it seems to add only a minor, maybe even superficial bit of security.

4. With refresh token rotation in place, is it really safe to store both tokens in local storage in the frontend?

This is suggested in the Auth0 blog post.

You Can Store Refresh Token In Local Storage

Yes, you read that right. When we have refresh token rotation in place, we can store tokens in local storage or browser memory.

The argument is that even a stolen refresh token (XSS) effectively expires at the same time the access token expires. By my understanding this is true under the assumption that the legitimate users continues browsing and will eventually use the refresh token that was stolen by the token thief, which will then logout both as described above.

Resources

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.