Guide to Using NestJS JWT

Introduction

JSON Web Token (JWT) is an open standard (RFC 7519) used for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

  • Advantages: No need to store sessions on the server (Stateless), easy system scalability, good support for multi-platform applications and microservices.
  • Limitations: Difficult to revoke tokens before expiration, token size larger than session ID, and if the Secret Key is exposed, the entire system will be compromised.

Detail

After creating the NestJS project, create the auth.dto.ts file to define the payload information when logging in with the following content:

import {IsNotEmpty, IsString} from 'class-validator'

export class LoginDto {
@IsString()
@IsNotEmpty()
username: string

@IsString()
@IsNotEmpty()
password: string
}


Create the environment.service.ts file:

import {Injectable} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'

@Injectable()
export class EnvironmentService {
jwtSecret: string
jwtRefreshSecret: string
jwtExpires: number
jwtExpiresRefresh: number

constructor(private readonly configService: ConfigService) {
this.jwtSecret = this.configService.get<string>('JWT_SECRET') || ''
this.jwtRefreshSecret = this.configService.get<string>('JWT_REFRESH_SECRET') || ''
this.jwtExpires = this.configService.get<number>('JWT_EXPIRES') || 0
this.jwtExpiresRefresh =
this.configService.get<number>('JWT_EXPIRES_REFRESH') || 0
}
}

This code snippet is used to read and centrally manage JWT-related environment variables (env), such as secret keys and token expiration times.


Create the jwt.strategy.ts file:

import {Injectable} from '@nestjs/common'
import {PassportStrategy} from '@nestjs/passport'
import {ExtractJwt, Strategy} from 'passport-jwt'
import {EnvironmentService} from 'src/service/environment.service'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private readonly envService: EnvironmentService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: envService.jwtSecret,
})
}

async validate(payload: any) {
return payload
}
}

This class defines the method for validating the Access Token from the request Header, checking the validity of the token sent from the user side against information from the server, and decoding user information from that token.


Create the jwt.guard.ts file to define a guard based on the 'jwt' strategy created above:

import {Injectable} from '@nestjs/common'
import {AuthGuard} from '@nestjs/passport'

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}


Create the jwt-refresh.strategy.ts file:

import {Injectable} from '@nestjs/common'
import {PassportStrategy} from '@nestjs/passport'
import {ExtractJwt, Strategy} from 'passport-jwt'
import {EnvironmentService} from 'src/service/environment.service'

@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(
Strategy,
'jwt-refresh'
) {
constructor(private readonly envService: EnvironmentService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: envService.jwtRefreshSecret,
})
}

async validate(payload: any) {
return payload
}
}

This strategy is also similar to JwtStrategy, but pay attention to the secretOrKey part, which uses envService.jwtRefreshSecret because this strategy is used for refreshing the token.


Create the jwt-refresh.guard.ts file to use the JwtRefreshStrategy:

import {Injectable} from '@nestjs/common'
import {AuthGuard} from '@nestjs/passport'

@Injectable()
export class JwtRefreshAuthGuard extends AuthGuard('jwt-refresh') {}


Create the auth.service.ts file:

import {
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import {JwtService} from '@nestjs/jwt'
import {LoginDto} from 'src/dto/auth.dto'
import {EnvironmentService} from './environment.service'

const USERS = [
{id: 1, username: 'admin', password: 'password-admin', fullName: 'Admin'},
{id: 2, username: 'user1', password: 'password-user1', fullName: 'User 1'},
]

@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly envService: EnvironmentService
) {}

async login(info: LoginDto) {
const user = USERS.find(
u => u.username === info.username && u.password === info.password
)
if (!user) throw new UnauthorizedException('Invalid account')
return this.getTokens(user.username)
}

async refreshTokens(username: string) {
const user = USERS.find(u => u.username === username)
if (!user) throw new UnauthorizedException()
return this.getTokens(user.username)
}

getProfile(username: string) {
const user = USERS.find(u => u.username === username)
return user ? {...user, password: undefined} : null
}

async getTokens(username: string) {
const payload = {username}
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.envService.jwtSecret,
expiresIn: this.envService.jwtExpires,
}),
this.jwtService.signAsync(payload, {
secret: this.envService.jwtRefreshSecret,
expiresIn: this.envService.jwtExpiresRefresh,
}),
])
return {
accessToken,
refreshToken,
expiresIn: Date.now() + this.envService.jwtExpires * 1000,
}
}
}

  • This is where the main business logic is processed, including checking the account, creating a new pair of Access/Refresh Tokens, and retrieving user information.
  • Here, I have hard-coded the user information for demo purposes; in reality, this information would be retrieved from a database.


Create the auth.controller.ts file:

import {
Body,
Controller,
Get,
Post,
Req,
UnauthorizedException,
UseGuards,
} from '@nestjs/common'
import {LoginDto} from 'src/dto/auth.dto'
import {JwtRefreshAuthGuard} from 'src/guard/jwt-refresh.guard'
import {JwtAuthGuard} from 'src/guard/jwt.guard'
import {AuthService} from 'src/service/auth.service'

@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}

@Post('login')
async login(@Body() body: LoginDto) {
return this.authService.login(body)
}

@Post('refresh')
@UseGuards(JwtRefreshAuthGuard)
async refresh(@Req() req: any) {
return this.authService.refreshTokens(req?.user?.username)
}

@Get('profile')
@UseGuards(JwtAuthGuard)
async getProfile(@Req() req: any) {
const profile = this.authService.getProfile(req?.user?.username)
if (!profile) throw new UnauthorizedException()
return profile
}
}

  • This class defines the HTTP endpoints (login, refresh, profile) and applies Guards to protect routes that require login.
  • Note that /refresh uses JwtRefreshAuthGuard, which requires sending up the refresh token, while /profile uses JwtAuthGuard, which requires sending up the access token.


Update the app.module.ts file:

import {Module} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {EnvironmentService} from './service/environment.service'
import {AuthService} from './service/auth.service'
import {AuthController} from './controller/auth.controller'
import {JwtModule} from '@nestjs/jwt'
import {PassportModule} from '@nestjs/passport'
import {JwtStrategy} from './strategy/jwt.strategy'
import {JwtRefreshStrategy} from './strategy/jwt-refresh.strategy'

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
PassportModule,
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET,
signOptions: {expiresIn: parseInt(process.env.JWT_EXPIRES || '0')},
}),
],
controllers: [AuthController],
providers: [
EnvironmentService,
AuthService,
JwtStrategy,
JwtRefreshStrategy,
],
})
export class AppModule {}

This is the application's general configuration file, where modules are connected, JwtModule is registered globally, and necessary controllers and providers are declared.


Finally, edit the .env file with values according to your needs:

JWT_SECRET = JWT_SECRET
JWT_REFRESH_SECRET = JWT_REFRESH_SECRET
JWT_EXPIRES = 300000
JWT_EXPIRES_REFRESH = 864000000


Testing the API with Postman yields the following results:




Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Deploying a NodeJS Server on Google Kubernetes Engine

Kubernetes Deployment for Zero Downtime

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Kubernetes Practice Series

NodeJS Practice Series

Sitemap