NestJS — Enterprise Node.js

JWT Authentication

18 min Lesson 27 of 30

JWT Authentication

Verifying a password once is not enough — the user makes many requests afterward, and you cannot ask for the password every time. A JWT (JSON Web Token) solves this: after login you issue a signed token, the client sends it on every request, and your server verifies it without a database lookup. This is the backbone of stateless authentication.

What a JWT is

A JWT is a signed string with three dot-separated parts: a header, a payload (claims like the user id and expiry), and a signature. Because it is signed with a secret only your server knows, the server can trust the payload without storing any session.

A JWT is signed, not encrypted. Anyone can decode and read the payload. Never put secrets (passwords, sensitive data) in it — only put non-sensitive identifiers like the user id and roles.

Issuing a token on login

Install @nestjs/jwt and configure it (secret + expiry come from config). After the local strategy validates the user, sign a token:

// JwtModule.registerAsync({ ... secret, signOptions: { expiresIn: '15m' } }) import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor(private jwt: JwtService) {} login(user: { id: number; email: string }) { const payload = { sub: user.id, email: user.email }; return { access_token: this.jwt.sign(payload) }; } }
The sub claim is the standard place for the subject (the user id). Keeping payloads small and standards-based keeps tokens compact and interoperable.

The JWT strategy

To protect routes, a JWT strategy extracts the token from the Authorization: Bearer header, verifies the signature, and returns the user info from the payload:

import { Strategy, ExtractJwt } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(config: ConfigService) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: config.get('JWT_SECRET'), }); } async validate(payload: { sub: number; email: string }) { return { userId: payload.sub, email: payload.email }; // becomes request.user } }

Protecting routes

@UseGuards(AuthGuard('jwt')) @Get('profile') getProfile(@Request() req) { return req.user; // { userId, email } from the verified token }

If the token is missing, expired, or tampered with, the guard rejects the request with 401 Unauthorized automatically.

Stateless and scalable

Why JWTs scale. Because verification is just a signature check, any server instance can validate a token without a shared session store. That makes JWTs ideal for horizontally-scaled APIs and microservices — no sticky sessions, no central session database.
Keep access tokens short-lived. A stolen JWT is valid until it expires, and there is no easy way to revoke a stateless token mid-life. Short expiry (e.g. 15 minutes) limits the damage — and is exactly why refresh tokens exist, covered next.

Summary

A JWT is a signed token carrying non-sensitive claims (like the user id in sub). Issue it on login with JwtService.sign(), and protect routes with a JWT strategy + AuthGuard('jwt') that verifies the Bearer token statelessly — no session store needed. Never put secrets in the payload, and keep access tokens short-lived. Next: refresh tokens, which solve the short-expiry problem securely.