Skip to main content

Chapter 6: Authentication & Authorization

Securing your application is critical. This chapter covers authentication (verifying identity), authorization (controlling access), JWT, OAuth, RBAC, password hashing, guards, and best practices for building secure APIs in NestJS. We’ll walk through practical examples and explain the “why” behind each step.

6.1 Authentication vs Authorization

Understanding the difference between authentication and authorization is fundamental to building secure applications.

Authentication

Authentication answers: “Who are you?”
  • Verifies user identity
  • Confirms credentials (username/password, token, etc.)
  • Establishes user session
  • Examples: Login, token validation

Authorization

Authorization answers: “What are you allowed to do?”
  • Controls access to resources
  • Enforces permissions and roles
  • Determines what actions are allowed
  • Examples: Admin-only routes, resource ownership checks
Analogy:
Think of authentication as showing your ID at the door (proving who you are), and authorization as checking if you’re allowed into the VIP section (what you can access).

The Relationship

Authentication (Who) → Authorization (What)
    ↓                      ↓
  Login              →  Access Control
  Token Validation   →  Role Checks
  Identity Verify    →  Permission Checks

6.2 Password Hashing

Before implementing authentication, you must understand password security. Never store passwords in plain text.

Why Hash Passwords?

  • Security: Even if database is compromised, passwords are protected
  • Privacy: Developers can’t see user passwords
  • Compliance: Required by security standards (GDPR, PCI-DSS)

Using bcrypt

npm install bcrypt
npm install --save-dev @types/bcrypt
// hash/hash.service.ts
import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

@Injectable()
export class HashService {
  async hash(password: string): Promise<string> {
    const saltRounds = 10;
    return bcrypt.hash(password, saltRounds);
  }

  async compare(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}

Password Validation

// dto/register.dto.ts
import { IsString, MinLength, Matches } from 'class-validator';

export class RegisterDto {
  @IsString()
  @MinLength(8)
  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, {
    message: 'Password must contain uppercase, lowercase, number, and special character',
  })
  password: string;
}

6.3 JWT Authentication

JSON Web Tokens (JWT) are a popular way to implement stateless authentication in APIs. JWTs are signed tokens that clients send with each request to prove their identity.

How JWT Works

  1. User logs in with credentials
  2. Server validates credentials
  3. Server creates and signs a JWT
  4. Client stores the JWT
  5. Client sends JWT in Authorization header
  6. Server verifies JWT and extracts user info
Diagram: JWT Flow
Client → [Login Request] → Server

Server validates credentials

Server creates JWT (signed)

Server returns JWT → Client

Client stores JWT

Client → [Request with JWT] → Server

Server verifies JWT

Server grants access

Installing JWT Package

npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install --save-dev @types/passport-jwt

JWT Module Setup

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

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'your-secret-key',
      signOptions: { expiresIn: '1d' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

Auth Service

// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { HashService } from '../hash/hash.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';

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

  async register(registerDto: RegisterDto) {
    const hashedPassword = await this.hashService.hash(registerDto.password);

    const user = await this.usersService.create({
      ...registerDto,
      password: hashedPassword,
    });

    const { password, ...result } = user;
    return result;
  }

  async validateUser(email: string, password: string): Promise<any> {
    const user = await this.usersService.findByEmail(email);
    
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const isPasswordValid = await this.hashService.compare(password, user.password);
    
    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const { password: _, ...result } = user;
    return result;
  }

  async login(loginDto: LoginDto) {
    const user = await this.validateUser(loginDto.email, loginDto.password);
    
    const payload = { email: user.email, sub: user.id, roles: user.roles };
    
    return {
      access_token: this.jwtService.sign(payload),
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
      },
    };
  }

  async validateToken(token: string) {
    try {
      return this.jwtService.verify(token);
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

JWT Strategy

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private usersService: UsersService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET || 'your-secret-key',
    });
  }

  async validate(payload: any) {
    const user = await this.usersService.findOne(payload.sub);
    
    if (!user) {
      throw new UnauthorizedException();
    }

    return { id: user.id, email: user.email, roles: user.roles };
  }
}

Local Strategy (Username/Password)

// 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({ usernameField: 'email' });
  }

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

Auth Controller

// auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Get, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';

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

  @Post('register')
  async register(@Body() registerDto: RegisterDto) {
    return this.authService.register(registerDto);
  }

  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Body() loginDto: LoginDto, @Request() req) {
    return this.authService.login(req.user);
  }

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

JWT Auth Guard

// auth/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

Local Auth Guard

// auth/guards/local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

Using JWT Guard

// users/users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@Controller('users')
@UseGuards(JwtAuthGuard)  // Protect all routes
export class UsersController {
  @Get()
  findAll() {
    return [];
  }
}

6.4 Authorization: Roles & Permissions

Role-Based Access Control (RBAC) restricts access based on user roles. This lets you control who can do what in your app.

Roles Decorator

// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Roles Guard

// auth/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    
    if (!requiredRoles) {
      return true;  // No roles required
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;

    if (!user || !user.roles) {
      return false;
    }

    return requiredRoles.some(role => user.roles.includes(role));
  }
}

Using Roles

// users/users.controller.ts
import { Controller, Delete, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';

@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
  @Delete(':id')
  @Roles('admin')
  remove(@Param('id') id: string) {
    return this.usersService.remove(id);
  }
}

Permission-Based Authorization

For more granular control, use permissions:
// auth/decorators/permissions.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Permissions = (...permissions: string[]) => 
  SetMetadata('permissions', permissions);

// auth/guards/permissions.guard.ts
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredPermissions = this.reflector.get<string[]>(
      'permissions',
      context.getHandler(),
    );

    if (!requiredPermissions) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;

    return requiredPermissions.every(permission => 
      user.permissions?.includes(permission)
    );
  }
}

