NestJS is a framework used to build the server-side of an application. Here I’m going to explain how to build the base of the application with basic security features.
Start New Project
First, you need to install NestJS CLI with the following command. You might require root permission for this command.
npm i -g @nestjs/cli
Then use the following command to create a new project.
nest new project_name cd project_name
Hot Reload
The next step would be adding auto building (Hot Reload) to the project so you don’t need to build the project in every code modification. For that, you need to install the following dev dependencies.
npm i --save-dev webpack-node-externals run-script-webpack-plugin webpack
Then create a new file named webpack-hmr.config.js
in your root directory. Insert the following code in it.
const nodeExternals = require('webpack-node-externals'); const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin'); module.exports = function (options, webpack) { return { ...options, entry: ['webpack/hot/poll?100', options.entry], externals: [ nodeExternals({ allowlist: ['webpack/hot/poll?100'], }), ], plugins: [ ...options.plugins, new webpack.HotModuleReplacementPlugin(), new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/], }), new RunScriptWebpackPlugin({ name: options.output.filename }), ], }; };
Then open the main.ts
file in the src
and replace its code with the following.
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; declare const module: any; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); if (module.hot) { module.hot.accept(); module.hot.dispose(() => app.close()); } } bootstrap();
Now open the package.json
in your root directory and replace start:dev
script with the following.
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch"
Now start the application using the following command.
npm run start:dev
Basic Login Authentication
Now I’m going to explain how to add the basic login authentication to the app you have just created. You need the following dependencies and dev dependencies for this.
npm install --save @nestjs/passport passport passport-local npm install --save-dev @types/passport-local
Then you need to generate the Auth and User modules with services with the following commands.
nest g module auth nest g service auth nest g module users nest g service users
Now let’s generate an interface to represent the user with the following command.
nest g interface interfaces/user
And add the following code.
export interface UserData { id: number username: string password: string }
Now open the users/users.service.ts
file and enter the following code. Here I have created two example users and a function to search for a user.
import { Injectable } from '@nestjs/common'; import { UserData } from 'src/interfaces/user.interface'; @Injectable() export class UsersService { private readonly users: Array<UserData> = [ { id: 1, username: "admin", password: "1234" }, { id: 2, username: "user", password: "4567" } ] async find(username: string): Promise<UserData | undefined> { return this.users.find(user => user.username === username) } }
Then export the UsersService
in the users/users.module.ts
file so it will be visible to the outside of the module.
import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; @Module({ providers: [UsersService], exports: [UsersService] }) export class UsersModule {}
Now open the auth/auth.service.ts
file and enter the following code. Here a function is created to validate the user with the support of UsersService
.
import { Injectable } from '@nestjs/common'; import { UsersService } from 'src/users/users.service'; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async validateUser(username: string, password: string): Promise<any> { const user = await this.usersService.find(username) if(user && user.password === password){ const { password, ...result } = user return result } return null } }
Then import the UsersService
to the Auth Module by editing auth/auth.module.ts
file.
import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; @Module({ imports: [UsersModule], providers: [AuthService] }) export class AuthModule {}
Now create a new file named local.strategy.ts
in the auth folder to implement the local authentication strategy. And enter the following code in it.
import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super() } async validate(username: string, password: string): Promise<any> { const user = await this.authService.validateUser(username, password) if(!user){ throw new UnauthorizedException() } return user } }
Then you need to configure auth module to use the local strategy by importing the PassportModule
and providing the LocalStategy
as follows.
import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; @Module({ imports: [ UsersModule, PassportModule ], providers: [AuthService, LocalStrategy] }) export class AuthModule {}
Now you should generate a file named local-auth.guard.ts
in the auth folder to implement the local guard which is going to be initiated in the login request as follows.
nest g gu auth/guards/local-auth
And enter the following code in it.
import { Injectable } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; @Injectable() export class LocalAuthGuard extends AuthGuard('local') {}
Now let’s create a controller in auth with the following command.
nest g co auth
Then create a POST request in it and initiate LocalAuthGuard as follows.
import { Controller, Post, Request, UseGuards } from '@nestjs/common'; import { LocalAuthGuard } from './guards/local-auth.guard'; @Controller('auth') export class AuthController { constructor() {} @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req) { return req.user } }
Now the basic login authentication code is finished. Create a POST request to http://localhost:3000/auth/login
with JSON body including username and password.
curl -X POST http://localhost:3000/auth/login -d '{"username": "admin", "password": "1234"}' -H "Content-Type: application/json"
You should get user information if the login is successful and if not, it should give you an error message.
Integrating JWT Authentication
Before integrating JWT to the above project, you need to install the following dependencies and dev dependencies.
npm install --save @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt
Then open the auth/auth.service.ts
file and create a method to sign the user with the required details as follows.
import { Injectable } from '@nestjs/common'; import { UsersService } from 'src/users/users.service'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor(private usersService: UsersService, private jwtService: JwtService) {} async validateUser(username: string, password: string): Promise<any> { const user = await this.usersService.find(username) if(user && user.password === password){ const { password, ...result } = user return result } return null } async sign(user: any) { const payload = { username: user.username, sub: user.id } return { access_token: this.jwtService.sign(payload) } } }
Now you need to create a secret key and expire time to be used in the JWT method. Create a new configuration file in src/config
folder named auth.config.ts
and enter the key as follows.
export const jwtConfig = { secret: "secretKey", expireTime: "1h", expireIgnore: false }
Now update the auth.module.ts file as follows.
import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; import { AuthController } from './auth.controller'; import { JwtModule } from '@nestjs/jwt'; import { jwtConfig } from 'src/config/auth.config'; @Module({ imports: [ UsersModule, PassportModule, JwtModule.register({ secret: jwtConfig.secret, signOptions: { expiresIn: jwtConfig.expireTime } }) ], providers: [AuthService, LocalStrategy], controllers: [AuthController], exports: [AuthService] }) export class AuthModule {}
Now update the auth.controller.ts file as follows.
import { Controller, Post, Request, UseGuards } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LocalAuthGuard } from './guards/local-auth.guard'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @UseGuards(LocalAuthGuard) @Post('login') async login(@Request() req) { return this.authService.sign(req.user) } }
Now send the previous POST request again and you should get the JWT token as the response.
curl -X POST http://localhost:3000/auth/login -d '{"username": "admin", "password": "1234"}' -H "Content-Type: application/json"
Next, I’m going to initiate JWT token guard for each request.
Now you have to create a file named jwt.strategy.ts
in the auth
folder and implement the JWT validation strategy in it as follows.
import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Strategy, ExtractJwt } from "passport-jwt"; import { jwtConfig } from "src/config/auth.config"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: jwtConfig.expireIgnore, secretOrKey: jwtConfig.secret }) } async validate(payload: any) { return { id: payload.sub, username: payload.username} } }
Then add JwtStrategy
to the providers
list of auth.module.ts
file.
import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; import { AuthController } from './auth.controller'; import { JwtModule } from '@nestjs/jwt'; import { JwtStrategy } from './jwt.strategy'; import { jwtConfig } from 'src/config/auth.config'; @Module({ imports: [ UsersModule, PassportModule, JwtModule.register({ secret: jwtConfig.secret, signOptions: { expiresIn: jwtConfig.expireTime } }) ], providers: [AuthService, LocalStrategy, JwtStrategy], controllers: [AuthController], exports: [AuthService] }) export class AuthModule {}
Now generate jwt-auth.guard.ts
file in the auth
folder as follows.
nest g gu auth/guards/jwt-auth
And implement the JWT guard which is going to be initiate in each request as follows.
import { Injectable } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}
Now let’s create a controller for users and implement protected routes in it using JWT guard. First, create the controller as follows.
nest g co users
Then update the users.service.ts
file as follows.
import { Injectable } from '@nestjs/common'; import { UserData } from 'src/interfaces/user.interface'; @Injectable() export class UsersService { private readonly users: Array<UserData> = [ { id: 1, username: "admin", password: "1234" }, { id: 2, username: "user", password: "1234" } ] async find(username: string): Promise<UserData | undefined> { return this.users.find(user => user.username === username) } async findByID(id: number): Promise<UserData | undefined> { return this.users.find(user => user.id == id) } async findAll(): Promise<Array<UserData>> { return this.users; } }
Then create some protected API calls in the users.controller.ts
file as follows.
import { Controller, Get, NotFoundException, Param, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private userService: UsersService) {} @UseGuards(JwtAuthGuard) @Get() async getAllUsers() { const users = await this.userService.findAll() let results = [] users.forEach(user => { const { password, ...result } = user results.push(result) }); return results } @UseGuards(JwtAuthGuard) @Get(':id') async getUser(@Param() params) { const user = await this.userService.findByID(Number(params.id)) if(user){ const { password, ...result } = user return result }else{ throw new NotFoundException() } } }
Now you need to use the token that you got from the login request to send a request as follows.
curl http://localhost:3000/users/1 -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR..."
Integrating Authorization
Here I’m going to explain how to add Permission-based authorization to the project.
First of all, let’s create some permissions. Create a file named permission.enum.ts
in src/enums
folder and put some permissions as follows.
export enum Permission { GET_USER = 'get_user', GET_ALL_USERS = 'get_all_users' }
Now you need to introduce permissions to the user.interface.ts file as follows.
import { Permission } from "src/enums/permission.enum"; export interface UserData { id: number username: string password: string permissions: Permission[] }
Then let’s add some permissions for the two users that we have created before.
import { Injectable } from '@nestjs/common'; import { Permission } from 'src/enums/permission.enum'; import { UserData } from 'src/interfaces/user.interface'; @Injectable() export class UsersService { private readonly users: Array<UserData> = [ { id: 1, username: "admin", password: "1234", permissions: [ Permission.GET_USER, Permission.GET_ALL_USERS ] }, { id: 2, username: "user", password: "1234", permissions: [ Permission.GET_USER ] } ] async find(username: string): Promise<UserData | undefined> { return this.users.find(user => user.username === username) } async findByID(id: number): Promise<UserData | undefined> { return this.users.find(user => user.id == id) } async findAll(): Promise<Array<UserData>> { return this.users; } }
Then you should create a decorator as follows to be used to specify the permissions required to access the resource.
nest g d decorators/permissions
And implement the code as follows.
import { SetMetadata } from '@nestjs/common'; import { Permission } from 'src/enums/permission.enum'; export const PERMISSIONS_KEY = 'permissions' export const Permissions = (...permissions: Permission[]) => SetMetadata(PERMISSIONS_KEY, permissions)
Now let’s generate a guard for permissions as follows.
nest g gu auth/guards/permissions
In the permissions.guard.ts file, implement the permission comparison method for the user as follows.
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { PERMISSIONS_KEY } from 'src/decorators/permissions.decorator'; import { Permission } from 'src/enums/permission.enum'; @Injectable() export class PermissionsGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(PERMISSIONS_KEY, [ context.getHandler(), context.getClass() ]) if(!requiredPermissions) { return true } const { user } = context.switchToHttp().getRequest() return requiredPermissions.some((permission) => user.permissions?.includes(permission)) } }
Now open the jwt.strategy.ts
file and add permissions in the return
of the validate
function, so the PermissionGuard
will get permissions to compare.
import { Injectable } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Strategy, ExtractJwt } from "passport-jwt"; import { jwtConfig } from "src/config/auth.config"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: jwtConfig.expireIgnore, secretOrKey: jwtConfig.secret }) } async validate(payload: any) { return { id: payload.sub, username: payload.username, permissions: payload.permissions} } }
And then open the auth.service.ts
file and enter permissions
to the payload
so the signed payload will have permissions.
import { Injectable } from '@nestjs/common'; import { UsersService } from 'src/users/users.service'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor(private usersService: UsersService, private jwtService: JwtService) {} async validateUser(username: string, password: string): Promise<any> { const user = await this.usersService.find(username) if(user && user.password === password){ const { password, ...result } = user return result } return null } async sign(user: any) { const payload = { username: user.username, sub: user.id, permissions: user.permissions } return { access_token: this.jwtService.sign(payload) } } }
Now open the users.controllers.ts
file and add @Permission(Permission.PERMISSION)
decorator to the requests as required.
Since both JwtAuthGuard
and PermissionsGuard
are used in every request here, move them to the top of the class.
import { Controller, Get, NotFoundException, Param, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; import { PermissionsGuard } from 'src/auth/guards/permissions.guard'; import { Permissions } from 'src/decorators/permissions.decorator'; import { Permission } from 'src/enums/permission.enum'; import { UsersService } from './users.service'; @Controller('users') @UseGuards(JwtAuthGuard, PermissionsGuard) export class UsersController { constructor(private userService: UsersService) {} @Get() @Permissions(Permission.GET_ALL_USERS) async getAllUsers() { const users = await this.userService.findAll() let results = [] users.forEach(user => { const { password, ...result } = user results.push(result) }); return results } @Get(':id') @Permissions(Permission.GET_USER) async getUser(@Param() params) { const user = await this.userService.findByID(Number(params.id)) if(user){ const { password, ...result } = user return result }else{ throw new NotFoundException() } } }
You can provide these app guards in the module as follows as well.
providers: [ { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: PermissionsGuard } ],
But here I didn’t do that because the UsersModule is imported to the AuthModule as well. So if I provide them in the module, it will be applied to the Auth Module and the login request will also be checked for the JWT access.
Integrating Helmet
The Helmet package helps to secure the app by setting various HTTP headers. First, let’s install the package.
npm i --save helmet
Now let’s create a global middleware using the following command.
nest g mi app
Now place the following code in the app.middleware.ts
file.
import { INestApplication } from "@nestjs/common"; import * as helmet from 'helmet'; export function middleware(app: INestApplication): INestApplication { const isProduction = (process.env.NODE_ENV === 'production') app.use(helmet({ contentSecurityPolicy: isProduction })) return app; }
And open the main.ts file and call the middleware function.
import { NestFactory } from '@nestjs/core'; import { middleware } from './app.middleware'; import { AppModule } from './app.module'; declare const module: any; async function bootstrap() { const app = await NestFactory.create(AppModule); middleware(app) await app.listen(3000); if (module.hot) { module.hot.accept(); module.hot.dispose(() => app.close()); } } bootstrap();
Now to set the environment, open the package.json
and modify start:dev
and start:prod
scripts as follows.
"start:dev": "NODE_ENV=development nest build --webpack --webpackPath webpack-hmr.config.js --watch", "start:prod": "NODE_ENV=production node dist/main"
CSRF Protection
Since we’re not using cookies, CSRF is not really required.
Preventing Brute-force Attacks
To prevent brute-force attacks, let’s install the throttler package.
npm i --save @nestjs/throttler
Then include the TTL and requests limit times in the auth.config.ts
file as follows.
export const jwtConfig = { secret: "secretKey", expireTime: "1h", expireIgnore: false } export const bruteForceLimits = { requestLimit: 10, ttl: 60 }
Then modify the app.module.ts
as follows.
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { bruteForceLimits } from './config/auth.config'; import { APP_GUARD } from '@nestjs/core'; @Module({ imports: [ AuthModule, UsersModule, ThrottlerModule.forRoot({ ttl: bruteForceLimits.ttl, limit: bruteForceLimits.requestLimit }) ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: ThrottlerGuard } ], }) export class AppModule {}
CORS Enabling
Insert CORS config into the auth.config.ts
as bellow.
export const jwtConfig = { secret: "l0XNT38sJ/OkVVpKTPNixGyH2SW5yF1NJqB9BAGczf1JKHIjenyqaQPOX9Tt9LBC3WHCoG08yhZe+MkQtATYgr5dLhFoTifsczfXNNMBTrXmJN5DV+WzesYS/daFMDC0vXmG/20ImrFsw22EKDWl4+VDAE1slVx/B41t5gNGt4ffd0UPE0wpekd23FOECD0EoTCLYsM7nSnMhUlKB4ONvAlOgObXLAgCgMkDe1g69kspxT1ev7/MyXv+xDRUikJgvPOuy7lZVMQ5eOC4ouELNT5L18yc9hbYEfQXDsUe6zCN6DNbASCYt2Eg/ki2nwpJb+NUT69ObWzxG9ZGJpgrqA==", expireTime: "1h", expireIgnore: false } export const bruteForceLimits = { requestLimit: 10, ttl: 60 } export const corsConfig = { origin: "*", methods: 'GET, PUT, POST, DELETE', allowedHeaders: 'Content-Type, Authorization' }
Then modify the app.middleware.ts
as below.
import { INestApplication } from "@nestjs/common"; import * as helmet from 'helmet'; import { corsConfig } from "./config/auth.config"; export function middleware(app: INestApplication): INestApplication { const isProduction = (process.env.NODE_ENV === 'production') app.use(helmet({ contentSecurityPolicy: isProduction })) app.enableCors({ origin: corsConfig.origin, methods: corsConfig.methods, allowedHeaders: corsConfig.allowedHeaders }); return app; }
Environmental Files
First, you should install the following packages before creating environmental files.
npm i --save @nestjs/config
Then create developmnt.env
and production.env
files in your root folder.
And let’s move the origin configuration to the development.env
file.
ORIGIN=http://localhost:3000
Now open the auth.config.ts
file and modify it as follows.
export const jwtConfig = { secret: "secretKey", expireTime: "1h", expireIgnore: false } export const bruteForceLimits = { requestLimit: 10, ttl: 60 } export const corsConfig = { methods: 'GET, PUT, POST, DELETE', allowedHeaders: 'Content-Type, Authorization' }
Now open the app.module.ts
file and modify it as follows to import the env variables.
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; import { bruteForceLimits } from './config/auth.config'; import { APP_GUARD } from '@nestjs/core'; import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: `${process.env.NODE_ENV || 'development'}.env`, isGlobal: true }), AuthModule, UsersModule, ThrottlerModule.forRoot({ ttl: bruteForceLimits.ttl, limit: bruteForceLimits.requestLimit }) ], controllers: [AppController], providers: [ AppService, { provide: APP_GUARD, useClass: ThrottlerGuard } ], }) export class AppModule {}
Finally, open the app.middleware.ts
file and insert the origin from the env.
import { INestApplication } from "@nestjs/common"; import * as helmet from 'helmet'; import { corsConfig } from "./config/auth.config"; export function middleware(app: INestApplication): INestApplication { const isProduction = (process.env.NODE_ENV === 'production') app.use(helmet({ contentSecurityPolicy: isProduction })) app.enableCors({ origin: process.env.ORIGIN, methods: corsConfig.methods, allowedHeaders: corsConfig.allowedHeaders }); return app; }
Remember to add everything to the production.env
as well before running the production build.
Compression
It’s important to add compression so the response body will be smaller and the app will be speeder. For adding compression, you need to install following package.
npm i --save compression
And then modify the app.middleware.ts
file as follows.
import { INestApplication } from "@nestjs/common"; import * as helmet from 'helmet'; import { corsConfig } from "./config/auth.config"; import * as compression from 'compression'; export function middleware(app: INestApplication): INestApplication { const isProduction = (process.env.NODE_ENV === 'production') app.use(helmet({ contentSecurityPolicy: isProduction })) app.use(compression()) app.enableCors({ origin: process.env.ORIGIN || '*', methods: corsConfig.methods, allowedHeaders: corsConfig.allowedHeaders }) return app; }
So that’s it
If you have any suggestions or questions about this guide, comment here or send me an email.
Nice article Rashintha. learned a lot of things. thanks for posting 👏👏
Thank you Harsha! Your comment is really valuable to me. Keep in touch with the blog.