We use cookies to enhance your experience on the site
CodeWorlds

Refresh Tokens - renewing legion passes

Gate guardian of the Empire! Architect Vitruvius has noticed that one-time passes (access tokens) expire too quickly, forcing legionaries to log in constantly. It's time to learn the Refresh Token system - a mechanism that allows you to securely renew access without having to re-enter authentication credentials!

What are Refresh Tokens?

Imagine a pass system in a legionary camp. A legionary receives two documents:

  • Access Token (daily pass) - short-lived, allows entry to the camp. Expires after a few hours
  • Refresh Token (centurion's seal) - long-lived, allows obtaining a new daily pass without appearing before the tribunal again

Why do we need two tokens?

  • Security - if someone intercepts the access token, they have access only for a short time
  • Convenience - the user doesn't have to log in every hour
  • Control - we can invalidate the refresh token, blocking access renewal

The refresh token mechanism is the foundation of secure authentication systems in production applications.

Generating a token pair

1// auth/auth.service.ts
2import { Injectable, UnauthorizedException } from '@nestjs/common';
3import { JwtService } from '@nestjs/jwt';
4import { ConfigService } from '@nestjs/config';
5import * as bcrypt from 'bcrypt';
6
7@Injectable()
8export class AuthService {
9 constructor(
10 private jwtService: JwtService,
11 private configService: ConfigService,
12 private usersService: UsersService,
13 ) {}
14
15 async login(loginDto: LoginDto): Promise<TokenPair> {
16 const user = await this.usersService.validateCredentials(
17 loginDto.username,
18 loginDto.password,
19 );
20
21 if (!user) {
22 throw new UnauthorizedException('Invalid login credentials, legionary!');
23 }
24
25 // Generate a token pair
26 const tokens = await this.generateTokenPair(user);
27
28 // Save the hashed refresh token in the database
29 const hashedRefreshToken = await bcrypt.hash(tokens.refreshToken, 10);
30 await this.usersService.updateRefreshToken(user.id, hashedRefreshToken);
31
32 return tokens;
33 }
34
35 async generateTokenPair(user: User): Promise<TokenPair> {
36 const payload = {
37 sub: user.id,
38 username: user.username,
39 role: user.role,
40 cohort: user.cohortName,
41 };
42
43 const [accessToken, refreshToken] = await Promise.all([
44 this.jwtService.signAsync(payload, {
45 secret: this.configService.get('JWT_ACCESS_SECRET'),
46 expiresIn: '15m', // Daily pass - short lifetime
47 }),
48 this.jwtService.signAsync(payload, {
49 secret: this.configService.get('JWT_REFRESH_SECRET'),
50 expiresIn: '7d', // Centurion's seal - longer lifetime
51 }),
52 ]);
53
54 return { accessToken, refreshToken };
55 }
56}

Token renewal endpoint

1// auth/auth.controller.ts
2@Controller('auth')
3export class AuthController {
4 constructor(private authService: AuthService) {}
5
6 @Post('login')
7 async login(@Body() loginDto: LoginDto) {
8 return this.authService.login(loginDto);
9 }
10
11 @Post('refresh')
12 async refreshTokens(@Body('refreshToken') refreshToken: string) {
13 return this.authService.refreshTokens(refreshToken);
14 }
15
16 @Post('logout')
17 @UseGuards(JwtAuthGuard)
18 async logout(@Req() req, @Body('refreshToken') refreshToken: string) {
19 // Invalidate refresh token on logout
20 await this.authService.logout(req.user.id);
21 return { message: 'Legionary logged out successfully' };
22 }
23}

Renewal logic in the service

1// auth/auth.service.ts (continued)
2async refreshTokens(refreshToken: string): Promise<TokenPair> {
3 // 1. Verify the refresh token
4 let payload;
5 try {
6 payload = await this.jwtService.verifyAsync(refreshToken, {
7 secret: this.configService.get('JWT_REFRESH_SECRET'),
8 });
9 } catch (error) {
10 throw new UnauthorizedException('Centurion\'s seal has expired - log in again!');
11 }
12
13 // 2. Find the user and check the stored refresh token
14 const user = await this.usersService.findById(payload.sub);
15
16 if (!user || !user.hashedRefreshToken) {
17 throw new UnauthorizedException('Access denied!');
18 }
19
20 // 3. Compare the tokens
21 const isTokenValid = await bcrypt.compare(refreshToken, user.hashedRefreshToken);
22
23 if (!isTokenValid) {
24 // Potential token theft - invalidate all sessions
25 await this.usersService.updateRefreshToken(user.id, null);
26 throw new UnauthorizedException('Token invalidated - suspicious activity detected!');
27 }
28
29 // 4. Generate a new token pair (Token Rotation)
30 const newTokens = await this.generateTokenPair(user);
31
32 // 5. Save the new refresh token
33 const hashedNewRefreshToken = await bcrypt.hash(newTokens.refreshToken, 10);
34 await this.usersService.updateRefreshToken(user.id, hashedNewRefreshToken);
35
36 return newTokens;
37}
38
39async logout(userId: number): Promise<void> {
40 // Invalidate the refresh token
41 await this.usersService.updateRefreshToken(userId, null);
42}

Token Rotation

A key security technique: with each refresh, we generate a new refresh token and invalidate the old one. If someone steals the refresh token and tries to use it after rotation - we detect it:

1// Token theft protection scheme
2// 1. Legionary logs in -> receives AT1 + RT1
3// 2. AT1 expires -> sends RT1 -> receives AT2 + RT2 (RT1 invalidated)
4// 3. Thief tries to use RT1 -> DENIED (token already rotated)
5// 4. System detects use of old token -> invalidates ALL user sessions

User schema with refresh token

1// users/user.schema.ts (Mongoose)
2import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
3import { Document } from 'mongoose';
4
5@Schema({ timestamps: true })
6export class User extends Document {
7 @Prop({ required: true, unique: true })
8 username: string;
9
10 @Prop({ required: true })
11 password: string;
12
13 @Prop()
14 email: string;
15
16 @Prop({ default: 'miles' })
17 role: string;
18
19 @Prop({ nullable: true })
20 hashedRefreshToken: string;
21
22 @Prop()
23 cohortName: string;
24}
25
26export const UserSchema = SchemaFactory.createForClass(User);

Refresh Token Guard

1// auth/guards/refresh-token.guard.ts
2import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
3import { AuthGuard } from '@nestjs/passport';
4
5@Injectable()
6export class RefreshTokenGuard extends AuthGuard('jwt-refresh') {}
7
8// auth/strategies/refresh-token.strategy.ts
9import { Injectable } from '@nestjs/common';
10import { PassportStrategy } from '@nestjs/passport';
11import { ExtractJwt, Strategy } from 'passport-jwt';
12import { ConfigService } from '@nestjs/config';
13import { Request } from 'express';
14
15@Injectable()
16export class RefreshTokenStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
17 constructor(private configService: ConfigService) {
18 super({
19 jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
20 secretOrKey: configService.get('JWT_REFRESH_SECRET'),
21 passReqToCallback: true,
22 });
23 }
24
25 validate(req: Request, payload: any) {
26 const refreshToken = req.body.refreshToken;
27 return { ...payload, refreshToken };
28 }
29}

Practical exercise

Implement a complete refresh token system for the legion:

1// Your code here
2// 1. Configure two JWT secrets (access and refresh) in ConfigService
3// 2. Implement the POST /auth/refresh endpoint with validation
4// 3. Add Token Rotation on each refresh
5// 4. Implement detection of reused old tokens
6// 5. Add cleanup of expired tokens (cron job)

Summary

You have been appointed gate guardian of the Empire! Now you can:

Generate token pairs - access token (short) + refresh token (long) ✓ Implement Token Rotation - new refresh token on each refresh ✓ Detect token theft - invalidating sessions when an old token is reused ✓ Securely store refresh tokens in the database (hashed with bcrypt) ✓ Implement logout - invalidating refresh token on logout ✓ Create Passport Strategies for refresh tokens

Now your legion has a secure pass renewal system!

Go to CodeWorlds