XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksNestJSAuthentication & Authorization

Authentication & Authorization

authjwtguardspassporthandbook

TL;DR

  • NestJS uses Guards for authorization (@UseGuards()).
  • Passport.js integration via @nestjs/passport for strategies (JWT, OAuth, Local).
  • JWT flow: Login → Issue token → Client sends token → Guard validates → Access granted.
  • Role-based access: Custom decorators + guards for granular permissions.

Step 1: JWT Authentication Flow

JWT (JSON Web Tokens) became the standard for API authentication because traditional session-based auth doesn't scale across microservices and serverless functions — you'd need shared session storage. JWTs are self-contained tokens that carry user identity and claims, can be verified without a database lookup (using cryptographic signatures), and work across services without shared state. Understanding the full flow (login → token issued → token sent on requests → token verified) is essential for any backend developer.

┌──────────┐        ┌──────────┐        ┌──────────────┐
│  Client  │──1────▶│  Auth    │──2────▶│   Database   │
│          │◀──3────│  Module  │◀───────│              │
│          │        └──────────┘        └──────────────┘
│          │
│          │──4────▶┌──────────┐
│          │        │ Protected│ (JWT in Authorization header)
│          │◀──5────│  Route   │
└──────────┘        └──────────┘

1. POST /auth/login (email + password)
2. Validate credentials against DB
3. Return JWT access token (+ optional refresh token)
4. Request with "Authorization: Bearer <token>"
5. Guard validates token → route handler executes

Step 2: Setting Up JWT Auth

This setup implements the JWT flow in NestJS using Passport.js (the de facto Node.js auth library) with NestJS's module system. The pieces are: a JWT module that signs tokens, a strategy that validates incoming tokens, and guards that protect routes. NestJS's dependency injection makes this elegant — you configure once in the auth module and apply protection anywhere with a single decorator. This pattern handles 90% of API authentication needs.

Install Dependencies

npm install @nestjs/passport @nestjs/jwt passport passport-jwt passport-local bcrypt
npm install -D @types/passport-jwt @types/passport-local @types/bcrypt

Auth Module

// 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";
import { UsersModule } from "../users/users.module";

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: "15m" }, // Short-lived access token
    }),
  ],
  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 * as bcrypt from "bcrypt";

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

  async validateUser(email: string, password: string) {
    const user = await this.usersService.findByEmail(email);
    if (!user) throw new UnauthorizedException("Invalid credentials");

    const passwordValid = await bcrypt.compare(password, user.passwordHash);
    if (!passwordValid) throw new UnauthorizedException("Invalid credentials");

    return user;
  }

  async login(user: { id: string; email: string; role: string }) {
    const payload = { sub: user.id, email: user.email, role: user.role };

    return {
      accessToken: this.jwtService.sign(payload),
      refreshToken: this.jwtService.sign(payload, { expiresIn: "7d" }),
    };
  }

  async refreshToken(token: string) {
    try {
      const payload = this.jwtService.verify(token);
      const user = await this.usersService.findById(payload.sub);
      return this.login(user);
    } catch {
      throw new UnauthorizedException("Invalid refresh token");
    }
  }
}

Step 3: Passport Strategies

Passport strategies are pluggable authentication mechanisms — each one knows how to validate a specific credential type (username/password, OAuth token, API key, etc.). The strategy pattern was chosen because authentication methods are endlessly varied but the framework integration (extracting credentials, attaching user to request) is always the same. NestJS wraps Passport strategies in injectable classes, giving you type safety and DI while keeping the battle-tested Passport ecosystem available.

Local Strategy (Username/Password Login)

// auth/strategies/local.strategy.ts
import { Strategy } from "passport-local";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { AuthService } from "../auth.service";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({ usernameField: "email" }); // Use "email" instead of "username"
  }

  async validate(email: string, password: string) {
    // Whatever this returns is attached to request.user
    return this.authService.validateUser(email, password);
  }
}

JWT Strategy (Token Validation)

// auth/strategies/jwt.strategy.ts
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: { sub: string; email: string; role: string }) {
    // This becomes request.user
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

Step 4: Guards & Controllers

NestJS guards are the enforcement layer — they intercept requests before they hit your controller and decide whether to allow or reject. Unlike middleware (which is generic), guards have access to the execution context (which route, which handler, which decorators are applied), making them perfect for auth decisions. The @UseGuards(AuthGuard('jwt')) pattern protects any route with one decorator, and custom guards let you implement role-based access, API key validation, or rate limiting.

Auth Controller

// auth/auth.controller.ts
import { Controller, Post, UseGuards, Request, Body } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { LocalAuthGuard } from "./guards/local-auth.guard";
import { Public } from "./decorators/public.decorator";

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

  @Public() // Skip JWT guard for login
  @UseGuards(LocalAuthGuard)
  @Post("login")
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @Public()
  @Post("refresh")
  async refresh(@Body("refreshToken") token: string) {
    return this.authService.refreshToken(token);
  }
}

