Fullstack CourseLearn by building
Back to week 3

Topic

RBAC & guards

Definition

Role-based access control (RBAC) grants permissions according to a role assigned to an authenticated subject, and a Nest guard is the component that reads that role — or other request context — to decide whether a request may reach a route handler.

In simpler words

RBAC says “admins can do this, members can’t”; a guard is the code that checks that rule before your controller runs.

Two guards run globally on every request in this API: JwtAuthGuard decides who you are, RolesGuard decides what your role allows.

After this you can

  • Read @Roles and predict which role passes
  • Explain why RolesGuard needs JwtAuthGuard to run first
  • Choose 401 vs 403 for a failing check

Two guards, two questions

Definition

A Nest guard implements canActivate and returns whether the current execution context may proceed, commonly using Reflector to read metadata — such as required roles — attached by decorators on the handler or class.

In simpler words

JwtAuthGuard answers “is there a valid session at all?”; RolesGuard answers “does that session’s role satisfy this route’s @Roles list?”

Both guards are registered globally via APP_GUARD in AuthModule, so every route is protected by default — a route opts out with @Public(), and a route narrows further with @Roles(...).

Order matters: RolesGuard reads request.user, so it only works because JwtAuthGuard already ran and attached that user during the same guard phase.

Admin-only delete (tickets.controller.ts)

@Delete(':id')
@Roles('admin')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseUUIDPipe) id: string) {
  await this.ticketsService.remove(id);
}

A member sees 403 here; an admin sees 204. Every other ticket route has no @Roles, so both roles may call it once authenticated.

Picking the right guard failure

Definition

A guard should reject a request with the HTTP status that matches the failed check: unauthenticated requests receive 401 Unauthorized, and authenticated-but-disallowed requests receive 403 Forbidden.

In simpler words

401 means “prove who you are”; 403 means “I know who you are, and the answer is still no.”

JwtAuthGuard.handleRequest throws UnauthorizedException when Passport could not validate a user — no cookie, an expired token, or an unknown subject all land here.

RolesGuard throws ForbiddenException only after it already has request.user and finds the role missing from the required list — never for a request with no session at all.

RolesGuard check

if (!user) throw new UnauthorizedException();
if (!roles.includes(user.role)) {
  throw new ForbiddenException('Insufficient permissions');
}

Guarding against a missing user first keeps the 401/403 distinction correct even if RolesGuard ran without JwtAuthGuard.

UI hide ≠ authorization; never trust body.role

Definition

Authorization is a server-side decision based on the authenticated subject and route metadata; client UI state and client-supplied role fields are not security controls.

In simpler words

Hiding a Delete button is polish. Nest guards are the boundary. A forged JSON body claiming role: "admin" must never grant power.

Roles come from the verified user record / trusted JWT claims after JwtAuthGuard — not from the request body.

Frontend checks improve UX (disable buttons early) but any attacker can call the API directly with curl/Postman.

When adding a dangerous route, ask: which @Roles decorate it, and what Postman proof shows a member gets 403?

Mistake: trust the body

// Wrong
@Post('tickets/:id/assign-admin')
assign(@Body() body: { role: string }) {
  if (body.role === 'admin') return this.promote();
}

// Right
@Post('tickets/:id/dangerous')
@Roles('admin')
dangerous(@CurrentUser() user: User) {
  return this.ticketsService.dangerous(user);
}

Attackers choose the body. Guards read the authenticated user Nest already verified.

Mistake: frontend-only authz

// Wrong — only React hides the button
{user.role === 'admin' && <DeleteButton />}

// Right — hide for UX AND enforce with @Roles('admin') on Nest

Both layers matter; only Nest is authoritative.

Keep in mind

  • Keep the role set small (admin | member here) — a growing role matrix belongs in a permissions table, not more guards.
  • Prefer guard-level checks over scattering role comparisons inside services.
  • 401 for “who are you”, 403 for “not for you” — never swap them for convenience.
  • UI checks are optional; Nest @Roles checks are mandatory for privileged routes.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.

Checking your session…

10 questions · concept 3 · syntax 2 · practical 3 · logic 2

Concept1. Which statement best defines RBAC & guards?
Syntax2. Which code-level choice matches RBAC & guards?
Practical3. A reviewer spots a bug related to RBAC & guards. What is the right fix?
Logic4. Which reasoning correctly explains RBAC & guards?
Concept5. Which boundary does correct use of RBAC & guards preserve?
Practical6. What is the safest next step when applying RBAC & guards?
Syntax7. Which implementation direction matches the rule for RBAC & guards?
Logic8. Which consequence follows from applying RBAC & guards correctly?
Concept9. Which claim about RBAC & guards is true in this Nest + Postgres monorepo?
Practical10. Which team practice best demonstrates RBAC & guards?