Search code examples
securityauthenticationaccess-tokenrestful-authenticationrefresh-token

Why not to have two access_tokens and two refresh_tokens stored one in cookie and one in localstorage to protect from XSS and CSRF


Background

Scenario is that we have a REACT SPA and a single API which is both a resource server and an authentication server. We want to implement simple tokens based auths. There is no dedicated SSO server so no need for OAuth2.

I've read lot of articles for the last two days on how to do this correctly and there is one thing that still bothers me. There is a lot of discussion whether the tokens should be stored in the LocalStorage or a cookie? First is vulnerable to XSS attacks because injected code can steal your data from the LocalStorage but protects against CSRF because forged request from malicious website won't be able to steal it. The latter is vulnerable to CSRF because forged request from malicious website makes browser send the cookies along but protects against XSS because malicious website don't have access to LocalStorage of the original website.

Question: then why not use both?

Why nobody creates two JWTs signed with two different keys and two different refresh_token secrets? One JWT and refresh_token is then stored in the LocalStorage and the other in a cookie. If I ask google it's always one vs. another. Is having both an anti pattern or a bad idea in any way? Because it is not that much work to implement that...

How do I imagine this should work

Here's how I think such flow can work:

  1. User enters login and password on a login form
  2. React app sends login request with login and password to /api/auth/login. In return it gets:
  • in body: JWT access_token signed by private_key1 and some secret refresh_token generated for this particular user and stored in some kind of persistence layer
  • as an httpOnly cookie: JWT access_token signed by private_key2
  • as an httpOnly cookie with path set to /api/auth/refresh: some secret refresh_token generated for this particular user and stored in some kind of persistence layer that is different than the one returned in body
  1. React app, with every next request, sends the first JWT as a value of Authentication header and the second one is attached automatically by the browser as a cookie.
  2. API checks signature of both and if both are correct then authenticates the request
  3. 3 - 4 repeats until any of the tokens expire (I assume their expiration time is the same but let's be more generic for the sake of this step through)
  4. React app sends a request to /api/auth/refresh with JWT and refresh_token from LocalStorage and browser attaches second JWT as a cookie and the other refresh_token as a cookie as well because the path condition for the cookie is met.
  5. API validates the signature of the expired JWTs and checks if both refresh_tokens exist in the persistence layer for this user and have not expired. If everything is fine new JWTs and also new refresh_tokens are generated and returned to the react APP same way as in point 2.

On logout refresh tokens are removed from the persistence layer so access tokens simply won't be refreshed.

Why do you need this refresh tokens if you think this flow is more secure?

I came to a conclusion that, indeed, adding refresh tokens doesn't increase security for users and have the drawback that there is this small gap after logout/revoke when JWTs are still valid but the refresh tokens are revoked/user logged out but have a lot of other pros that I managed to gather from many many discussions from this portal:

  • Validating only JWT's signature is much faster than checking both signature and if it has not been revoked/user logged out in the persistence layer (probably DB). These checks have to be done only for refresh tokens so much less frequent.
  • Refreshing tokens, because is less frequent, can take a bit longer which let us do here some additional validation of, for example, user IP (if hasn't suddenly changed to China since the last refresh) or if number of requests hasn't increased dramatically for the last 4 minutes etc.
  • Because refresh tokens are much more important you can put much more focus on securing this one endpoint - e.g., if you log requests and responses it is more likely that access_token will leak in your logs but when seeing a pull request that touches /api/auth/refresh your team may have a policy that all team members need to accept it plus maybe someone from security team
  • Claims in JWTs are refreshed every 4 minutes (or whatever expiration time you choose for your short lived JWTs)
  • If your private keys leak you can change them on the fly. It will immediately invalidate all existing JWTs but refresh tokens are safe so users won't be logged out. Instead their JWTs will be refreshed in the background with next request (if implemented correctly in fronted of course).

Solution

  • We have implemented this flow and here are two main conclusions:

    • This flow works great and gives more security when API and frontend are hosted on the same domain (e.g., api.example.com and example.com)
    • httpOnly cookies are blocked by the Safari browser when API and frontend are hosted on a different domain (e.g., frontend.com and api.com)