Tuesday, June 1, 2021

Exploring JWT refresh tokens

So JWTs are a very common stateless authentication token that are generated server side, and sent to a client. Every request that the client makes has to have a valid JWT, very similar to a session cookie in a 3 tier web application. The big difference though is that in a session cookie, the session is stored on the server and the session cookie is compared every single time, on every request.

JWT statelessness

With JWTs there is no server side state - and hence nothing to compare against. This apparently has huge performance benefits - which is probably true just because it's repeated so many times everywhere :). The access token is sent as part of every request. The refresh token is not; its only sent to an authorization server when you want a new access token. The access token is verified to see if its been tampered with (signature field) but not verified against any server side state.

Storage

If the client is a mobile app, both tokens are stored on the client device - I explicitly state this as its important when we consider the threat model of a token getting leaked. So if the device is stolen, the tokens are stolen. If there is a vulnerability in some 3rd party dependency you have no clue about, the tokens could be stolen. Or maybe your own code has a vulnerability and you accidentally leak these tokens or log them in an unsafe place.

Token leakage

Now JWT tokens once assigned cannot be expired whenever we choose. There is no way to do it. You can though set an expiry timestamp in the token, and it'll die after that. So the shorter the validity, the better it is. Coz if you set an access token to have a 24 hour validity, and it gets stolen - well guess what, the attacker can use that for 24 hours and there isn't a thing you can do about it. Same deal with refresh tokens, only worse, as the attacker can keep minting new access tokens with the stolen refresh token.

Except that you can revoke refresh tokens if you detect a leak if you want to. It does come at a cost though.

Statefulness - 1

But this then means that you have to maintain state for the refresh tokens on your server. The next time you see the leaked refresh token, you basically remove that entry server side so its no longer recognized as valid, mint a new refresh token and give that to the client. Remember here, that any access tokens already minted will not be expired. So refresh tokens help you in the future - they do not save you from the past.

Statefulness - 2

And so, you might think that sounds bad, and decide to keep a table to track every access token ever minted for a specific refresh token. That way when you see the access token got leaked, you can go and take it off the list and deny future requests. You can check every single request to be safe. Which would totally work, except that you've lost a lot (if not all) the benefits of having a stateless JWT, which is no server side state.

Middle ground

And so once you realize the above, a common middle ground is to keep a very short lived access token (5-15 minutes) and a relatively long lived refresh token (60 minutes - 24 hours) and gamble on everything else. By that I mean, that you hope neither gets stolen somehow. If the access token does get stolen, well that sucks but at least the attacker can only do 15 minutes worth of damage. Refresh tokens you hope are never stolen.

Middle ground - Security

Which all sounds great, but it doesn't defend you against if they get stolen.

  • You almost certainly have to invest in tooling that will detect leakage of these tokens.
  • You have to write secure code to store refresh tokens server side securely.
  • You have to write secure code to exchange refresh tokens for new access tokens and refresh tokens.
  • You have to store refresh tokens securely on your mobile client and make it as hard as possible for someone who steals the device to get to your token.

All of which is doable of course, but it takes time, money and commitment from a business to build the above systems in a secure manner.

My secure solution which is the best ever (in my mind anyway ;))

Assuming we continue with the stateless JWTs, I'm seeing very little benefit for the refresh tokens and a lot of downside. So I'd strongly recommend we ditch them completely.

  • We just take an access token (15 min expiry), check if its valid and give the client a new access token back.
  • Keep the old access token valid for another 5 minutes maybe, to defend against dropped requests. After 5 minutes only respect requests with the new token.
  • Save the state of the client server side so they can get back to wherever they were at any point, in case of flaky network connections.
  • Invest in the tooling to detect leaked tokens. DO THIS even if you ignore the entire blog and have a permanent token till the end of time on the client's device.
  • Of course the access token can get stolen too, but the damage is controlled, compared to the solution with the refresh token.
  • Try your best to authenticate the client, but make them reauthenticate after all attempts have failed.

What are the holes in the world's best solution? :). Is there something in refresh tokens that I have missed apart from the fact that its there to help the client not sign in again easily? Are they worth the complexity?