DEV Community

Discussion on: JWT Authentication Best Practices

Collapse
 
branislavlazic profile image
Branislav Lazic • Edited

Now, that's a good point since it brings a crazy amount of complexity. It feels like refresh token mechanism doesn't even pay off if you have a single API service. Let's dive deeper.

So, what is it even? Let's say, a long lived token used strictly to obtain a new access token or to revoke itself!

How does it look? How do I even generate it?
Basically, you can use JWT format here too. Nothing wrong if you have a same format for access and refresh token. If you want the custom one, the safest option is to use a proven library for random strings that cannot be easily guessed (not the time based UUID's or timestamps!) and sign then with e.g. HMAC.

Now, since we know their nature and how they look, let's see how to use them.

If we make refresh token a long lived stateless JWT token, we are in a deep trouble. In case if it gets stolen, there is no effective mechanism to revoke it. The only mechanism is to change a token secret which might also require our API service downtime. Basically, this will log out ALL users since their refresh tokens will fail to verify against a new secret key. And they will have to login again.

What to do?
Save it in the data store. What kind of data store? Ideally the one that can be scaled effectively, but scaling of refresh token data store is not a big deal, since that data store doesn't handle a huge load. Refresh token is sent relatively rarely. Ideally, only after an access token expires. And it means that you have a state you have to maintain.

But wait! There's a potential danger in storing refresh token! Because, you don't want to store refresh token, but rather, its meta data. E.g. ID, user ID, expiry time... Storing the whole refresh token is similar to storing plain text passwords. In case if database leaks, refresh tokens will be compromised. On the other hand, meta data is useless.

So far so good.

What about an authentication flow itself?
Login -> we get both access and refresh token -> after the access token expires, we send refresh token to obtain a new access token
Logout -> we send refresh token -> we confirm it's validity -> if valid, we parse it, extract the ID, and delete it, or mark it as "used" in the data store by its ID -> if not valid (maybe expired), also good, since it can't be used to obtain new access tokens

But again, a tricky situation. What if someone steals a refresh token? It means they could use it to obtain access tokens for quite a long time.

Refresh token rotation to the rescue! Basically, each time we exchange a refresh token for an access token, we respond with a new refresh and access token, and mark the old refresh token in the data store as "used".

Imagine a scenario. An attacker steals a refresh token -> they use it to obtain a new access token and refresh token (old refresh token is marked as "used") -> our user tries to use the same refresh token to prolong their session -> server checks whether the refresh token is used -> BOOM! It is! -> server revokes all refresh tokens for that user.

But that's just one security mechanism... Imagine if an attacker actually waits for a user to close their browser and uses a last active refresh token. It means that a user will not present their refresh token any soon. An attacker can use that users offline time to do whatever they want.

So yeah, it's tricky as hell. For a single service (that even requires scaling to multiple instances), maybe it's way better to use sessions serialized in a data store such as Redis or an external authorization server such as Keycloak if you really want to go with tokens.