AuthenticationSecurity · NestJS

Sign the shelter's staff in: exchange a username and password for a signed JWT, verify it on every later request in a guard bound to the whole app, and mark the routes anyone may use with @Public().

What you will learn

Read the theory for Authentication

All Security lessons

All NestJS courses

loading types…

What you'll learn

  • Explain what a JWT is (a signed, readable, expiring claim about who is asking) and why the server can trust one it did not store
  • Register JwtModule for the whole app and sign a payload with JwtService.signAsync
  • Write a guard that reads a bearer token, verifies it, puts the payload on the request and answers 401 for anything else, and bind it with APP_GUARD
  • Open individual routes with a @Public() decorator the guard reads through Reflector

Authentication

So far anyone who can reach the cats API can add a cat to the shelter, or delete one. The service has no idea who is asking. Authentication is the answer to that question: a visitor proves who they are once, with something only they know, and the API attaches an identity to every request they make afterwards. The mechanism this lesson builds is the one most APIs use: a username and password exchanged for a token, and the token presented on every later request in a header.

What a token is

The API could keep a table of sessions and hand each visitor a random id. A JSON Web Token avoids the table. It is three base64url segments joined by dots: a header naming the algorithm, a payload of claims ({ "sub": 7, "username": "ada", "iat": 1788765411, "exp": 1788765471 }), and a signature computed from the first two with a secret only the server knows. Whoever holds the token can read the payload; base64url is not encryption. What they cannot do is change it, because the signature would no longer match, and they cannot make one up, because they lack the secret. So the server trusts the payload without storing anything: verifying the signature is the whole check. sub is the subject (the user's id), iat when it was issued, exp when it stops being valid, all added by the signer.

Signing and verifying with @nestjs/jwt

JwtModule.register() configures one signer for the app. global: true makes JwtService injectable from any module without importing JwtModule again; secret is the signing key; signOptions.expiresIn gives every token a lifetime, '60s', '1h', '7d':

JwtModule.register({
  global: true,
  secret: 'a long random string from configuration',
  signOptions: { expiresIn: '1h' },
})

JwtService then signs and verifies. Both have promise-returning forms, which is what a handler awaits:

const token = await this.jwtService.signAsync({ sub: 7, username: 'ada' });
const payload = await this.jwtService.verifyAsync(token);

verifyAsync returns the payload when the signature matches and the token has not expired. Otherwise it throws, and the error says why: JsonWebTokenError: invalid signature for a token the secret never produced, TokenExpiredError: jwt expired once exp is in the past, JsonWebTokenError: jwt malformed for something that is not three segments. An API does not repeat those reasons to the client; it answers 401 Unauthorized and keeps the detail for its logs.

Where the token travels

The client sends it in the Authorization header, with a scheme that says what kind of credential follows: Authorization: Bearer eyJhbGci.... The scheme matters. The same header can carry Basic dXNlcjpwYXNz (a base64 username and password), and a guard that accepts any scheme would treat a Basic credential as a token. Reading the header is a split on the space:

const [scheme, credential] = request.headers.authorization?.split(' ') ?? [];

and only scheme === 'Bearer' yields a token.

The guard, and where it sits

Basics lesson 8 built guards that answer one question, may this request proceed, and throw or return false when not. Authentication is that question. A guard that verifies the token runs after middleware and before pipes and the handler, so a handler never sees an unauthenticated request. The verified payload goes on the request object, request['user'] = payload, where a handler reads it with @Request() or, better, a custom decorator (Basics lesson 10).

Bound with @UseGuards() on each controller, such a guard is easy to forget on the next one. APP_GUARD binds it once for every route in the app, from any module's providers:

providers: [{ provide: APP_GUARD, useClass: AuthGuard }]

Then every route requires a token, including the one that issues tokens. Nobody could ever sign in. The docs' answer is metadata: a @Public() decorator is SetMetadata('isPublic', true), and the guard reads it with Reflector:

const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
  context.getHandler(),
  context.getClass(),
]);

getAllAndOverride looks at the handler first, then its class, so a route can be opened individually or a whole controller at once.

Your task

The shelter's staff sign in; visitors browse.

  1. In auth/auth.module.ts, register the JWT module for the whole app with the secret from constants.ts and tokens that expire after 60 seconds, and bind AuthGuard to every route.
  2. In auth/auth.service.ts, once the password checks out, answer with a token whose payload carries the user's id as sub and the username.
  3. In auth/auth.guard.ts, let a @Public() route through; otherwise require Authorization: Bearer <token>, verify it, put the payload on request.user, and answer 401 for a missing header, another scheme, or a token that does not verify.
  4. Mark the login route public, and in cats/cats.controller.ts keep listing and reading a cat public while creating and deleting need a token.

Do step 2 before step 1 and press Run: Nest refuses to start, because AuthService asks for a JwtService that no module provides yet. Read the message, then register the module.

When it fails

  • Nest can't resolve dependencies of the AuthService (UsersService, ?): JwtService is not available. JwtModule.register() is missing from the imports, or it lacks global: true and the module that needs it does not import it.
  • Every route is 401, including POST /auth/login: the guard is global and nothing is marked public, or the guard never reads the metadata; the sign-in route is the first to open.
  • GET /auth/profile is 200 with Basic in the header: the guard takes whatever follows the first space. Check the scheme.
  • A forged token gets a 500 instead of a 401: verifyAsync threw and nothing caught it. Wrap it and turn any failure into UnauthorizedException.
  • request.user is undefined in the handler: the guard verified the token but never stored the payload, or it stored it under another name.

Remember

  • A JWT is readable, signed and expiring; the signature is what the server checks, so it stores nothing.
  • JwtModule.register({ global, secret, signOptions }), then signAsync to issue and verifyAsync to check; both throw rather than return on failure.
  • The client sends Authorization: Bearer <token>; the scheme is part of the check.
  • APP_GUARD guards every route; @Public() plus Reflector opens the ones that must stay open.
Stuck? Show a hint

auth.module.ts: imports JwtModule.register({ global: true, secret: jwtConstants.secret, signOptions: { expiresIn: '60s' } }) and a provider { provide: APP_GUARD, useClass: AuthGuard }. auth.service.ts: this.jwtService.signAsync({ sub: user.userId, username: user.username }). auth.guard.ts: reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]); request.headers.authorization?.split(' ') gives [scheme, token]; jwtService.verifyAsync(token) inside try/catch, and request['user'] = payload. Controllers: @Public() on login and on the two GET cats routes.