Guards: Authorization and Authentication
Implementing authentication using Passport.js or similar libraries. Creating guards to protect routes based on user roles and permissions.
Authorization with Passport.js in NestJS
Introduction to Passport.js
Passport.js is authentication middleware for Node.js. It's incredibly flexible and can be plugged into any Express-based web application, including NestJS. It provides a set of strategies for authenticating users using various methods, such as username/password, OAuth, OpenID, and more.
Understanding Authorization
Authorization is the process of determining what a user is allowed to do within a system. After authentication (proving who the user *is*), authorization determines their access rights and permissions (what they are *allowed* to do). Passport.js primarily handles authentication, but it lays the groundwork for robust authorization implementation in NestJS. We leverage authenticated user information to grant or deny access to specific resources or functionalities.
Passport.js Strategies
A Passport strategy is a mechanism for authenticating a user. Passport.js comes with many pre-built strategies, and you can also create your own. Here are a few common strategies:
- Local Strategy: Authenticates users using a username and password stored in your database.
- JWT Strategy: Authenticates users using JSON Web Tokens (JWTs).
- OAuth 2.0 Strategies: Authenticates users using OAuth 2.0 providers like Google, Facebook, GitHub, etc.
Session Management
Session management is crucial for maintaining a user's authenticated state across multiple requests. Passport.js typically relies on sessions (often with express-session
) to store the authenticated user's information. JWT strategies can be used instead for stateless authentication.
Session-based Authentication
With session-based authentication, the server stores user session information in memory or database. On each request, the browser sends the session ID (usually in a cookie), and the server uses this to retrieve the user's data.
Stateless Authentication with JWT
In this case, the server does not store any session information. Each request from the browser includes the JWT, and the server validates it to authenticate the user. JWT is typically signed to prevent tampering.
Implementing User Authorization in NestJS with Passport.js
Here's a step-by-step guide to implementing user authorization in NestJS using Passport.js:
1. Install Dependencies
First, install the necessary packages:
npm install --save @nestjs/passport passport passport-local @nestjs/jwt passport-jwt bcryptjs
Install session packages (for session-based authentication):
npm install --save express-session @types/express-session
2. Configure Passport Module
Create a passport.module.ts
:
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UserModule } from '../user/user.module'; // Import your UserModule
import { AuthService } from './auth.service'; // Auth Service for authentication logic
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config'; // For JWT secret
@Module({
imports: [
UserModule,
PassportModule.register({ session: true }), // Enable session support (or remove for JWT only)
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '60s' }, // Adjust as needed
}),
inject: [ConfigService],
}),
],
providers: [LocalStrategy, JwtStrategy, AuthService],
exports: [AuthService], // Export AuthService to be used in other modules
})
export class AuthModule {}
3. Create Local Strategy (Username/Password)
Create a file src/auth/strategies/local.strategy.ts
:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
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;
}
}
4. Create JWT Strategy
Create a file src/auth/strategies/jwt.strategy.ts
:
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: any): Promise<any> {
return { userId: payload.sub, username: payload.username };
}
}
5. Create Auth Service
Create src/auth/auth.service.ts
. This service handles the core authentication logic:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOne(username);
if (user && await bcrypt.compare(pass, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
6. Create Auth Controller
Create a controller src/auth/auth.controller.ts
to handle authentication endpoints.
import { Controller, Request, Post, UseGuards, Body } from '@nestjs/common';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { CreateUserDto } from '../user/dto/create-user.dto';
import { UserService } from '../user/user.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService, private userService: UserService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
@Post('register')
async register(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
@UseGuards(JwtAuthGuard)
@Post('profile')
getProfile(@Request() req) {
return req.user;
}
}
7. Create Guards
Create a guard src/auth/guards/local-auth.guard.ts
:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Create a guard src/auth/guards/jwt-auth.guard.ts
:
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(
context: ExecutionContext,
): boolean | Promise | Observable {
// Add your custom authentication logic here
// for example, calling super.logIn(request) to establish a session.
return super.canActivate(context);
}
handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
8. User Module and Service
This example assumes you have a User module with a UserService for handling user creation, retrieval, and validation. Ensure you have a `UserModule` and `UserService` set up that interact with your database.
Example user service: src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class UserService {
private users = []; // In-memory storage for example purposes. Replace with database interaction.
async create(createUserDto: CreateUserDto) {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(createUserDto.password, saltRounds);
const user = { ...createUserDto, userId: this.users.length + 1, password: hashedPassword };
this.users.push(user); // Simulate database storage.
return { userId: user.userId, username: user.username };
}
async findOne(username: string): Promise {
return this.users.find(user => user.username === username);
}
async findById(userId: number): Promise {
return this.users.find(user => user.userId === userId);
}
}
Example user module: src/user/user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
@Module({
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
9. Protect Routes
To protect routes, use the JwtAuthGuard
:
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Controller('profile')
export class ProfileController {
@UseGuards(JwtAuthGuard)
@Get()
getProfile(@Request() req) {
return req.user;
}
}
Common Authorization Flows
- Login: User provides credentials (username/password), the server authenticates the user using the local strategy, and returns a JWT.
- Protected Resource Access: User sends a request with the JWT in the
Authorization
header. The JWT strategy verifies the token, and if valid, grants access to the protected resource. - OAuth 2.0: The user is redirected to the OAuth provider (e.g., Google, Facebook) to authorize the application. The provider redirects the user back to your application with an authorization code, which your application then uses to obtain an access token. Passport.js handles the complexities of the OAuth flow.
Advanced Authorization Considerations
- Role-Based Access Control (RBAC): Assign roles (e.g., admin, user, moderator) to users and grant permissions based on those roles.
- Attribute-Based Access Control (ABAC): Make authorization decisions based on attributes of the user, resource, and environment.
- Fine-Grained Authorization: Control access to specific data fields or operations within a resource.
- Policies: Define authorization rules as policies that can be reused across different parts of the application.