DEV Community

Álvaro Veiga
Álvaro Veiga

Posted on

Implement Refresh Token Automatic Reuse Detection without cluttering your database

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())
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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 },
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
noinkling profile image
Malcolm • Edited

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?

Collapse
 
alvaromrveiga profile image
Álvaro Veiga

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

Collapse
 
urielsouza29 profile image
Uriel dos Santos Souza

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

Collapse
 
zuluana profile image
Oranda • Edited

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).