Welcome back! Last time we discussed secret key management and today, we’re diving into one of the trickiest aspects of JWT implementation – token revocation and refresh tokens. It’s like trying to take back an email after you’ve hit send; not impossible, but definitely requires some clever maneuvering and good timing!.
Disclaimer: I am not a JavaScript developer; I hate JavaScript, so please use all of my code snippets as inspiration. You might find better examples on StackOverflow.
The token revocation conundrum
One of the oft-touted benefits of JWTs is that they’re stateless. The server doesn’t need to keep track of active sessions. But this becomes a double-edged sword when you need to invalidate a token before its expiration time.
Imagine this scenario: you notice suspicious activity on a user’s account. You want to invalidate their current session immediately. With traditional server-side sessions, you’d just delete the session. With JWTs? Well, you’re in for a fun ride.
Approaches to token revocation
1. The ostrich approach (NOT recommended!)
This is where you stick your head in the sand and pretend the problem doesn’t exist. You simply wait for the token to expire.
```javascript
// The "la la la, I can't hear you" method
const token = jwt.sign({ userId: 123 }, secretKey, { expiresIn: '15m' });
```
Pro: It’s easy!
Con: Not a great look or incident response capability.
2. The block list method
Keep a block list of revoked tokens. When a token needs to be invalidated, add it to the list.
```javascript
const revokedTokens = new Set();
const revokeToken = (token) => {
revokedTokens.add(token);
};
const isTokenRevoked = (token) => {
return revokedTokens.has(token);
};
```
Pro: relatively simple to implement.
Con: as your block-list grows, so does your “stateless” server’s state.
3. The version control method
Include a version number in the JWT payload. When token revocation is needed for all tokens, increment a global version number on the server.
```javascript
const globalTokenVersion = 1;
const createToken = (userId) => {
return jwt.sign({ userId, tokenVersion: globalTokenVersion }, secretKey);
};
const verifyToken = (token) => {
const decoded = jwt.verify(token, secretKey);
return decoded.tokenVersion === globalTokenVersion;
};
```
Pro: Allows for mass revocation of tokens.
Con: Can’t revoke individual tokens without affecting others.
Enter the refresh token
Now let’s talk about refresh tokens; the long-lived counterparts to short-lived access tokens. They’re like the backup singers to the JWT’s lead vocalist – not always in the spotlight, but crucial to the performance.
The idea is simple:
- User logs in and receives both an access token and a refresh token.
- The access token is short-lived (say, 15 minutes).
- When the access token expires, the client uses the refresh token to get a new access token.
```javascript
const createTokenPair = (userId) => {
const accessToken = jwt.sign({ userId }, accessSecretKey, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, refreshSecretKey, { expiresIn: '1d' });
return { accessToken, refreshToken };
};
const refreshAccessToken = (refreshToken) => {
const { userId } = jwt.verify(refreshToken, refreshSecretKey);
return jwt.sign({ userId }, accessSecretKey, { expiresIn: '15m' });
};
```
The refresh token tango
Using refresh tokens adds a new layer of complexity, but also provides more control:
- Security: if an access token is compromised, it has a limited lifespan.
- User experience: users don’t need to log in frequently.
- Revocation: you can revoke a user’s session by invalidating their refresh token.
But beware! Refresh tokens are powerful and long lived. Treat them like the crown jewels:
- Store them securely (HttpOnly, secure cookies are a good start).
- Implement strict validation on the server.
- Consider using a rotating refresh token scheme for extra security.
A cautionary tale: the Microsoft misstep
Here’s where Microsoft provides us with a valuable lesson in what not to do: they allow refresh tokens (amongst other things) to be used to obtain credentials for different applications and services. This design choice (interesting, I know) creates an attack vector for horizontal privilege escalation.
The safer approach? Limit your refresh token scope. Each refresh token should only be valid for re-authentication to the specific service it was initially granted for. Think of it like a hotel key card – it should only work for your room, not the entire hotel chain.
Do not follow in Microsoft’s footsteps when it comes to refresh tokens – limit the scope so users can only reauthenticate for the service they authenticated to. You don’t want to give attackers an easy route for horizontal privilege escalation.
If you’re looking to abuse this, check out TokenTactics by Steve Borosh.
The rotating refresh token scheme
This is like playing hot potato with your tokens:
- Every time a refresh token is used, invalidate it and issue a new one.
- If someone tries to use an invalidated refresh token, sound the alarms!
```javascript
const rotateRefreshToken = (oldRefreshToken) => {
const { userId } = jwt.verify(oldRefreshToken, refreshSecretKey);
invalidateRefreshToken(oldRefreshToken);
return createTokenPair(userId);
};
```
This way, if a refresh token is stolen, the window of opportunity for misuse is limited.
Token revocation and refresh tokens: in conclusion
Token revocation and refresh tokens are like the advanced yoga poses of the JWT world. They require flexibility, balance, and a good deal of practice to get right. But master these techniques, and you’ll have a JWT implementation that’s both secure and user-friendly.
Remember:
- Short-lived access tokens limit the damage of token compromise.
- Refresh tokens provide a better user experience but need careful handling.
- Always have a strategy for token revocation, even if it’s not perfect.
- Logging and alerting are your friends
Stay tuned for our next post, where we’ll dive into the great debate: storing JWTs in cookies vs local storage.