Roles
Signed in is not the same as allowed: give each member of staff roles, carry them in the token, and let a second global guard decide, route by route, which role a request needs.
What you'll learn
- Separate authentication (who) from authorization (what they may do), and answer 401 for the first and 403 for the second
- Carry roles as a claim in the token and read them from request.user in a guard
- Write a guard that reads required roles from handler and class metadata through Reflector and lets undecorated routes through
- Order global guards so the one that needs a user runs after the one that finds it
Ada registered in lesson 2 and can now sign in. That means she can also add cats to the shelter and delete them, because the only question the API asks is whether the token verifies. Signing in establishes who is asking; it says nothing about what they may do. That second question is authorization, and it is a different check with a different answer: a stranger gets 401 Unauthorized, a known user asking for more than they are allowed gets 403 Forbidden. Keeping the two apart is the first thing this lesson teaches, and the codes tell a client which problem it has.
Role-based access control
The simplest way to say what a user may do is to give them a role, user or admin, and to give each route the roles it requires. A route with no requirement is open to anyone signed in; a route that requires admin lets an admin through and refuses everyone else. This is RBAC, and it fits most APIs until they need finer answers (lesson 4 gets there).
Where do a user's roles come from at request time? They could be looked up from the users table on every request. Or they can travel inside the token as a claim, next to sub and username, signed like everything else, so the guard reads them with no lookup at all. The token way is faster and simpler, with one consequence worth knowing: the roles in a token are the roles at sign-in, and a promotion or a demotion takes effect at the next sign-in, which is one reason tokens expire.
Metadata, decorator, guard
Basics lesson 10 built the pattern: a decorator attaches metadata to a route, a guard reads it back through Reflector. The docs' authorization page shows a @Roles() decorator built exactly that way, and it is in the starter. What the guard does with it is the transfer. As a neighbour, here is the same shape for permissions instead of roles, where a route names an action and a user holds a list of them:
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[] | undefined>(PERMISSIONS_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!required) {
return true; // nothing asked, nothing checked
}
const { user } = context.switchToHttp().getRequest();
return required.every((permission) => user?.permissions?.includes(permission));
}
}
Three things in it carry over. getAllAndOverride reads the handler's metadata first and falls back to the class, so one route can be opened or closed individually and a whole controller can be closed at once. The early return true for an undecorated route is what keeps every ordinary route working; without it, a guard bound globally refuses everything it was not told about. And the guard returns a boolean rather than throwing: when a guard returns false, Nest answers 403 Forbidden with the body { "message": "Forbidden resource", "error": "Forbidden", "statusCode": 403 }, which is the right answer for a known user asking too much.
Where it sits: guard order
The roles guard reads request.user, and request.user is put there by AuthGuard. So it must run after it. Guards bound with APP_GUARD run in the order they are registered, top to bottom in the providers array, and stop at the first that refuses. Registered the other way round, the roles guard reads an undefined user and refuses every protected route before the token was ever checked, or worse, throws a TypeError and answers 500. A public route passes both: the first returns true for @Public(), the second finds no required roles.
Your task
Ada may record microchips; only john, the admin, changes the roster.
- In
auth/auth.service.ts, put the user's roles into the token payload besidesubandusername. Registration already stores[Role.User], and the seeded admin holds[Role.Admin]. - In
auth/roles.guard.ts, read the required roles from the handler, then the class; let a route with none through; otherwise require that the user on the request holds at least one of them. - In
auth/auth.module.ts, bindRolesGuardglobally, afterAuthGuard. - In
cats/cats.controller.ts, require the admin role for creating and deleting a cat; recording and reading a microchip stays open to any signed-in member of staff.
Sign in as ada through the request panel and try DELETE /cats/1; then sign in as john. Look at the two bodies: one names the problem and one does not, on purpose.
When it fails
- John gets 403 on
POST /cats: his token has no roles claim; the payload insignInstill carries onlysubandusername. Decode the token's middle segment in the request panel to see what it holds. - Ada gets 403 recording a microchip: the guard has no early return for a route that requires nothing, so every route became admin-only.
- Every protected route is 403, or a 500
Cannot read properties of undefined (reading 'roles'):RolesGuardis registered beforeAuthGuardand runs before there is a user. - Ada can delete cats:
@Roles(Role.Admin)is missing on the route, orRolesGuardwas never bound. - Nobody can add a cat, not even john, and the answer is 401: the roles guard throws
UnauthorizedExceptioninstead of returningfalse; the wrong code for the wrong question.
Remember
- Authentication is who (401 when unknown); authorization is what they may do (403 when not allowed).
- Roles travel in the token as a claim and are read from
request.user; they are the roles at sign-in. - A roles guard reads handler-then-class metadata, lets undecorated routes through, and returns
falseto get Nest's 403. - Global guards run in registration order; the one that needs a user comes after the one that finds it.
Stuck? Show a hint
auth.service.ts: add roles: user.roles to the payload. roles.guard.ts: this.reflector.getAllAndOverride<Role[] | undefined>(ROLES_KEY, [context.getHandler(), context.getClass()]); return true when undefined; else requiredRoles.some((role) => user?.roles?.includes(role)) with user from request['user']. auth.module.ts: a second { provide: APP_GUARD, useClass: RolesGuard } after the AuthGuard one. cats.controller.ts: @Roles(Role.Admin) on create and remove.