NestJS JWT Authentication – End-to-End Production Guide

Search for a command to run...

No comments yet. Be the first to comment.
Let’s build a Secure Personal AI API with Ollama, Nginx, and Cloudflare Tunnel using oracle ubuntu instance Prerequisites and Target Audience ⚠️ Important Notice: This is not a beginner-friendly tutorial. This guide assumes you have practical, hands-...

Introduction Running applications on Oracle Cloud Infrastructure (OCI) requires more than just starting your server—you need a robust setup that ensures your services stay running, restart automatically after crashes, and survive system reboots. This...

Ever found yourself in one of these situations? "Can you check my website?" but your app is running on localhost:3000 Need to test webhooks from external services but they can't reach your local machine Want to show a client your work-in-progress ...

Introduction to Amazon EC2 Compute refers to the processing power needed to run applications, manage data, and perform calculations. In the cloud, this power is available on-demand. You can access it remotely without owning or maintaining physical ha...
This article walks you through every single file required to get a secure, refresh-token-enabled JWT authentication system in NestJS with Prisma, bcrypt, Passport, and class-validator.
Copy / paste the snippets in order and you will have a working project.
npm i @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
npm i -D @types/passport-jwt @types/bcrypt
.env)DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
JWT_SECRET="super-secret-access"
JWT_REFRESH_SECRET="super-secret-refresh"
JWT_EXPIRES="15m"
JWT_REFRESH_EXPIRES="7d"
This configuration file likely contains environment variables used by a web application.
DATABASE_URL: This variable stores the connection information for the PostgreSQL database, including the username, password, host, port, and database name.
JWT_SECRET: This variable stores a secret key used for generating JSON Web Tokens (JWT) for authentication and authorization purposes. It should be kept private and secure to prevent unauthorized access.
JWT_REFRESH_SECRET: This variable stores a separate secret key used specifically for generating refresh tokens in a JWT-based authentication system. Refresh tokens are used to obtain new access tokens after they expire without requiring re-authentication.
JWT_EXPIRES: This variable specifies the expiration time for access tokens created by the application. In this case, it is set to 15 minutes, meaning that access tokens will be valid for 15 minutes before they expire.
JWT_REFRESH_EXPIRES: This variable specifies the expiration time for refresh tokens generated by the application. In this case, it is set to 7 days, allowing users to obtain new access tokens for a week without having to re-authenticate.
By using these environment variables, the application can securely store sensitive information such as database credentials and authentication secrets, without having to hardcode them in the codebase.
prisma/schema.prisma)generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum RoleEnum {
IT_HEAD
SYSTEM_OWNER
FINANCE_OFFICER
REGISTRATION_CLERK
AUDITOR
VIEWER
}
model FncciUser {
id Int @id @default(autoincrement())
email String @unique
password String
role RoleEnum @default(VIEWER)
firstName String
lastName String
phoneNumber String
refreshToken String? // stores hashed refresh token
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
After editing:
npx prisma migrate dev --name init
npx prisma generate
src/auth/dto/create-auth.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsEnum, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { RoleEnum } from '@prisma/client';
export class CreateAuthDto {
@ApiProperty({ example: 'john@doe.com' })
@IsEmail() @IsNotEmpty()
email: string;
@ApiProperty({ minLength: 8, maxLength: 20 })
@IsString() @MinLength(8) @MaxLength(20) @IsNotEmpty()
password: string;
@ApiProperty({ enum: RoleEnum, example: RoleEnum.VIEWER })
@IsEnum(RoleEnum) @IsNotEmpty()
role: RoleEnum;
@ApiProperty({ example: 'John' })
@IsString() @IsNotEmpty()
firstName: string;
@ApiProperty({ example: 'Doe' })
@IsString() @IsNotEmpty()
lastName: string;
@ApiProperty({ example: '+977-9801234567' })
@IsString() @IsNotEmpty()
phoneNumber: string;
}
src/auth/dto/login-auth.dto.ts
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class LoginAuthDto {
@IsEmail() @IsNotEmpty()
email: string;
@IsString() @IsNotEmpty()
password: string;
}
src/decorator/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
src/auth/guard/jwt-auth.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorator/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return isPublic ? true : super.canActivate(context);
}
}
src/auth/strategy/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get<string>('JWT_SECRET'),
});
}
async validate(payload: any) {
return { id: payload.sub, email: payload.email, role: payload.role };
}
}
src/auth/strategy/jwt-refresh.strategy.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
constructor(private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromBodyField('refreshToken'),
secretOrKey: config.get<string>('JWT_REFRESH_SECRET'),
passReqToCallback: true,
});
}
async validate(req: Request, payload: any) {
return { id: payload.sub, email: payload.email, role: payload.role, refreshToken: req.body.refreshToken };
}
}
src/auth/auth.service.ts
import { ConflictException, ForbiddenException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { CreateAuthDto } from './dto/create-auth.dto';
import { LoginAuthDto } from './dto/login-auth.dto';
@Injectable()
export class AuthService {
private readonly salt = 10;
constructor(
private prisma: PrismaService,
private jwt: JwtService,
) {}
async register(dto: CreateAuthDto) {
const exists = await this.prisma.fncciUser.findUnique({ where: { email: dto.email } });
if (exists) throw new ConflictException('Email already taken');
const hashed = await bcrypt.hash(dto.password, this.salt);
const user = await this.prisma.fncciUser.create({
data: { ...dto, password: hashed },
select: { id: true, email: true, role: true, firstName: true, lastName: true, phoneNumber: true, createdAt: true },
});
return user;
}
async login(dto: LoginAuthDto) {
const user = await this.prisma.fncciUser.findUnique({ where: { email: dto.email } });
if (!user) throw new NotFoundException('User not found');
const ok = await bcrypt.compare(dto.password, user.password);
if (!ok) throw new UnauthorizedException('Wrong password');
const payload = { sub: user.id, email: user.email, role: user.role };
const accessToken = this.jwt.sign(payload, { expiresIn: '15m' });
const refreshToken = this.jwt.sign(payload, { secret: process.env.JWT_REFRESH_SECRET, expiresIn: '7d' });
await this.prisma.fncciUser.update({ where: { id: user.id }, data: { refreshToken: await bcrypt.hash(refreshToken, this.salt) } });
return { accessToken, refreshToken };
}
async refreshTokens(refreshToken: string) {
try {
const payload = this.jwt.verify(refreshToken, { secret: process.env.JWT_REFRESH_SECRET });
const user = await this.prisma.fncciUser.findUnique({ where: { id: payload.sub } });
if (!user || !user.refreshToken) throw new ForbiddenException('Access Denied');
const rtMatches = await bcrypt.compare(refreshToken, user.refreshToken);
if (!rtMatches) throw new ForbiddenException('Access Denied');
const newPayload = { sub: user.id, email: user.email, role: user.role };
const newAccess = this.jwt.sign(newPayload, { expiresIn: '15m' });
const newRefresh = this.jwt.sign(newPayload, { secret: process.env.JWT_REFRESH_SECRET, expiresIn: '7d' });
await this.prisma.fncciUser.update({ where: { id: user.id }, data: { refreshToken: await bcrypt.hash(newRefresh, this.salt) } });
return { accessToken: newAccess, refreshToken: newRefresh };
} catch {
throw new ForbiddenException('Invalid refresh token');
}
}
async logout(userId: number) {
await this.prisma.fncciUser.update({ where: { id: userId }, data: { refreshToken: null } });
return { message: 'Logged out' };
}
}
src/auth/auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post, Req, UseGuards, ValidationPipe } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateAuthDto } from './dto/create-auth.dto';
import { LoginAuthDto } from './dto/login-auth.dto';
import { Public } from '../decorator/public.decorator';
import { Request } from 'express';
@Controller('auth')
export class AuthController {
constructor(private auth: AuthService) {}
@Public()
@Post('register')
async register(@Body(ValidationPipe) dto: CreateAuthDto) {
return this.auth.register(dto);
}
@Public()
@HttpCode(HttpStatus.OK)
@Post('login')
async login(@Body() dto: LoginAuthDto) {
return this.auth.login(dto);
}
@Public()
@HttpCode(HttpStatus.OK)
@Post('refresh')
async refresh(@Body('refreshToken') refreshToken: string) {
return this.auth.refreshTokens(refreshToken);
}
@Post('logout')
async logout(@Req() req: Request) {
const user = req.user as any;
return this.auth.logout(user.id);
}
}
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { PrismaModule } from '../prisma/prisma.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';
import { JwtRefreshStrategy } from './strategy/jwt-refresh.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({}),
PrismaModule,
],
providers: [AuthService, JwtStrategy, JwtRefreshStrategy],
controllers: [AuthController],
exports: [AuthService],
})
export class AuthModule {}
main.ts)src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.enableCors({ origin: true, credentials: true });
await app.listen(process.env.PORT || 3000);
}
bootstrap();
src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthModule } from './auth/auth.module';
import { JwtAuthGuard } from './auth/guard/jwt-auth.guard';
@Module({
imports: [AuthModule],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
npm run start:dev
| Endpoint | Public | Body | Response |
| POST /auth/register | ✅ | CreateAuthDto | { id, email, … } |
| POST /auth/login | ✅ | LoginAuthDto | { accessToken, refreshToken } |
| POST /auth/refresh | ✅ | { refreshToken } | { accessToken, refreshToken } |
| POST /auth/logout | ❌ | – | { message } |
All other routes automatically require a valid bearer token.
That’s the complete, production-ready JWT authentication system you can drop into any NestJS project.