Refresh Tokens and Token Rotation in NestJS: Secure JWT Authentication

As you build secure applications with NestJS and JWT authentication, one critical concern is token expiration. Access tokens should have short lifespans for security, but that often forces users to log in frequently. This is where refresh tokens come in — allowing you to issue new access tokens without requiring the user to log in again.

In this module, you’ll learn how to implement refresh tokens, understand token rotation, and ensure your authentication flow is both user-friendly and secure.


Table of Contents

  1. What Are Refresh Tokens?
  2. Why Use Refresh Tokens?
  3. How Refresh Token Flow Works
  4. Generating and Issuing Refresh Tokens
  5. Storing Refresh Tokens Securely
  6. Token Rotation: Preventing Replay Attacks
  7. Implementing Refresh Token Endpoint in NestJS
  8. Best Practices
  9. Conclusion

What Are Refresh Tokens?

A refresh token is a long-lived token that is used to obtain a new access token after the original one expires. Unlike the access token, which is usually valid for a few minutes, refresh tokens can last for days or even weeks.


Why Use Refresh Tokens?

  • Short-lived access tokens reduce the risk of token theft.
  • User doesn’t have to log in again after the token expires.
  • Enables automatic session renewal (e.g., mobile apps, SPAs).
  • Supports secure token rotation mechanisms.

How Refresh Token Flow Works

  1. Login: User provides credentials → server returns an access token + refresh token.
  2. Access Token Expires: Client detects expiry.
  3. Refresh Request: Client sends the refresh token to a dedicated endpoint.
  4. New Token Issued: Server verifies refresh token → generates a new access token (and optionally a new refresh token).
  5. Token Rotation (optional): Replace the used refresh token with a new one.

Generating and Issuing Refresh Tokens

You can generate a refresh token similarly to an access token, but with a different secret and longer expiration:

tsCopyEdit// auth.service.ts
async generateTokens(user: any) {
  const payload = { sub: user.id, email: user.email };

  const accessToken = this.jwtService.sign(payload, {
    secret: process.env.ACCESS_TOKEN_SECRET,
    expiresIn: '15m',
  });

  const refreshToken = this.jwtService.sign(payload, {
    secret: process.env.REFRESH_TOKEN_SECRET,
    expiresIn: '7d',
  });

  return { accessToken, refreshToken };
}

Storing Refresh Tokens Securely

Options:

  • HTTP-only cookies (recommended for web apps).
  • Database (to support token revocation and rotation).
  • Client-side local storage (less secure, not recommended).

Example: Storing hashed refresh tokens in DB

tsCopyEditasync storeRefreshToken(userId: number, token: string) {
  const hashed = await bcrypt.hash(token, 10);
  await this.usersService.update(userId, { refreshToken: hashed });
}

Token Rotation: Preventing Replay Attacks

Token rotation means replacing the refresh token every time it is used. This prevents replay attacks where a stolen token could be reused indefinitely.

Steps for rotation:

  1. Store hashed refresh tokens in the database.
  2. When a refresh token is used:
    • Validate it.
    • Invalidate/delete it.
    • Issue and store a new one.
  3. Reject reused or expired tokens.

Implementing Refresh Token Endpoint in NestJS

Here’s an example of a /auth/refresh endpoint:

tsCopyEdit// auth.controller.ts
@Post('refresh')
async refresh(@Body() body: { refreshToken: string }) {
  const user = await this.authService.verifyRefreshToken(body.refreshToken);
  const tokens = await this.authService.generateTokens(user);
  await this.authService.storeRefreshToken(user.id, tokens.refreshToken);
  return tokens;
}
tsCopyEdit// auth.service.ts
async verifyRefreshToken(token: string) {
  try {
    const payload = this.jwtService.verify(token, {
      secret: process.env.REFRESH_TOKEN_SECRET,
    });

    const user = await this.usersService.findById(payload.sub);
    if (!user || !(await bcrypt.compare(token, user.refreshToken))) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    return user;
  } catch {
    throw new UnauthorizedException('Invalid refresh token');
  }
}

Best Practices

  • Use different secrets for access and refresh tokens.
  • Use HTTP-only cookies for web clients to reduce XSS risks.
  • Store refresh tokens hashed in the database.
  • Implement refresh token rotation to prevent reuse.
  • Blacklist or expire old tokens if a new one is issued.

Conclusion

Refresh tokens significantly enhance the security and usability of your authentication system in NestJS. With proper token rotation and validation, you can prevent common vulnerabilities while ensuring seamless user sessions.