Implementing Refresh Tokens with Sessions in NestJS: A Comprehensive Guide
In modern web applications, managing user sessions and implementing secure authentication mechanisms are paramount. One of the robust patterns used to handle authentication is via refresh tokens along with access tokens. In this article, we’ll explore how to implement refresh token functionality in a NestJS application using TypeScript.
Why Use Refresh Tokens?
Before diving into the implementation, it’s important to understand why refresh tokens are essential:
- Enhanced Security: Access tokens usually have short lifetimes. Users need to obtain new ones frequently, reducing the window of opportunity for attackers.
- Scalability: This pattern allows developers to scale out stateless backends more effectively by not repeatedly verifying long-term credentials (like passwords).
- Convenience: It improves user experience since users don’t need to re-enter their login credentials often.
Getting Started
Let’s start by setting up a simple NestJS project.
npm install -g @nestjs/cli
nest new auth-demo
cd auth-demo
After creating your project, install necessary dependencies:
npm install @nestjs/jwt passport-jwt @nestjs/passport passport bcryptjs
npm install --save-dev @types/passport-jwt
Setup some placeholder files like AuthModule, AuthService, JwtStrategy, etc., that we’ll fill as we go.
Step 1: Define Models
Create user.model.ts
file inside /src/users
.
import { Schema, Document } from 'mongoose';
export interface User extends Document {
email: string;
password: string;
refreshToken?: string; // Optional field for storing refresh token
}export const UserSchema = new Schema({
email: { type: String, required: true },
password: { type: String, required: true },
refreshToken: { type: String }
});
Step 2: Setup Authentication Logic
Firstly, let’s define our JWT strategy. Create a new file called jwt.strategy.ts
.
import { InjectModel } from '@nestjs/mongoose';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { Model } from 'mongoose';
import { User } from '../users/user.model';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectModel('User') private readonly userModel: Model<User>
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'yourSecretKey',
});
} async validate(payload: any): Promise<User> {
const { sub: userId } = payload;
const user = await this.userModel.findById(userId);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Step 3: Auth Service and Controller
Create an auth.service.ts
file.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import * as bcrypt from 'bcrypt';
import { User } from '../users/user.model';
@Injectable()
export class AuthService {
constructor(
@InjectModel('User') private readonly userModel: Model<User>,
private readonly jwtService: JwtService
) {} async validateUser(email: string, pass: string): Promise<any> {
const user = await this.userModel.findOne({ email }).exec();
if (user && await bcrypt.compare(pass, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
} async login(user: any) {
const payload = { email: user.email, sub: user._id };
return {
access_token: this.jwtService.sign(payload),
refresh_token: await this.createRefreshToken(user)
};
} async createRefreshToken(user: User): Promise<string> {
const refreshToken = this.jwtService.sign({}, { expiresIn: '7d' });
user.refreshToken = refreshToken;
await user.save();
return refreshToken;
} async refreshAccessToken(refreshToken: string) {
try {
const decoded = this.jwtService.verify(refreshToken);
const user = await this.userModel.findOne({ refreshToken }); if (!user) {
throw new UnauthorizedException('Invalid refresh token');
} const payload = { email: user.email, sub: user._id };
return {
access_token: this.jwtService.sign(payload)
};
} catch (e) {
throw new UnauthorizedException('Invalid or expired refresh token');
}
}
}
Finally, in auth.controller.ts
, wire up your routes.
import { Controller, Post, Request, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {} @Post('login')
async login(@Body() req) {
return this.authService.login(req);
} @Post('refresh-token')
async refreshToken(@Body('refreshToken') refreshToken: string) {
return this.authService.refreshAccessToken(refreshToken);
}
}
Final Thoughts
This guide gives you a solid foundation to implement JWT-based authentication and refresh token handling in your NestJS application. Managing authentication properly ensures security and enhances the user’s overall experience. As always, consider further refining your authentication logic based on specific requirements and best practices.
Remember to keep your secrets safe, rotate them periodically, and update your strategies as new security threats emerge. Secure coding is a continuous process, so stay updated!
Happy coding! 💻✨