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, and a guard that answers a strategy's refusal with the right status.
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
- Override handleRequest() in a guard that extends AuthGuard(), and explain why the default answers every strategy failure with a 401
- 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 then takes over the one decision the guard makes: what a failed strategy answers.
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. By default a failure in either strategy, a missing password, a bad token, a validate() that threw, becomes Nest's 401 Unauthorized.
Extending a guard
When the strategy has finished, the guard decides the answer in one method, handleRequest(err, user, info, context, status). err is whatever validate() threw, user what it returned (or false when the strategy refused before calling it), and info and status are the strategy's own account of a refusal. The default is three lines: throw err if there is one, throw UnauthorizedException if there is no user, return the user. So every failure is a 401, including the ones the strategy knew were something else. passport-local never calls validate() when the body lacks a username or a password: it refuses with { message: 'Missing credentials' } and status 400, and the default turns that into 401 Unauthorized, which tells a client its password was wrong when it never sent one.
Overriding handleRequest() in the guard's own class is the docs' way to change the answer. Here a header API-key guard says what was missing instead of a bare 401:
@Injectable()
export class ApiKeyGuard extends AuthGuard('headerapikey') {
handleRequest<TUser = any>(err: any, user: TUser, info: any): TUser {
if (err) {
throw err;
}
if (!user) {
throw new UnauthorizedException(info?.message ?? 'API key required');
}
return user;
}
}
What it returns becomes request.user; what it throws is the response. canActivate() can be overridden the same way, which is how the global JwtAuthGuard checks @Public() before it calls super.canActivate(context).
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, overridehandleRequest(): an errorvalidate()threw passes through; a request the strategy refused with status400is a400 Bad Requestcarrying the strategy's message; no user is a 401; otherwise the user. Before you do, send a login without a password and read the 401. - In
auth/auth.module.ts, provide both strategies; inauth/auth.controller.ts, run the local guard on the login route.
When it fails
- A login without a password is
401 Unauthorized: the guard keeps the defaulthandleRequest(), or checks!userbeforestatus; a refused request has no user either, so the 400 is never reached. error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.: one path through the override neither throws nor returns the user.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.handleRequest(err, user, info, context, status)decides what a strategy's failure answers; the default makes every failure a 401.- 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 }. local-auth.guard.ts: handleRequest(err, user, info, context, status): if (err) throw err; if (status === 400) throw new BadRequestException(info.message); if (!user) throw new UnauthorizedException(); return user. auth.module.ts: providers LocalStrategy and JwtStrategy. auth.controller.ts: @UseGuards(LocalAuthGuard) on login.