GuardsBasics · NestJS

Decide who may call a handler with a guard that reads role metadata off the handler through the Reflector, and answer 401 or 403 depending on why.

What you will learn

Read the theory for Guards

All Basics lessons

All NestJS courses

loading types…

What you'll learn

  • Implement CanActivate and choose between returning false and throwing, and what the client gets for each
  • Read handler metadata in a guard with the Reflector and a decorator made by Reflector.createDecorator
  • Bind a guard to a controller with @UseGuards and mark individual handlers with a metadata decorator

Guards

Middleware can check who a request comes from, and lesson 6 said why it cannot do more: it runs before routing, so it does not know which handler the request is for. Deciding whether this user may call this handler needs something that knows both. That is a guard: a class with one method, canActivate(), that returns true to let the request through or false to stop it, and that receives an ExecutionContext describing the handler about to run.

The interface

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    if (!request.user) {
      throw new UnauthorizedException();
    }
    return true;
  }
}

canActivate() may return a boolean, a Promise<boolean> or an Observable<boolean>, so a guard can look something up. context.switchToHttp().getRequest() is the same request object middleware saw, which is how the two cooperate: middleware attaches something to the request, a guard reads it.

There are two ways to refuse. Return false, and Nest throws ForbiddenException for you:

{ "message": "Forbidden resource", "error": "Forbidden", "statusCode": 403 }

Or throw an exception yourself, for a different answer. The guard above throws UnauthorizedException, 401, when there is no user at all, which is the HTTP way of saying "sign in first", as opposed to 403, "you are signed in, and still not allowed". The client can act on the difference.

What the context knows

ExecutionContext extends the ArgumentsHost from lesson 7 with two methods: getClass() returns the controller class, and getHandler() returns the method about to run. Those are the keys to the handler's metadata, extra information attached to a handler by a decorator, which is the mechanism behind every "this route requires X" feature in Nest.

Making a metadata decorator takes one line:

export const Roles = Reflector.createDecorator<string[]>();

Roles is now a decorator that takes a string[], and @Roles(['admin']) on a handler stores that array on it. Reading it back needs the Reflector, a provider from @nestjs/core that any guard can inject:

constructor(private readonly reflector: Reflector) {}

// inside canActivate():
const required = this.reflector.get(Roles, context.getHandler());

reflector.get(Decorator, target) returns what the decorator stored on that target, typed as the decorator declares, or undefined when the target was never decorated. Pass context.getClass() instead to read metadata from the controller, or use getAllAndOverride() to look at both.

Binding a guard

@UseGuards(RolesGuard) on a handler covers that handler; on the controller class it covers every handler in it. app.useGlobalGuards(new RolesGuard()) in main.ts covers everything, but a guard created that way cannot inject anything; a global guard with dependencies is registered under the APP_GUARD token in a module instead. Pass the class, not an instance, wherever you can, and Nest creates the guard with its dependencies, here the Reflector.

Where it sits

Guards run after middleware and before interceptors and pipes. When a guard refuses, nothing after it runs: no interceptor, no pipe, no handler. The exception goes straight to the exceptions layer, so a global filter shapes it like any other.

Your task

CurrentUserMiddleware is given: it reads an x-user header and attaches { name, roles } to the request for two known users, ada (an admin) and bob (a reader). Roles is given too. Write the rest:

  1. Implement RolesGuard.canActivate(). A handler that requires no roles is open to everyone. A handler that requires roles refuses a request with no user at all with 401 Unauthorized, and refuses a user who holds none of the required roles with 403.
  2. Bind the guard to the whole CatsController.
  3. Mark create() and remove() as admin-only. Reading stays open.

The tests call the routes with no user, as bob and as ada. Real authentication replaces the middleware in the security course; the guard will not change.

When it fails

  • Everything answers 200: the guard is not bound, or canActivate() still returns true before checking.
  • Reading is refused too: the guard treats a handler without metadata as protected. reflector.get() returns undefined for those, and undefined means open.
  • Anonymous requests get 403 instead of 401: the guard returns false for a missing user instead of throwing.
  • Nest can't resolve dependencies of the RolesGuard (?): the guard is newed somewhere Nest cannot inject the Reflector. Bind the class, not an instance.

Remember

  • A guard decides whether a request reaches its handler, and it knows the handler through ExecutionContext.
  • Return false for a 403, or throw for any other answer.
  • Metadata is put on a handler by a decorator and read back with reflector.get(Decorator, context.getHandler()).
  • @UseGuards() on a class covers every handler in it.
Stuck? Show a hint

Start with this.reflector.get(Roles, context.getHandler()): undefined means the handler requires nothing, so return true. Otherwise take the user from context.switchToHttp().getRequest(); no user means throw UnauthorizedException, and a user is allowed when one of the required roles is in user.roles. Then @UseGuards(RolesGuard) on the class and @Roles(['admin']) on the two handlers.