Microservices are a great tool when it comes to designing scalable and extensible architectures. They can be used to encapsulate different behavior...
For further actions, you may consider blocking this person and/or reporting abuse
And what about refresh token?
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.
Super content. Ty!
Very interesting article.
I am wondering what happens if an attacker intrudes between the client app and backend? I believe this could be dangerous if the attacker captures the password and username.
Indeed, this is why you use HTTPS, so that a man in the middle attack is not possible.
What do you do when you have different microservices that require the user to be authenticated? Do you make a request to the auth microservice with the JWT that comes on the Cookie in every request? or just validate that the JWT is not expired with jwt.verify?
No it won't. That's why we have "Domain" attribute. A cookie will not be sent if our server, and malicious one, don't share the same domain: developer.mozilla.org/en-US/docs/W....
Nice article! Something about the discussion between session-based tokens is funny though: One could always use a "normal" JWT and add the application server session as a claim. (In fact, this is how our organization does it.)
Kudos for pointing to the problems with the local storage, and the cookie alternative. I came to the same conclusion after lots of research.
I was thinking the same. Indeed, they can. But stealing a token is still worse than making a request on behalf of a user. With a stolen token, an attacker can make requests not just for a predetermined set of API calls (the ones coded in your client app), but also, on other services that require the stolen access token. In the case if we store an access token in an httpOnly cookie, the attacker can make a request only for a limited set of API calls. Other services could remain isolated.
Thanks for the article. I have two questions about JWT:
Great Content Fernando, thanks! :)
This is awesome, thanks for sharing. Great amount of detail!
Did you had a look at PASETO already? :)
Without https not even data encrypted, you can't guarantee that server is actually your trusted server.
Thanks for the article. I think no application should ever store JWTs in a storage. JWTs are short lived.