Identity and access management (IAM) is an essential component of application security. It helps ensure that the right individuals can access the right technology resources, like emails, databases, data and applications, while keeping unauthorized users out.
NestJS is a popular Node.js framework for building scalable and efficient server-side applications. Implementing IAM in NestJS can greatly improve security while enhancing your user experience. In this guide, I will explore how to implement IAM in a NestJS application from start to finish.
What Is IAM?
IAM is a framework of technologies and policies that helps manage user identities and control access to user resources. It includes authentication, authorization, user provisioning, role-based access control (RBAC) and audit logging. With IAM, you can:
- Ensure secure authentication mechanisms.
- Implement appropriate authorization rules.
- Maintain user roles and permissions.
- Monitor and audit access to resources.
OK, I Get IAM … but What Is NestJS?
NestJS is an extensive Node.js framework that helps you build server-side applications. NestJS leverages TypeScript and uses a modular architecture inspired by Angular, making it a strong choice for scalable applications and providing a solid foundation for implementing IAM.
Implement JWT Authentication in NestJS
Authentication is the process of verifying a user’s identity using authentication strategies including JSON Web Tokens (JWT) and OAuth2. Follow these steps to set up JWT authentication in a NestJS application.
First, install the necessary dependencies:
npm install @nestjs/jwt @nestjs/passport passport-jwt
Next, create a module for authentication. This module will handle user login, token generation and token validation.
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './jwt.strategy';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'yourSecretKey',
signOptions: {
expiresIn: '1h',
},
}),
UsersModule,
],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
})
export class AuthModule {}
Create the AuthService
to handle authentication logic:
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { User } from '../users/user.entity';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<User | null> {
const user = await this.usersService.findByUsername(username);
if (user && user.password === password) {
return user;
}
return null;
}
async login(user: User) {
const payload = { username: user.username, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
Next, define the JwtStrategy
to handle token validation:
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { AuthService } from './auth.service';
import { User } from '../users/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'yourSecretKey',
});
}
async validate(payload: any): Promise<User | null> {
const user = await this.authService.validateUser(payload.username, null);
return user;
}
}
Finally, create the AuthController
for user login:
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);
if (user) {
return this.authService.login(user);
} else {
throw new Error('Invalid credentials');
}
}
@Post('protected')
@UseGuards(AuthGuard('jwt'))
async protectedRoute() {
return { message: 'You have access to this protected route' };
}
}
The LoginDto
defines the expected request body for the login endpoint:
import { IsString } from 'class-validator';
export class LoginDto {
@IsString()
username: string;
@IsString()
password: string;
}
Now you have a basic JWT authentication system in place. Users can log in and receive a JWT token, which they can use to access protected routes.
Implement RBAC Authorization in NestJS
Authorization is the process of determining whether a user has permission to access certain resources. RBAC is a common approach to authorization in NestJS.
To implement RBAC, first, create a RolesGuard
that checks if a user has the appropriate role to access a resource:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true; // No roles defined, allow access
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return roles.includes(user.role);
}
}
Define a custom decorator to specify required roles:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
With these components, you can create a protected route that requires specific roles:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
import { AuthGuard } from '@nestjs/passport';
@Controller('example')
@UseGuards(AuthGuard('jwt'), RolesGuard) // Apply both JWT and RolesGuard
export class ExampleController {
@Get('admin')
@Roles('admin') // Only users with 'admin' role can access this route
getAdminData() {
return { message: 'This is admin data' };
}
@Get('user')
@Roles('user', 'admin') // Users with 'user' or 'admin' role can access this route
getUserData() {
return { message: 'This is user data' };
}
}
Enable User Provisioning and Audit Logging
Beyond authentication and authorization, user provisioning and audit logging are crucial components of IAM.
Set Up User Provisioning
User provisioning involves creating, updating and deleting user accounts. You can implement a user service to manage these operations:
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
async createUser(username: string, password: string, role: string): Promise<User> {
const user = new User();
user.username = username;
user.password = password;
user.role = role;
return this.userRepository.save(user);
}
async findByUsername(username: string): Promise<User | null> {
return this.userRepository.findOne({ where: { username } });
}
}
Implement Audit Logging
Audit logging helps track user activities, providing insights into who accessed what and when.
Middleware in NestJS provides a centralized way to apply logic to incoming requests before they reach controllers, making it ideal for logging, authentication checks, rate limiting, etc. By placing audit logging in a middleware, you can capture and record relevant information consistently for all or specific endpoints without duplicating logic across controllers.
Here’s an example of how you might implement audit logging as middleware in a NestJS application:
Create Middleware for Audit Logging
Define a middleware that logs relevant information for each request, such as the HTTP method, URL, user identity (if authenticated) and timestamp.
import { Injectable, NestMiddleware, Req, Res, Next } from '@nestjs/common';
import { Request, Response } from 'express';
@Injectable()
export class AuditMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
const { method, originalUrl } = req;
const user = req.user; // Assumes you're using authentication with JWT
const userId = user ? user.id : 'anonymous';
console.log(`[Audit Log] User ${userId} made a ${method} request to ${originalUrl} at ${new Date().toISOString()}`);
next(); // Proceed to the next middleware or controller
}
}
Apply Middleware to the Module
To ensure that the middleware runs for specific routes or globally, register it in the corresponding module(s).
Apply Middleware Globally
To apply the middleware globally, add it to the root module’s configure
method:
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuditMiddleware } from './audit.middleware';
@Module({
imports: [], // Import other modules here
controllers: [AppController],
providers: [AppService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuditMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL }); // Apply to all routes
}
}
Apply Middleware to Specific Routes
If you want to apply the middleware only to specific routes, you can specify the routes to which it should apply:
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { AuditMiddleware } from './audit.middleware';
@Module({
imports: [UsersModule, AuthModule], // Add other modules here
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuditMiddleware)
.forRoutes(
{ path: 'auth/*', method: RequestMethod.ALL }, // Apply to all auth routes
{ path: 'users/*', method: RequestMethod.ALL } // Apply to all user routes
);
}
}
Conclusion
Implementing IAM in a NestJS application involves several key components, including authentication, authorization, user provisioning and audit logging.
This article provided a comprehensive guide with practical examples to help you implement IAM in NestJS. With these components in place, your application will be more secure and better equipped to manage user identities and access to resources.
About the author: Chesvic Hillary
Chesvic Hillary is a Senior Back-End Engineer and technologist for Andela, one of the world’s largest private talent marketplaces. Based in Lagos, Nigeria, Chesvic is an experienced technical writer and code instructor who has mentored junior engineers and helped them develop programming skills while contributing to complex algorithm logic and architecture migration projects.