JWT Auth Guard (Global)

// auth/guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Reflector } from "@nestjs/core";
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
  constructor(private reflector: Reflector) {
    super();
  }

  canActivate(context: ExecutionContext) {
    // Check if route is marked @Public()
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    return super.canActivate(context);
  }
}

Public Decorator

// auth/decorators/public.decorator.ts
import { SetMetadata } from "@nestjs/common";

export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Register Guard Globally

// app.module.ts
import { APP_GUARD } from "@nestjs/core";
import { JwtAuthGuard } from "./auth/guards/jwt-auth.guard";

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard, // All routes protected by default
    },
  ],
})
export class AppModule {}

Step 5: Role-Based Authorization

Role-based access control (RBAC) is the most common authorization pattern: users have roles (admin, editor, viewer), and routes require specific roles. NestJS implements this with custom decorators (to declare required roles on routes) and guards (to check the authenticated user's roles against requirements). This separation of declaration and enforcement keeps controllers clean and makes authorization logic testable and reusable. The pattern extends to permission-based or attribute-based access control as your needs grow.

Roles Decorator

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

export enum Role {
  User = "user",
  Admin = "admin",
  SuperAdmin = "superadmin",
}

export const ROLES_KEY = "roles";
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Roles Guard

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

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) return true; // No roles required

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

Usage

@Controller("admin")
export class AdminController {
  @Get("users")
  @Roles(Role.Admin, Role.SuperAdmin)
  @UseGuards(RolesGuard)
  getUsers() {
    return this.usersService.findAll();
  }

  @Delete("users/:id")
  @Roles(Role.SuperAdmin)
  @UseGuards(RolesGuard)
  deleteUser(@Param("id") id: string) {
    return this.usersService.delete(id);
  }
}

Step 6: Complete Auth Flow

This brings all the pieces together into a production-ready authentication flow: registration (hash password, store user), login (validate credentials, issue JWT), protected routes (verify token, extract user), and refresh tokens (issue short-lived access tokens with long-lived refresh tokens for security). Refresh tokens solve the UX problem of short token expiry — users don't re-login every 15 minutes, but compromised tokens expire quickly.

// Client-side usage:

// 1. Login
const res = await fetch("/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "user@example.com", password: "secret" }),
});
const { accessToken, refreshToken } = await res.json();

// 2. Access protected route
const data = await fetch("/users/me", {
  headers: { Authorization: `Bearer ${accessToken}` },
});

// 3. Refresh when access token expires
const newTokens = await fetch("/auth/refresh", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ refreshToken }),
});

Interview Questions

  1. How does JWT authentication work in NestJS?

    • User logs in → server validates credentials → issues signed JWT → client sends JWT in Authorization header → JwtAuthGuard validates signature and expiry → request.user is populated.
  2. What's the difference between authentication and authorization?

    • Authentication: "Who are you?" (login, JWT validation). Authorization: "What can you do?" (roles, permissions, guards).
  3. Why use short-lived access tokens + refresh tokens?

    • Access tokens (15min) limit damage if stolen. Refresh tokens (7d) provide good UX without re-login. Refresh tokens can be revoked server-side.
  4. How do Guards work in NestJS?

    • Guards implement CanActivate. They run before route handlers and return true (allow) or false/throw (deny). They have access to ExecutionContext for request details.
Guards & MiddlewareMicroservices & Communication Patterns

On this page

  • TL;DR
  • Step 1: JWT Authentication Flow
  • Step 2: Setting Up JWT Auth
  • Install Dependencies
  • Auth Module
  • Auth Service
  • Step 3: Passport Strategies
  • Local Strategy (Username/Password Login)
  • JWT Strategy (Token Validation)
  • Step 4: Guards & Controllers
  • Auth Controller
  • JWT Auth Guard (Global)
  • Public Decorator
  • Register Guard Globally
  • Step 5: Role-Based Authorization
  • Roles Decorator
  • Roles Guard
  • Usage
  • Step 6: Complete Auth Flow
  • Interview Questions