Authentication — JWT & Guards
Who Are You? Prove It!
Open interactive version (quiz + challenge)Real-world analogy
Authentication is like a nightclub. The bouncer (Guard) checks your ID (JWT token) at the door. If valid, you're in! If expired or fake, you're out. Some VIP rooms (admin routes) need a special stamp (role) on your wristband!
What is it?
Authentication verifies WHO you are (login). Authorization verifies WHAT you can do (permissions). JWTs are stateless tokens that contain encoded user info, verified by the server using a secret key.
Real-world relevance
Nearly every web app uses JWT for authentication. Google, GitHub, and Stripe all use OAuth + JWT. NestJS makes it clean with decorators like @UseGuards() and @Roles().
Key points
- JWT (JSON Web Token) — A signed token with user info like userId and role. The server verifies it using a secret key without querying the DB each request. Like a concert wristband with three parts: header, payload, signature.
- Passport.js — Passport has 500+ auth strategies: local (email/password), Google OAuth, Facebook, GitHub, and more. NestJS integrates it via @nestjs/passport. Each strategy handles its specific login flow for you.
- Guards — Guards are the conceptual gatekeepers of your API — middleware that decides if a request should proceed. Think of them as security checkpoints: Is this user authenticated? Do they have the right role? The concept applies to any framework, not just NestJS. For NestJS-specific Guard implementation (AuthGuard, RolesGuard, custom decorators), see Lesson 45.
- Role-Based Access — Define roles like admin, manager, user, and guest with different permissions. Admins delete users, regular users edit only their profile. Guards check the JWT payload and reject unauthorized requests.
- Token Refresh — Access tokens expire in 15-30 minutes for security. Refresh tokens last 7-30 days. When access tokens expire, exchange the refresh token for a new one — no re-login needed. Always rotate refresh tokens!
- Password Hashing — NEVER store plain-text passwords! Use bcrypt to hash with a salt before saving. Even if your database is breached, hashes cannot be reversed. Use bcrypt.compare() during login to verify credentials.
- OAuth 2.0 Social Login — Let users log in with Google, GitHub, or Facebook instead of creating passwords. OAuth 2.0 redirects to the provider, they authorize your app, and you get a token with their profile. Improves UX greatly.
- CSRF & Security Headers — CSRF tricks users into making unwanted requests. Protect with CSRF tokens and helmet middleware for security headers. Set HttpOnly and Secure flags on cookies and use SameSite attribute for protection.
- Two-Factor Authentication — Add extra security with a second step after the password. Users scan a QR code with Google Authenticator then enter a 6-digit code each login. Libraries like speakeasy make TOTP-based 2FA straightforward.
- Session vs Token Auth — Sessions store user state on the server (Redis or memory) using cookies. JWT is stateless — the token itself holds all info. Tokens suit APIs and mobile apps; sessions work better for server-rendered web apps.
Code example
// 1. Login — get your JWT token 🎟️
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private jwtService: JwtService,
) {}
@Post('login')
async login(@Body() dto: LoginDto) {
const user = await this.authService.validateUser(
dto.email, dto.password
);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
// Create JWT with user info
const token = this.jwtService.sign({
sub: user.id,
email: user.email,
role: user.role, // 'admin', 'user', etc.
});
return { access_token: token };
}
}
// 2. JWT Guard — the bouncer 🚪
@Injectable()
export class JwtGuard extends AuthGuard('jwt') {
// Passport automatically verifies the token!
}
// 3. Role Guard — VIP check 🌟
@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 { user } = context.switchToHttp().getRequest();
if (!user) return false; // No authenticated user
return requiredRoles.includes(user.role);
}
}
// 4. Using guards on routes — clean & simple! ✨
@Controller('admin')
@UseGuards(JwtGuard, RolesGuard) // Must be logged in + right role
export class AdminController {
@Get('dashboard')
@Roles('admin') // Only admins!
getDashboard() {
return { message: 'Welcome to admin panel!' };
}
@Get('stats')
@Roles('admin', 'manager') // Admins OR managers
getStats() {
return this.statsService.getAll();
}
}Line-by-line walkthrough
- 1. 1. Login — get your JWT token 🎟️
- 2. Decorator that adds metadata or behavior
- 3. Exporting for use in other files
- 4.
- 5.
- 6.
- 7.
- 8.
- 9. Decorator that adds metadata or behavior
- 10.
- 11. Declaring a variable
- 12.
- 13. Closing expression
- 14.
- 15. Conditional check
- 16. Throwing an error
- 17. Closing block
- 18.
- 19. Create JWT with user info
- 20. Declaring a variable
- 21.
- 22.
- 23.
- 24.
- 25.
- 26. Returning a value
- 27. Closing block
- 28. Closing block
- 29.
- 30. 2. JWT Guard — the bouncer 🚪
- 31. Decorator that adds metadata or behavior
- 32. Exporting for use in other files
- 33. Passport automatically verifies the token!
- 34. Closing block
- 35.
- 36. 3. Role Guard — VIP check 🌟
- 37. Decorator that adds metadata or behavior
- 38. Exporting for use in other files
- 39.
- 40.
- 41.
- 42. Declaring a variable
- 43.
- 44. Closing expression
- 45. Declaring a variable
- 46. Returning a value
- 47. Closing block
- 48. Closing block
- 49.
- 50. 4. Using guards on routes — clean & simple! ✨
- 51. Decorator that adds metadata or behavior
- 52. Decorator that adds metadata or behavior
- 53. Exporting for use in other files
- 54. Decorator that adds metadata or behavior
- 55. Decorator that adds metadata or behavior
- 56.
- 57. Returning a value
- 58. Closing block
- 59.
- 60. Decorator that adds metadata or behavior
- 61. Decorator that adds metadata or behavior
- 62.
- 63. Returning a value
- 64. Closing block
- 65. Closing block
Spot the bug
@Post('login')
async login(@Body() dto: LoginDto) {
const user = await this.authService.validate(dto.email, dto.password);
const token = this.jwtService.sign({
sub: user.id,
password: user.password,
});
return { access_token: token };
}Need a hint?
JWT tokens are NOT encrypted - anyone can read them...
Show answer
The password is included in the JWT payload! JWTs are base64-encoded, not encrypted, so anyone can read them. Fix: remove 'password: user.password' from the token payload.
Explain like I'm 5
Authentication is like showing your library card to borrow books. First you sign up and get a card (token). Every time you want a book, you show the card. Some special books are only for teachers (admin roles). The librarian checks your card every time!
Fun fact
JWT tokens are NOT encrypted — they're just Base64 encoded. Anyone can read them! The 'signature' part just proves they weren't tampered with. Never put secrets in a JWT! 🤫
Hands-on challenge
Decode a JWT token at jwt.io — paste any token and see the three parts: Header, Payload, and Signature. Notice how the payload is readable!
More resources
- NestJS Authentication (NestJS Official)
- JWT.io Introduction (JWT.io)
- JWT Authentication Tutorial (Web Dev Simplified)