Skip to content

NestJS Auth Guards

Auth guards are the most common real-world use of the guard mechanism, verifying that a request carries valid credentials before it reaches a protected route. This lesson builds a simple token-based auth guard.

A Basic Auth Guard

An auth guard typically extracts a token from the Authorization header, verifies it, and attaches the resulting user information onto the request object so later handlers and services can access it.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.replace('Bearer ', '');
    if (!token) return false;

    const user = await this.authService.verifyToken(token);
    request.user = user;
    return true;
  }
}

Attaching request.user here lets a later custom decorator (like @CurrentUser()) or controller logic access the authenticated user without repeating the token verification logic.

Protecting Routes With the Guard

@UseGuards(AuthGuard)
@Get('profile')
getProfile(@Req() req) {
  return req.user;
}
  • Return false (or throw UnauthorizedException) when credentials are missing or invalid.
  • Attach the resolved user to the request so downstream code doesn't re-verify the token.
  • For real projects, prefer Passport strategies (covered later) over hand-rolled token parsing.
  • Keep token verification logic in a service, not inline in the guard, so it's independently testable.

Auth Guard Cheatsheet

Key pieces of a typical authentication guard.

Piece Purpose
Authorization header Carries the bearer token from the client
Token verification service Validates and decodes the token
request.user Where the authenticated user is commonly attached
UnauthorizedException Thrown for missing or invalid credentials

Throwing UnauthorizedException vs Returning False

Returning false from canActivate() results in a generic 403 Forbidden with no message. Throwing UnauthorizedException('Invalid token') instead produces a proper 401 with a descriptive message, generally the better choice for authentication failures specifically.

if (!token) {
  throw new UnauthorizedException('Missing bearer token');
}

Allowing Public Routes to Skip the Guard

A global auth guard needs a way to skip specific public routes (like login or health checks). A custom @Public() decorator combined with Reflector lets the guard check for that metadata and bypass verification when present.

canActivate(context: ExecutionContext): boolean {
  const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
  if (isPublic) return true;
  // ...normal token verification
}

Common Mistakes

  • Returning a generic false instead of throwing a descriptive UnauthorizedException.
  • Hardcoding token verification logic inline instead of delegating to a testable service.
  • Applying a global auth guard without a mechanism to exempt public routes like /login.
  • Not attaching the resolved user to the request, forcing every downstream handler to re-verify.

Key Takeaways

  • Auth guards verify credentials and decide whether a request may proceed.
  • Throwing UnauthorizedException gives clients a clearer 401 response than returning false.
  • Attaching the authenticated user to the request avoids repeated verification downstream.
  • A @Public() metadata decorator lets specific routes opt out of a global auth guard.

Pro Tip

Build the @Public() decorator and metadata check before you ever apply an auth guard globally, retrofitting an escape hatch after every route already requires authentication is much more error-prone than designing it in from the start.