We use cookies to enhance your experience on the site
CodeWorlds

Guards - who gets through the gate

The sentry at the outer post counts everyone coming in and writes down the hour. He does the same for a merchant, a courier and a legionary - because at the outer post it is not yet known where any of them is headed. Only at the treasury door stands someone who knows which chamber the visitor is knocking at, and who can therefore say "not you".

That is the whole difference between middleware and a guard, and in this lesson we shall close it out. We begin by finishing the business of the previous lesson: how middleware attaches itself to the application at all.

Registering middleware - three elements

A middleware class does nothing on its own until you say where it should run. That is the job of a module implementing the

NestModule
interface and filling in its
configure
method:

1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
3@Module({
4  controllers: [TributesController],
5})
6export class AppModule implements NestModule {
7  configure(consumer: MiddlewareConsumer) {
8    consumer.apply(LoggerMiddleware).forRoutes('tributes');
9  }
10}

The written order is always the same:

configure(consumer: MiddlewareConsumer) {
opens the method,
consumer.apply(LoggerMiddleware)
names the middleware class,
.forRoutes('tributes')
names the routes it should run on, and the brace
}
closes the whole. The process has four steps: create the middleware class with
@Injectable()
, implement
NestModule
in the module class, call
consumer.apply()
inside
configure()
, and finally specify the routes with
.forRoutes()
.

Note the argument

'tributes'
. Middleware is given a path pattern, not the name of a controller or a method - and that is the first clue to what it lacks.

What middleware does not know

Middleware executes before the guard and does not know the target route. This is no oversight on the framework authors' part but a consequence of timing: middleware runs at the Express level, before NestJS has settled which controller and which method will handle the request. It receives

req
,
res
and
next
- three HTTP objects and nothing beyond them.

That makes middleware excellent for anything independent of the destination: logging, CORS headers, cookie parsing. It is unfit for decisions of the kind "this endpoint requires an administrator role", because at the moment it runs, the notion of "this endpoint" does not yet exist.

CanActivate - the sentry's interface

A guard is a class implementing the

CanActivate
interface. Not
NestMiddleware
- that belongs to middleware from the previous lesson; not
NestInterceptor
- those are interceptors, which come next; not
PipeTransform
- those are pipes, further on still. Each of the four mechanisms of the request cycle has its own interface and its own single method:

1@Injectable()
2export class AuthGuard implements CanActivate {
3  canActivate(context: ExecutionContext): boolean {
4    const request = context.switchToHttp().getRequest();
5    const token = request.headers.authorization;
6
7    return !!token;
8  }
9}

The body of the method reads in four steps, always in this order:

canActivate(context: ExecutionContext) {
takes the context,
const request = context.switchToHttp().getRequest();
extracts the request object from it,
const token = request.headers.authorization;
reaches for the header, and
return !!token;
turns it into a decision.

That decision is the boolean

true
when access is to be granted - not the string
'allowed'
, not an object with roles, not
null
. The double exclamation mark in
!!token
does exactly that: it turns "the header is there or it is not" into
true
or
false
. When
false
comes back, NestJS rejects the request with
403 Forbidden
, and the controller method does not run at all.

ExecutionContext - the missing piece

The

context
argument is why a guard can do more than middleware.
ExecutionContext
provides information about the target controller and method (handler)
- not database access, not file system access, not server configuration. Those are obtained elsewhere: the database by injecting a repository, the configuration through
ConfigService
.

Two methods will do for a start.

context.switchToHttp().getRequest()
descends to the HTTP layer for the familiar request object - "switch", because NestJS also serves WebSockets and microservices, and the context is common to all of them.
context.getHandler()
returns the controller method itself that is to handle the request, and
context.getClass()
returns the controller class.

And this is precisely what middleware lacks. A guard, holding the handler, can ask: "what does this particular method require?".

Attaching a guard

A guard is attached with the

@UseGuards
decorator - on a single method or on a whole controller:

1@Controller('treasury')
2@UseGuards(AuthGuard)
3export class TreasuryController {
4  @Get()
5  findAll() {
6    return this.treasuryService.findAll();
7  }
8}

The notation has three parts:

@UseGuards(
opens the decorator,
AuthGuard
names the guard class - the class, not an instance of it, because creation is left to dependency injection - and
)
closes it. Several guards may be listed, separated by commas; they are checked in turn, and a single
false
is enough for the request to fall.

When a sentry is to watch the whole application, you register it globally:

app.useGlobalGuards(new AuthGuard())
in
main.ts
, or as a provider with the
APP_GUARD
token. The second road is the better one when the guard needs something itself - a provider goes through dependency injection,
new
does not.

Reflector - a guard that reads requirements

Since a guard knows the handler, it can read the requirements written beside it. Requirements are pinned on with

SetMetadata
and read back with the
Reflector
class:

1export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
2
3@Injectable()
4export class RolesGuard implements CanActivate {
5  constructor(private reflector: Reflector) {}
6
7  canActivate(context: ExecutionContext): boolean {
8    const requiredRoles = this.reflector.get<string[]>(
9      'roles',
10      context.getHandler(),
11    );
12
13    if (!requiredRoles) {
14      return true;
15    }
16
17    const request = context.switchToHttp().getRequest();
18
19    return requiredRoles.includes(request.user?.role);
20  }
21}

SetMetadata('roles', roles)
sticks a label on the method under the key
'roles'
.
this.reflector.get<string[]>('roles', context.getHandler())
reads it back - the first argument is the key, the second is where to look for it, namely our handler. No label means an endpoint with no requirements, so we return
true
and let it through.

One guard serves the whole application this way:

@Roles('senator')
above one method,
@Roles('centurion', 'tribune')
above another, and the comparison logic written once. It is the same
SetMetadata
we shall return to with custom decorators - there you will see how to roll
@Roles
and
@UseGuards
into a single seal.

Summary

The outer post counts those coming in, the treasury sentry decides, @name:

  • middleware is registered in a module implementing
    NestModule
    :
    configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware)
    .forRoutes('tributes')
    }
    ,
  • middleware executes before the guard and does not know the target route - it runs before NestJS has settled which handler will serve the request,
  • a guard implements the
    CanActivate
    interface
    - not
    NestMiddleware
    , not
    NestInterceptor
    , not
    PipeTransform
    ,
  • canActivate()
    returns
    true
    to allow access - not the string
    'allowed'
    , not an object with roles, not
    null
    ;
    false
    gives a
    403
    and the handler does not run,
  • the order within the method:
    canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;
    return !!token;
    ,
  • ExecutionContext
    provides information about the target controller and method (handler)
    - not the database, not the file system, not server configuration,
  • context.getHandler()
    returns the method,
    context.getClass()
    the controller,
    switchToHttp().getRequest()
    the request object,
  • attaching:
    @UseGuards(
    AuthGuard
    )
    , globally through
    useGlobalGuards
    or the
    APP_GUARD
    token,
  • SetMetadata('roles', roles)
    stores requirements beside the handler, and
    Reflector
    reads them:
    this.reflector.get<string[]>('roles', context.getHandler())
    .

In the next lesson you will meet interceptors - the third mechanism of the cycle, and the first to touch the response rather than only the request. For now remember the difference: middleware asks "what has arrived", a guard asks "where is it going, and is that allowed".

Go to CodeWorlds