Requiring permissions

Routes may now require one of a set of roles to be accessed.

A decorator will be used to define the necessary roles for a route. Let's then create it in auth/decorators/roles.decorator. Notice that it accepts an array of roles.

export const ROLES_KEY = 'roles';

export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Another approach would be to define the decorator like this. It may be interesting in simple cases where the passed metadata should simply be appended to the route.

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

You can read more about this approach in this NestJS doc.

Afterwards, we'll create our first guard from scratch, the RolesGuard. As can be guessed, it will protect the routes according to the necessary roles.

nest g gu auth/guards/roles

Below we can see a basic skeleton, with a reflector and the canActivate() signature.

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext) {}
}

Inside the canActivate() method, the first step is to collect the roles metadata from the route (or controller). Remember that, if there is metadata for both the controller and a route, then the one from the route will be used. If there is no metadata, this guard will be out of the way.

const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
  context.getHandler(),
  context.getClass(),
]);
if (!requiredRoles) return true;

We could also use getAllAndMerge() for merging the metadata of controller and route together, if this behavior was desired.

Then, the user is extracted from the request. The Request type is from express, which has a user interface in it. As we are sure that the user in the request will always have the fields in the RequestUser interface, we can make the type assertion to it without weighing our conscience. Lastly, check if the user is an ADMIN, immediately giving access in positive case.

const request = context.switchToHttp().getRequest<Request>();
const user = request.user as RequestUser;
if (user.role === Role.ADMIN) return true;

And finally, check if the user has one of the required roles.

const hasRequiredRole = requiredRoles.some((role) => user.role === role);
return hasRequiredRole;

Now, the RolesGuard can be activated globally, after the JwtAuthGuard.

{
  provide: APP_GUARD,
  useClass: RolesGuard,
}

We can then state that a route requires one or more roles like the following:

@Roles(Role.ADMIN)

Commit - Global roles guard and decorator for required roles in routes

With this, we can start to protect some routes. For example, we can require the manager role for the following routes:

  • create(), update() and remove() routes in the

    • ProductsController

    • CategoriesController

  • find() routes in the UsersController

And require the admin role for the assignRole() route.

Commit - Requiring roles in some routes

Last updated