While studying how to implement refresh tokens rotation in a Node.js project I came into this blog post from Auth0: What Are Refresh Tokens and How to Use Them Securely. In the section where they explain about Refresh Token Automatic Reuse Detection it is said:
The 🚓 Auth0 Authorization Server has been keeping track of all the refresh tokens descending from the original refresh token. That is, it has created a "token family".
Refresh Token Automatic Reuse Detection section
But if the tokens are never compromised and the application is used regularly by many users that would mean lots of inactive refreshed tokens cluttering the database before expiration.
A Solution
You can add a family property in your refresh tokens model in the database, this is my model using Prisma ORM:
model UserTokens {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
refreshToken String
family String @unique
browserInfo String? // Show the user logged devices
expiresAt DateTime
createdAt DateTime @default(now())
}
The family receives a v4 UUID when the user logs in and a brand new refresh token is created.
The tokenFamily is added to the refresh token payload for future refreshes:
In the following code snippets I'm using NestJS framework and TypeScript
/** Creates the refresh token and saves it in the database */
private async createRefreshToken(
payload: {
sub: string;
tokenFamily?: string;
},
browserInfo?: string,
): Promise<string> {
if (!payload.tokenFamily) {
payload.tokenFamily = uuidV4();
}
const refreshToken = await this.jwtService.signAsync(
{ ...payload },
refreshJwtConfig,
);
await this.saveRefreshToken({
userId: payload.sub,
refreshToken,
family: payload.tokenFamily,
browserInfo,
});
return refreshToken;
}
Now that we have our refreshToken created and stored we can use it to refresh the accessToken and rotate the current refreshToken. But first we need to validate it:
/** Checks if the refresh token is valid */
private async validateRefreshToken(
refreshToken: string,
refreshTokenContent: RefreshTokenPayload,
): Promise<boolean> {
const userTokens = await this.prismaService.userTokens.findMany({
where: { userId: refreshTokenContent.sub, refreshToken },
});
const isRefreshTokenValid = userTokens.length > 0;
if (!isRefreshTokenValid) {
await this.removeRefreshTokenFamilyIfCompromised(
refreshTokenContent.sub,
refreshTokenContent.tokenFamily,
);
throw new InvalidRefreshTokenException();
}
return true;
}
/** Removes a compromised refresh token family from the database
*
* If a token that is not in the database is used but it's family exists
* that means the token has been compromised and the family should me removed
*
* Refer to https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation#automatic-reuse-detection
*/
private async removeRefreshTokenFamilyIfCompromised(
userId: string,
tokenFamily: string,
): Promise<void> {
const familyTokens = await this.prismaService.userTokens.findMany({
where: { userId, family: tokenFamily },
});
if (familyTokens.length > 0) {
await this.prismaService.userTokens.deleteMany({
where: { userId, family: tokenFamily },
});
}
}
If the token is invalid but the family exists that means this is a token descending from the original refreshToken, so that family was compromised and should be removed.
Conclusion
To implement Refresh Token Rotation Automatic Reuse Detection without storing all refresh tokens descending from the original one you can create a tokenFamily property in your database model and check for unregistered descendants.
I did not go into full details on how I implemented the whole authentication process in this article, but if you want you can check the source code in the project's repository in GitHub
Top comments (4)
Correct me if I'm wrong, but in the example repo it seems that the only thing ensuring that the tokens within a given family are different is the expiry time, which seems a little brittle/non-obvious. Do you think it would be a good idea to add another source of randomness like a nonce?
Hi, Malcolm! Thank you for your question!
In the createRefreshToken function a version 4 UUID is being assigned as the new token family, adding the source of uniqueness. I used this npm package
Do you have more links about?
pt-BR
Como você é brasileiro.
Só achei seu post sobre isso!
Gostaria de mais links sobre o assunto de como implementar
Ou um post mais detalhado! Esse esta pouco para iniciantes
Abraços
Great article! Just wondering, but is it common to add the family as a property of the refresh token itself? This way, all I'd need to store is a map from family to the latest token in the family. I can obtain the family from the current refresh token. This way, I have no need to go to the DB and can store everything in Redis (backed up to disk).