Course outline

NestJS / Intermediate: policies, adapters and sources / Lesson 8

Guards, principals and object authorization

Build trusted principal context and apply shared domain policy across HTTP and non-HTTP entry points.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish authentication, route metadata and object eligibility.
  • Use execution context deliberately instead of assuming every request is HTTP.

A guard is a policy adapter, not magic identity

A guard can examine execution context and decide whether a handler may proceed. It should consume a verified principal or perform the reviewed authentication protocol, not trust a userId header merely because it exists. Route metadata can describe required roles or permissions, but only an enforcing guard gives those labels behavior. Object-specific policy also needs current tenant, ownership and workflow information, often best centralized in a service. A role named editor does not imply access to every tenant’s course. Keep identity provenance and authorization scope explicit at every transport entry point.

Keep transport extraction separate from shared policy

The fragment extracts an HTTP request through ExecutionContext and delegates a coarse prerequisite to an injected policy. It is HTTP-specific and should not be copied unchanged into a message consumer or GraphQL resolver. A request.principal property is an application contract established elsewhere, not a built-in Nest guarantee. Register the guard through a container-aware binding when it has dependencies. The actual service must still enforce object rules before protected effects, especially if the same use case is reachable by a worker that does not pass through this HTTP guard.

typescript Illustrative HTTP guard adapter
@Injectable()
export class CatalogGuard implements CanActivate {
  constructor(private readonly policy: CatalogPolicy) {}
  canActivate(context: ExecutionContext) {
    const req = context.switchToHttp().getRequest();
    return this.policy.mayEnter(req.principal);
  }
}

Decorator imports, principal construction and policy implementation are assumed. The guard was not instantiated or invoked by Nest.

Define override and inheritance semantics

A route may inherit controller metadata or override it according to the guard’s chosen Reflector lookup method. Decide whether permissions combine, replace or form alternatives, then test that decision. A public-route marker requires explicit handling and must not become an accidental bypass for a protected mutation. Class and method metadata are review artifacts, not evidence that every service path is protected. Keep denied-object disclosure consistent across individual lookup, lists and mutation payloads so another graph or transport route cannot reveal or change the same object under a weaker policy.

Test the same object with different principals

The local predicate requires both tenant membership and an editor role for a synthetic course edit. It rejects a cross-tenant editor and a same-tenant viewer, showing why either condition alone is insufficient. This is not credential verification, a comprehensive policy engine or a Nest guard test. Real rules can include enrollment, delegation and revocation-sensitive state. Keeping the domain decision independent allows HTTP, workers and message adapters to share it without assuming that one transport’s guard covers all others. Test the permitted effect itself, not only a denied response code.

javascript Complete offline object-policy table
function mayEdit(principal,course) {
  return principal.tenantId === course.tenantId && principal.roles.includes("editor");
}
const course={id:"c1",tenantId:"lab-a"};
console.assert(mayEdit({tenantId:"lab-a",roles:["editor"]},course));
console.assert(!mayEdit({tenantId:"lab-b",roles:["editor"]},course));
console.assert(!mayEdit({tenantId:"lab-a",roles:["viewer"]},course));

Only the same-tenant editor passes the declared rule. Identity authenticity and guard metadata are not tested.

Review caches and current authority

A cached principal or role can become stale after revocation. Define how long decisions may rely on copied claims and when the authoritative source must be checked. Store request context in request-scoped data, not a singleton currentUser property that concurrent requests overwrite. Test rejected callers with a service spy proving no write, alternate transport paths and identity changes while work is pending. Keep safe error codes and internal correlation separate. This course executes only the predicate and does not validate a token, create a guard, configure Reflector metadata or exercise any live authentication system.

Common errors and fixes

A role grants cross-tenant edits

Why it happens
Role membership replaced object scope.
How to fix it
Combine current identity, tenant and domain rules.
Prevent it next time
Test the same object under several principals.

A custom metadata label enforces nothing

Why it happens
No bound guard interprets it under the intended policy.
How to fix it
Bind and test the enforcing guard and lookup semantics.
Prevent it next time
Review class/method override fixtures.

A worker bypasses HTTP-only policy

Why it happens
Protection existed only in a transport guard.
How to fix it
Enforce shared object policy inside the domain use case.
Prevent it next time
Test every supported entry point.

Check your understanding

1. Why does a same-tenant viewer fail the local edit rule?

Show worked solution

Tenant scope identifies the eligible namespace, while the editor role authorizes this action. Neither condition subsumes the other. Real policies may be richer, but should preserve the distinction and test negative cases for each prerequisite. A successfully authenticated principal does not automatically satisfy either rule.

2. Why is a guard test not enough for a shared publication service?

Show worked solution

Other adapters can invoke the service without that guard, and the guard may not have loaded the current object. Apply domain eligibility before effects, then test HTTP binding and non-HTTP calls separately. This avoids making security depend on callers remembering one particular controller decorator.

Key takeaways

  • Trusted identity, metadata policy and object eligibility are separate layers.
  • Shared use cases need shared authorization, regardless of transport.

Official references