6.5 OAuth2 & Social Login

NestJS supports OAuth2 via Passport strategies. This lets users log in with Google, Facebook, GitHub, etc.

Installing Passport OAuth

npm install @nestjs/passport passport-google-oauth20
npm install --save-dev @types/passport-google-oauth20

Google Strategy

// auth/strategies/google.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
      scope: ['email', 'profile'],
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: any,
    done: VerifyCallback,
  ): Promise<any> {
    const { name, emails, photos } = profile;
    const user = {
      email: emails[0].value,
      firstName: name.givenName,
      lastName: name.familyName,
      picture: photos[0].value,
      accessToken,
    };
    done(null, user);
  }
}

Google Auth Controller

// auth/auth.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

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

  @Get('google')
  @UseGuards(AuthGuard('google'))
  async googleAuth(@Req() req) {
    // Initiates Google OAuth flow
  }

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleAuthRedirect(@Req() req) {
    const user = req.user;
    
    // Create or find user in database
    const dbUser = await this.authService.findOrCreateUser(user);
    
    // Generate JWT
    const token = this.authService.generateToken(dbUser);
    
    return { access_token: token, user: dbUser };
  }
}

6.6 Refresh Tokens

Refresh tokens allow users to get new access tokens without re-authenticating.

Implementing Refresh Tokens

// auth/auth.service.ts
async login(loginDto: LoginDto) {
  const user = await this.validateUser(loginDto.email, loginDto.password);
  
  const payload = { email: user.email, sub: user.id };
  
  const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });
  const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });

  // Store refresh token in database
  await this.usersService.updateRefreshToken(user.id, refreshToken);

  return {
    access_token: accessToken,
    refresh_token: refreshToken,
    user: {
      id: user.id,
      email: user.email,
    },
  };
}

async refreshToken(refreshToken: string) {
  try {
    const payload = this.jwtService.verify(refreshToken);
    const user = await this.usersService.findOne(payload.sub);

    if (user.refreshToken !== refreshToken) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    const newPayload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(newPayload, { expiresIn: '15m' }),
    };
  } catch (error) {
    throw new UnauthorizedException('Invalid refresh token');
  }
}

6.7 Multi-Factor Authentication (MFA)

Enhance security by requiring a second factor (e.g., OTP, email code) after password login.

MFA Service

// auth/mfa.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { EmailService } from '../email/email.service';

@Injectable()
export class MfaService {
  constructor(
    private usersService: UsersService,
    private emailService: EmailService,
  ) {}

  async generateOTP(userId: number): Promise<string> {
    const otp = Math.floor(100000 + Math.random() * 900000).toString();
    const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes

    await this.usersService.saveOTP(userId, otp, expiresAt);
    await this.emailService.sendOTP(userId, otp);

    return otp;
  }

  async verifyOTP(userId: number, otp: string): Promise<boolean> {
    const user = await this.usersService.findOne(userId);
    
    if (!user.otp || user.otp !== otp) {
      return false;
    }

    if (new Date() > user.otpExpiresAt) {
      return false;
    }

    await this.usersService.clearOTP(userId);
    return true;
  }
}

6.8 Security Best Practices

Following security best practices protects your application and users.

Password Security

  • Always hash passwords (use bcrypt with salt rounds >= 10)
  • Enforce strong password policies
  • Never log or return passwords
  • Use password reset tokens (time-limited)

JWT Security

  • Use strong, random secrets
  • Set appropriate expiration times
  • Use HTTPS in production
  • Store secrets in environment variables
  • Consider refresh tokens for long sessions

HTTP Security Headers

// main.ts
import helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(helmet());
  await app.listen(3000);
}

Rate Limiting

npm install @nestjs/throttler
// app.module.ts
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
})
export class AppModule {}

CORS Configuration

// main.ts
app.enableCors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true,
});

Input Validation

Always validate and sanitize input:
app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }),
);

Security Checklist

  • Always hash passwords
  • Use HTTPS in production
  • Store secrets in environment variables
  • Set secure HTTP headers (helmet)
  • Limit login attempts (prevent brute force)
  • Validate and sanitize all input
  • Keep dependencies up to date
  • Use CORS to restrict allowed origins
  • Log authentication events for auditing
  • Implement rate limiting
  • Use refresh tokens for long sessions
  • Consider MFA for sensitive applications

6.9 Real-World Example: Complete Auth Module

Here’s a complete authentication module structure:
auth/
├── auth.module.ts
├── auth.service.ts
├── auth.controller.ts
├── strategies/
│   ├── jwt.strategy.ts
│   ├── local.strategy.ts
│   └── google.strategy.ts
├── guards/
│   ├── jwt-auth.guard.ts
│   ├── local-auth.guard.ts
│   └── roles.guard.ts
├── decorators/
│   ├── roles.decorator.ts
│   └── permissions.decorator.ts
└── dto/
    ├── login.dto.ts
    ├── register.dto.ts
    └── refresh-token.dto.ts

6.10 Summary

You’ve learned how to secure your NestJS APIs: Key Concepts:
  • Authentication: Verifying user identity
  • Authorization: Controlling access to resources
  • JWT: Stateless token-based authentication
  • OAuth2: Social login integration
  • RBAC: Role-based access control
  • Password Hashing: Secure password storage
  • MFA: Multi-factor authentication
Best Practices:
  • Always hash passwords
  • Use HTTPS in production
  • Store secrets in environment variables
  • Set secure HTTP headers
  • Implement rate limiting
  • Validate all input
  • Use refresh tokens
  • Log authentication events
Next Chapter: Learn about testing strategies, including unit tests, integration tests, and E2E tests.