Passport
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'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()
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.
- In
auth/local.strategy.ts,validate()asksAuthService.validateUser(); no user is a 401. - 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. - In
auth/local-auth.guard.ts, give the guard what Nest 12 needs. Run before you do: read the message. - In
auth/auth.module.ts, provide both strategies; inauth/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 inproviders, so Passport never learned it exists.- Login is 401 for the right password:
validate()returns nothing for a valid user, orvalidateUsercompares the hash with===. - Every token is 401:
secretOrKeyis not the secret the token was signed with, orjwtFromRequestlooks somewhere else than the bearer header. request.userhassubinstead ofuserId: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 isrequest.user, what it throws is a 401. AuthGuard('name')runs the strategy of that name; strategies are providers,PassportModuleis imported.- Under Nest 12 a guard extending
AuthGuard()declaresconstructor() { 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.