In the domain of computer security, authentication and authorization are fundamental processes. Authentication, the initial step, validates a user's identity through methods like passwords or biometrics. Once authenticated, authorization comes into play, determining the user's permissions and access rights. It's crucial to note that authentication precedes authorization, establishing the user's identity before enforcing access control measures.
The purpose of this article is to provide a step-by-step guide for implementing authentication system in a NestJS project using the Passport middleware module.
Our authentication system will embody the key attributes of an exemplary system: it will be secure, easily extensible, and simple, as every good authentication system should be.
If you want to look at the full code you can check out my repo on Github.
Dependencies installation
- passport (^0.7.0) is an authentication middleware for Node.js, widely used and extensible.
- passport-local ( ^1.0.0) is a Passport strategy for authentication with an email and password.
- passport-jwt (^4.0.1)is a Passport strategy for authentication with a JSON Web Token (JWT).
- @nestjs/passport (^10.0.3) is a Passport integration for NestJS.
- @nestjs/jwt (^10.2.0) is used to handle JWT tokens in NestJS. JWT (JSON Web Tokens) is a compact and safe way to transmit information between parties as a JSON object. this information can be verified and trusted because it is digitally signed.
- bcrypt (5.1.1)is a library for hashing passwords.
npm install @nestjs/passport passport passport-local passport-jwt @nestjs/jwt bcrypt
❓ Why you should use Passport Module to implement your authentication service
For implementing authentication in your NestJs App I highly recommend relying on the Passport module. It enhances security through its robust strategies like JWT and OAuth, aligning with industry best practices. Among other things, it supports a plug-and-play architecture, enabling the addition of new authentication methods or the replacement of existing ones without major code modifications. This means you can effortlessly integrate custom authentication strategies, ensuring scalability and adaptability for your app's future needs. In other words, Passport ensures a smooth developer experience, making it easy for you to understand and implement authentication mechanisms.
User Module
Define a model (e.g., a class or an interface) for your users, including necessary fields such as email
, password
, etc. The user service responsible for all interactions with the database regarding users includes methods to create, find, update, and delete users. However, notice that the signup method must be handled by the authentication service.
Authentication Module
Your authentication module should be (at least) responsible for the following crucial tasks:
- User Identity verification
- Password Hashing and Storage
- Token Generation and Validation
Authentication Service
Create an AuthService
service that will inject necessary services, such as UsersService
for interacting with the user database and JwtService
for handling JWT tokens.
AuthService Methods:
- validateUser: Takes a username and password as input and checks if the user exists and if the password is correct.
- login: Takes user data as input and generates a JWT token
-
register: Checks if a user with the provided email already exists.If a user with the provided email already exists, throws a BadRequestException, if not, hash and salt the user's password using
bcrypt
, store the new user in the database and log the user in.
//auth.service.ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from 'src/users/users.entity';
import { AccessToken } from './types/AccessToken';
import { UsersService } from 'src/users/users.service';
import { RegisterRequestDto } from './dtos/register-request.dto';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User> {
const user: User = await this.usersService.findOneByEmail(email);
if (!user) {
throw new BadRequestException('User not found');
}
const isMatch: boolean = bcrypt.compareSync(password, user.password);
if (!isMatch) {
throw new BadRequestException('Password does not match');
}
return user;
}
async login(user: User): Promise<AccessToken> {
const payload = { email: user.email, id: user.id };
return { access_token: this.jwtService.sign(payload) };
}
async register(user: RegisterRequestDto): Promise<AccessToken> {
const existingUser = this.usersService.findOneByEmail(user.email);
if (existingUser) {
throw new BadRequestException('email already exists');
}
const hashedPassword = await bcrypt.hash(user.password, 10);
const newUser: User = { ...user, password: hashedPassword };
await this.usersService.create(newUser);
return this.login(newUser);
}
}
❓ Why is hashing and salting passwords mandatory?
A salt is simply a random data used as an additional input to the hashing function to safeguard your password. The random string from the salt makes the hash unpredictable.
A password hash involves converting the password into an alphanumeric string using specialized algorithms.
Hashing and salting are irreversible and ensure that even if someone gains access to the hashed passwords, they will not be able to decrypt them to recover the original passwords.
Hystorically bcrypt is recognized as the best hashing algorithm. However, in terms of robustness against all the new cryptographic attacks targeting hashing algorithms, the current clear winner isargon2
. However, since the “youth" (2015) of this algorithm, I chose to usebcrypt
Passport Configuration
Create a Local Strategy for authentication with username and password.
You need to create a LocalStrategy
that extends PassportStrategy
.
This authentication method is primarily used for authenticating users based on local credentials such as usernames and passwords. By default the strategy looks for a username
field, in the code below we override usernameField
to look for an email
field since we want to login with an email.
- validate method : Requires the server to check the provided username and password against the authentication service.
//local.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthenticationService } from '../authentication.service';
import { User } from '../../app/users/users.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthenticationService) {
super({
usernameField: 'email',
});
}
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Create a JWT Strategy to validate JWT tokens
The JwtStrategy
validates the token sent by the user. It extracts the user ID from the token and looks it up in the database.
This authentication method is used for authenticating users based on the presence and validity of a JSON Web Token (JWT). It extracts the user ID from the token and looks it up in the database.
-
validate method: Involves checking the digital signature of the JWT to ensure its integrity and validating claims to determine the user's identity and permissions. The
validate
method receives the decoded data from the JWT token and returns user data if the token is valid. You can add additional logic, e.g., to fetch additional user information from the database.
//jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { AccessTokenPayload } from '../types/AccessTokenPayload';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_SECRET'),
});
}
async validate(payload: AccessTokenPayload) {
return payload;
}
}
Authentication Controller
Create endpoints for signup (register
) and login (login
) that both return JWT tokens. For the login endpoint, the@UseGuards
decorator is applied to the login
method, indicating that the AuthGuard
named 'local' should be used, that will trigger the LocalStrategy
previously implemented.
// auth.controller.ts
import {
BadRequestException,
Body,
Controller,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import { RegisterRequestDto } from './dtos/register-request.dto';
import { LoginResponseDTO } from './dtos/login-response.dto';
import { RegisterResponseDTO } from './dtos/register-response.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req): Promise<LoginResponseDTO | BadRequestException> {
return this.authService.login(req.user);
}
@Post('register')
async register(
@Body() registerBody: RegisterRequestDto,
): Promise<RegisterResponseDTO | BadRequestException> {
return await this.authService.register(registerBody);
}
}
❓ Passing the Access Token to the client
In this tutorial we pass the access token to the client in the response body for simplicity reasons, but it can also be done with a cookie. Since cookies are automatically included in every HTTP request, it simplifies the process of including the access token without explicit action from the client.
You can read this article Cookie-Based Authentication vs Token-Based Authentication to choose the most adapted method to your case.
Authentication Module Configuration
Your AuthModule
should import the JwtModule
to handle JSON Web Token (JWT) generation and verification.
The registerAsync
method allows for asynchronous configuration, facilitating the use of the ConfigService
to retrieve the JWT secret from environment variables stored in a .env
file.
Storing sensitive configurations in a .env
file as environment variables is preferred because it enhances security and avoids accidental exposure in version control systems (⚠️ don’t forget to add it in the .gitignore
). This practice facilitates easy configuration management across different environments while preserving confidentiality and minimizing the risk of inadvertent information disclosure.
// auth.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { LocalStrategy } from './strategy/local.strategy';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';
@Module({
imports: [
UsersModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: parseInt(
configService.getOrThrow<string>(
'ACCESS_TOKEN_VALIDITY_DURATION_IN_SEC',
),
),
},
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
Routes security: implementing a whitelisting strategy
❓ Why should you use a whitelisting strategy ?
To ensure the security of your app, the best practice is to choose a whitelisting strategy. Whitelisting, involves explicitly allowing only approved elements or actions, providing precise control over permissions in a system. It is considered a better practice than blacklisting as it reduces the attack surface by permitting only known and necessary entities but also minimizes developers' errors resulting from oversights or thoughtlessness.
In our context, to implement whitelisting you basically just have to create a guard for JWT-based authentication, and a decorator to designate public routes exempt from JWT authentication. This strategy offers a granular and flexible approach to securing specific endpoints.
Create a guard and a decorator
First, create the following decorator:
//public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true);
Then you can add the following JwtGuard
class:
//jwt.guard.ts
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(
context: ExecutionContext,
): Promise<boolean> | boolean | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
}
-
Class constructor: takes a single parameter - an instance of the
Reflector
class. TheReflector
class is part of the core NestJS library and is used to retrieve metadata associated with classes, methods, and their decorators. -
canActivate Method:
The
canActivate
method is a crucial part of theJwtGuard
class. It is invoked during the request lifecycle to determine whether the current request should be allowed or denied based on the provided JWT.- The first step is to check if the route or endpoint is marked as public using the
@Public
decorator previously created. - The
Reflector
is used to retrieve metadata labeled as 'isPublic' from the handler and class associated with the current execution context. - If the route is marked as public, the method returns
true
, indicating that no further authentication checks are needed. - If the route is not marked as public, the
super.canActivate(context)
call invokes the corresponding method in the parent class (AuthGuard
). This method performs the actual JWT validation and authentication checks.
- The first step is to check if the route or endpoint is marked as public using the
Configure the AppModule
Within the providers
array, the provider for the JWT guard is registered using the APP_GUARD
token. This provider specifies the use of the JwtGuard
class as the global guard for the entire application.
//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { JwtStrategy } from './auth/strategy/jwt.strategy';
import { APP_GUARD } from '@nestjs/core';
import { JwtGuard } from './auth/guards/jwt.guard';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
...
UsersModule,
AuthModule,
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtGuard,
},
JwtStrategy,
],
})
export class AppModule {}
Now if the routes are not marked with @Public()
, every incoming request undergoes JWT authentication.
For example, in the GET endpoint below, the global guard validates and decodes the JWT access token and attaches the user information to the request object, making it accessible in the controller methods.
//app.controller.ts
import { Controller, Get, Request } from '@nestjs/common';
import { AppService } from './app.service';
import { AccessTokenPayload } from './auth/types/AccessTokenPayload';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
async getHello(@Request() req): Promise<string> {
const accessTokenPayload: AccessTokenPayload =
req.user as AccessTokenPayload;
return await this.appService.getHello(accessTokenPayload.userId);
}
Specify public routes
You have now the option to apply the @Public()
decorator to methods or classes that don't require JWT authentication. In our scenario, we apply this decorator to the AuthController
.
//auth.controller.ts
@Public()
@Controller('auth')
export class AuthController {
//...
}
Create custom decorators
In order to make your code more readable and transparent, you can create your own custom decorator and reuse it across all of your controllers. It’s a very good practice to avoid code duplication.
Depending on the specific requirements of your project, (e.g. Role-based access control), you can create numerous and various decorators and guards to address diverse authorization scenarios. For this article we will only create a @User()
decorator that extracts the user information from the request object in the NestJS execution context.
// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
Then you just have to pass it to your controller to retrieve the user information encapsulated in your request.
//app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { User } from './auth/decorators/user.decorator';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
async getHello(@User() user): Promise<string> {
return await this.appService.getHello(user.id);
}
}
Conclusion
In conclusion, crafting a secure and efficient authentication system in a NestJS application is a meticulous but easy process. We have integrated Passport to handle various authentication strategies, used JWT tokens to enhance security, and the global guards to ensure consistent authentication across the entire project. By prioritizing security, simplicity, and extensibility in our implementation, we have established a foundation that not only safeguards user data but also allows for seamless integration of future features.
By following this step-by-step guide, you can establish a robust authentication system in your NestJS applications, promoting secure user interactions and data protection. The provided code snippets serve as a foundation for building upon and adapting to specific project requirements (Role-based access control, OAuth).
However, I did not get into refresh token even though they are an essential component of secure authentication systems, particularly in scenarios where long-lived sessions are required. A deep dive into implementing refresh tokens in a NestJS context could be valuable for you if you aim to build robust and secure authentication systems.
Top comments (0)