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);

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

Underneath 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;

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,
}

It's important to follow this order! Otherwise, the guards' behavior will be compromised.

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

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

  • find() routes in the UsersController

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

Commit - Requiring roles in some routes

Last updated