Guards: Authorization and Authentication

Implementing authentication using Passport.js or similar libraries. Creating guards to protect routes based on user roles and permissions.


NestJS Authentication with Passport.js

Introduction to Passport.js

Passport.js is authentication middleware for Node.js. It's designed to be unobtrusive and supports a wide variety of authentication strategies, from simple username/password logins to delegating authentication to providers like Google, Facebook, Twitter, and more. In essence, it provides a clean and consistent interface for authenticating users across different methods.

Authentication with Passport.js: Core Concepts

Strategies

A Passport "strategy" is a mechanism for authenticating users. Each strategy handles a specific type of authentication. Common strategies include:

  • Local Strategy: Authenticates users based on a username and password stored in your application's database.
  • OAuth 2.0 Strategies (Google, Facebook, etc.): Delegate authentication to third-party providers. Users log in to the provider, and your application receives an access token to verify their identity.
  • JWT (JSON Web Token) Strategy: Authenticates users using tokens generated and verified using cryptographic keys. JWTs are stateless and suitable for API authentication.
  • OpenID Connect Strategy: An authentication layer on top of OAuth 2.0. Provides a standardized way for applications to verify user identities based on a central identity provider.

Authentication Flows

The general authentication flow using Passport.js involves these steps:

  1. User Initiates Login: The user attempts to log in to your application.
  2. Route Handling: Your application's route handler triggers the appropriate Passport.js strategy.
  3. Strategy Execution: The chosen strategy performs the authentication process (e.g., verifying username/password, redirecting to a third-party provider).
  4. Callback Handling: If authentication is successful, the strategy provides user information to a callback function.
  5. User Serialization/Deserialization: Passport.js serializes and deserializes user information to manage sessions (if using sessions).
  6. Authorization: The application uses the authenticated user information to grant access to protected resources.

Session Management (Optional, but common)

When using sessions, Passport.js serializes the user object into the session store and deserializes it when subsequent requests are made. This avoids the need to authenticate the user on every single request. Typically, you'll serialize only a user ID and then retrieve the full user object from the database during deserialization.

Implementing User Authentication in NestJS with Passport.js

1. Installation

Install the necessary packages:

npm install --save @nestjs/passport passport passport-local @nestjs/jwt passport-jwt bcrypt
  • @nestjs/passport: Provides the Passport.js module for NestJS.
  • passport: The core Passport.js library.
  • passport-local: The Passport.js strategy for local authentication.
  • @nestjs/jwt: NestJS module for JWT handling.
  • passport-jwt: The Passport.js strategy for JWT authentication.
  • bcrypt: A library for hashing passwords.

2. Configure the Passport Module

Import the PassportModule in your main app module (app.module.ts):

 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 { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    AuthModule,
    UsersModule,
    PassportModule.register({ session: true }), // Session is optional
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {} 

The PassportModule.register({ session: true }) line configures Passport to use sessions. Remove the { session: true } if you're using a stateless strategy like JWT.

3. Create User Module

Create a users module to manage user data (users/users.module.ts):

 import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  providers: [UsersService],
  controllers: [UsersController],
  exports: [UsersService], // Important for AuthModule to access UsersService
})
export class UsersModule {} 

4. User Entity/Service (Example)

Define your user entity and service (example using in-memory data, replace with database access):

users/users.service.ts

 import { Injectable } from '@nestjs/common';

export interface User {
  id: number;
  username: string;
  password?: string; // Password should be hashed and not stored in plain text
}

@Injectable()
export class UsersService {
  private readonly users: User[] = [
    { id: 1, username: 'testuser', password: 'hashedpassword' }, // Replace with secure password storage
  ];

  async findOne(username: string): Promise<User | undefined> {
    return this.users.find(user => user.username === username);
  }

  async findById(id: number): Promise<User | undefined> {
    return this.users.find(user => user.id === id);
  }
} 

Important: Never store passwords in plain text. Use a library like bcrypt to hash passwords before storing them in the database.

5. Create Authentication Module

Create an auth module (auth/auth.module.ts):

 import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from '../users/users.module';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: 'YOUR_SECRET_KEY', // Replace with a strong, random secret
      signOptions: { expiresIn: '60s' }, // Adjust as needed
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {} 

Note: Replace 'YOUR_SECRET_KEY' with a strong, randomly generated secret key. This is crucial for JWT security.

6. Implement Local Strategy

Create a local.strategy.ts file:

 import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {}

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user; // Return the user object
  }
} 

7. Implement JWT Strategy

Create a jwt.strategy.ts file:

 import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly usersService: UsersService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'YOUR_SECRET_KEY', // Same secret as in JwtModule
    });
  }

  async validate(payload: any) {
    const user = await this.usersService.findById(payload.sub); // 'sub' typically contains the user ID
    if (!user) {
        return null; // Or throw an exception
    }
    return user; // Return the user object
  }
} 

Remember to replace 'YOUR_SECRET_KEY' with the same secret used in the JwtModule.

8. Implement Authentication Service

Create an auth.service.ts file:

 import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password) {
      const passwordMatches = await bcrypt.compare(pass, user.password);
      if (passwordMatches) {
        const { password, ...result } = user; // Exclude password from the returned object
        return result;
      }
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.id }; // 'sub' for user ID
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
} 

9. Implement Authentication Controller

Create an auth.controller.ts file:

 import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { LocalAuthGuard } from './local-auth.guard';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
} 

10. Create Authentication Guards

Create the local-auth.guard.ts file:

 import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {} 

Create the jwt-auth.guard.ts file:

 import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {} 

Common Authentication Flows

Local Authentication (Username/Password)

  1. The user submits their username and password to the /auth/login endpoint.
  2. The LocalAuthGuard intercepts the request.
  3. Passport.js uses the LocalStrategy to validate the username and password against the database.
  4. If validation succeeds, the AuthService.login method is called to generate a JWT token.
  5. The JWT token is returned to the client.

JWT Authentication

  1. The client sends a request to a protected endpoint (e.g., /auth/profile) with the JWT token in the Authorization header (Bearer <token>).
  2. The JwtAuthGuard intercepts the request.
  3. Passport.js uses the JwtStrategy to verify the token's signature and extract the user information from the payload.
  4. If the token is valid, the user object is attached to the request (req.user).
  5. The route handler can then access the authenticated user information.

Important Considerations

  • Security: Always use strong, randomly generated secrets for JWT signing. Protect your secrets!
  • Password Hashing: Never store passwords in plain text. Use a robust hashing algorithm like bcrypt.
  • Token Expiration: Set appropriate expiration times for JWT tokens to limit the impact of compromised tokens.
  • Refresh Tokens: Consider using refresh tokens to allow users to stay logged in for longer periods without repeatedly entering their credentials.
  • CORS: Configure CORS (Cross-Origin Resource Sharing) properly to allow requests from your frontend application.
  • Input Validation: Thoroughly validate all user input to prevent security vulnerabilities like SQL injection and cross-site scripting (XSS).