Skip to content
NestJS ns auth 4 min read

Authentication Overview

Authentication is the process of verifying who a caller is before your application trusts them with protected resources. NestJS does not ship a single opinionated auth system; instead it gives you composable building blocks — Passport strategies, Guards, and the module/provider system — that you assemble to fit your domain. This page surveys the main approaches so you can pick the right one before diving into the implementation pages.

The role of Passport

Passport is the de facto authentication middleware for Node.js, and NestJS wraps it with first-class support via @nestjs/passport. Passport’s core idea is the strategy: a self-contained unit that knows how to authenticate one kind of credential — a username/password form, a JWT bearer token, a Google OAuth callback, and so on. There are 500+ community strategies.

In NestJS you subclass PassportStrategy, implement a validate() method, and register it as a provider. Passport calls validate() after it parses the request; whatever you return becomes request.user.

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 readonly authService: AuthService) {
    super(); // defaults to fields 'username' and 'password'
  }

  async validate(username: string, password: string) {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user; // attached to request.user
  }
}

Tip: You can use NestJS authentication without Passport at all — a custom Guard that verifies a token is perfectly valid. Passport simply saves you from re-implementing well-tested credential parsing and the OAuth dance.

Stateful sessions vs stateless JWT

The biggest architectural decision is whether the server remembers who is logged in (sessions) or whether the client carries proof of identity on every request (JWT).

With sessions, the server creates a session record after login, stores it (memory, Redis, a database), and hands the client an opaque session ID in a cookie. Every subsequent request includes the cookie; the server looks the session up to identify the user.

With JWT (JSON Web Tokens), the server signs a token containing the user’s claims and returns it. The client sends it back (usually in an Authorization: Bearer header). The server validates the signature — no lookup required — making the approach naturally stateless and horizontally scalable.

ConcernSessionsJWT
Server storageRequired (Redis/DB)None for access tokens
ScalingNeeds shared session storeStateless, scales freely
RevocationInstant (delete the record)Hard — token valid until expiry
TransportCookieHeader or cookie
CSRF exposureYes, needs protectionLower with header transport
Best fitServer-rendered apps, BFFSPAs, mobile, microservices

A common modern pattern is short-lived JWT access tokens plus long-lived refresh tokens: the access token expires in minutes, and a stored refresh token lets the client mint a new one without re-entering credentials — recovering some revocability while keeping requests stateless.

How guards enforce authenticated access

A Guard is a NestJS provider implementing CanActivate. It runs before the route handler and decides whether the request proceeds. @nestjs/passport ships AuthGuard('<strategy>'), which invokes the named strategy and, on success, populates request.user.

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

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

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

  @UseGuards(AuthGuard('jwt'))
  @Get('profile')
  getProfile(@Request() req) {
    return req.user; // populated by the JWT strategy's validate()
  }
}

Guards can be applied per-route (@UseGuards), per-controller, or globally via APP_GUARD. A common production setup registers a JWT guard globally and opts specific routes out with a custom @Public() metadata decorator.

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './jwt-auth.guard';

@Module({
  providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AuthModule {}

When a guard rejects a request, Nest returns a 401 Unauthorized automatically.

Output:

$ curl -i http://localhost:3000/auth/profile
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"statusCode":401,"message":"Unauthorized"}

OAuth and social login

For “Sign in with Google/GitHub/etc.”, you delegate credential checking to a third-party provider over OAuth 2.0. Passport strategies such as passport-google-oauth20 handle the redirect, the authorization-code exchange, and profile retrieval; your validate() then finds or provisions a local user and the app issues its own session or JWT. This keeps a single internal identity model regardless of the upstream provider.

Best Practices

  • Never store plaintext passwords — hash with bcrypt or argon2 and only ever compare hashes.
  • Keep JWT secrets and signing keys in environment config, never in source; rotate them periodically.
  • Prefer short access-token lifetimes paired with refresh tokens over long-lived JWTs you cannot revoke.
  • Apply a sensible default by registering an auth guard globally and explicitly marking public routes.
  • Use HTTPS everywhere and set HttpOnly, Secure, and SameSite on any auth cookies.
  • Return generic error messages on failed logins to avoid leaking whether a username exists.
  • For sessions, back them with a shared store (Redis) so they survive restarts and scale across instances.
Last updated June 14, 2026
Was this helpful?