PassportSecurity · NestJS

The same sign-in, the way most of the Node world writes it: Passport strategies for the password check and for the bearer token, wrapped by @nestjs/passport into guards, with one Nest 12 trap the docs' recipe walks into.

What you will learn

Read the theory for Passport

All Security lessons

All NestJS courses

loading types…

What you'll learn

  • Describe what a Passport strategy is (a way of finding credentials in a request plus a verify step) and what @nestjs/passport turns it into (a guard)
  • Write a local strategy whose validate() checks a username and password, and a JWT strategy configured to read a bearer token with a secret and an expiry
  • Explain why a guard that extends AuthGuard('local') needs its own constructor under Nest 12, and read the DI error when it lacks one
  • Wire strategies as providers, PassportModule as an import, and a global JWT guard that honours @Public()

Passport

Lessons 1 to 4 built sign-in by hand: a service that checks a password, a guard that reads a bearer token. That is the docs' recommended path, and it is enough for many APIs. But most Node services you will open do it another way: Passport, the authentication library the docs call the most popular in the ecosystem, with hundreds of strategies for passwords, tokens, OAuth providers and more. The @nestjs/passport module folds it into the guard model you already know, so a strategy becomes a guard and a strategy's answer becomes request.user. This lesson rebuilds lesson 3's sign-in with it, so you can read either style, and meets one place where the docs' recipe does not survive Nest 12 as written.

A strategy is two things

Every Passport strategy answers two questions: where are the credentials in this request, and what makes them valid. passport-local looks in the body for username and password; passport-jwt looks wherever you tell it, usually the Authorization header, and verifies the token before you see it. The second question is yours to answer, in a validate() method: its arguments are what the strategy extracted, its return value becomes request.user, and throwing refuses the request. @nestjs/passport gives PassportStrategy(Strategy) as a base class, so a strategy is an injectable like any provider, which is how it gets AuthService:

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
  constructor(private readonly keys: KeysService) {
    super({ header: 'X-Api-Key', prefix: '' });
  }

  async validate(apiKey: string) {
    const owner = await this.keys.ownerOf(apiKey);
    if (!owner) {
      throw new UnauthorizedException();
    }
    return owner; // request.user
  }
}

The super() call carries the strategy's options. For passport-jwt they say where the token is (jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()), whether an expired token is refused (ignoreExpiration: false), and the secretOrKey to verify with; the strategy then calls validate(payload) only for a token that verified, which is why that method just reshapes the payload. Nothing registers a strategy but Nest instantiating it, so each one is listed in a module's providers, beside an import of PassportModule.

A guard per strategy

AuthGuard('local') from @nestjs/passport is a guard that runs the strategy registered under that name, and the docs give each a class of its own so it can be extended:

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

On the login route, @UseGuards(LocalAuthGuard) runs the local strategy on the body; if validate() returned a user, the handler finds it in request.user and signs a token for it. Everywhere else, a guard for the jwt strategy does what lesson 1's hand-written guard did; bound with APP_GUARD and extended to check @Public() through Reflector before calling super.canActivate(), it is the same global guard with Passport underneath. A failure in either strategy, a missing password, a bad token, a validate() that threw, becomes Nest's 401 Unauthorized.

Where the recipe breaks under Nest 12

That two-line guard inherits a constructor from the mixin AuthGuard() returns, and that constructor has one parameter, the module's options, marked @Optional(). Nest reads a class's constructor parameter types by walking up to the parent, but it reads the optional markers from the class itself only. So the subclass inherits a required AuthModuleOptions dependency that nothing provides, and the app refuses to start:

Nest can't resolve dependencies of the LocalAuthGuard (?). Please make sure that the argument AuthModuleOptions at index [0] is available in the AppModule context.

The fix is to give the class its own constructor, constructor() { super(); }, so it declares no dependencies of its own. A guard that injects something anyway, JwtAuthGuard with its Reflector, has one already and is not affected. Lesson 16 of Techniques met the same rule on ConsoleLogger; it is Nest's behaviour, not Passport's.

Your task

Rebuild sign-in on Passport, keeping every rule from lesson 3.

  1. In auth/local.strategy.ts, validate() asks AuthService.validateUser(); no user is a 401.
  2. In auth/jwt.strategy.ts, configure the strategy to read a bearer token from the Authorization header, refuse expired tokens and verify with the secret; validate() returns { userId, username, roles } from the payload.
  3. In auth/local-auth.guard.ts, give the guard what Nest 12 needs. Run before you do: read the message.
  4. In auth/auth.module.ts, provide both strategies; in auth/auth.controller.ts, run the local guard on the login route.

When it fails

  • Nest can't resolve dependencies of the LocalAuthGuard (?): the constructor-less guard; see above.
  • Unknown authentication strategy "local" (logged, and every login 500 or 401): the strategy class is not in providers, so Passport never learned it exists.
  • Login is 401 for the right password: validate() returns nothing for a valid user, or validateUser compares the hash with ===.
  • Every token is 401: secretOrKey is not the secret the token was signed with, or jwtFromRequest looks somewhere else than the bearer header.
  • request.user has sub instead of userId: validate() returned the raw payload; the roles guard still works, the profile does not match.
  • Expired tokens are accepted: ignoreExpiration: true, or unset for a strategy whose default is to ignore.

Remember

  • A strategy extracts credentials and calls your validate(); what it returns is request.user, what it throws is a 401.
  • AuthGuard('name') runs the strategy of that name; strategies are providers, PassportModule is imported.
  • Under Nest 12 a guard extending AuthGuard() declares constructor() { super(); } or inherits a dependency it cannot get.
  • The JWT strategy's options say where the token is, whether expiry counts, and which secret verifies it.
Stuck? Show a hint

local.strategy.ts: const user = await this.authService.validateUser(username, password); if (!user) throw new UnauthorizedException(); return user. jwt.strategy.ts: super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: jwtConstants.secret }); validate returns { userId: payload.sub, username: payload.username, roles: payload.roles }. Both guards: constructor() { super(); } (JwtAuthGuard already has one). auth.module.ts: providers LocalStrategy and JwtStrategy. auth.controller.ts: @UseGuards(LocalAuthGuard) on login.