API Security
How the authentication and authorization model is enforced in the NestJS API. This is the implementation reference for securing endpoints; see the architecture doc for the conceptual model.
Everything below lives under api/src/app/auth and api/src/app/identity.
Guards
Three composable guards, applied in order — each depends on the previous one.
| Guard | Enforces | On failure |
|---|---|---|
JwtAuthGuard | Valid bearer JWT; attaches request.user (AuthenticatedUser) | 401 |
TenantContextGuard | X-Tenant-Code header + an active membership in that tenant; attaches request.activeContext (roles + permissions) | 400 (missing header) / 403 (no membership) |
PermissionsGuard | Permissions from @RequirePermissions(...) are all present in the active context (AND semantics) | 403 |
PermissionsGuard is a no-op when a route has no @RequirePermissions metadata,
so it is safe to pair with TenantContextGuard on every tenant-scoped route.
Securing an endpoint
@Controller('students')
@UseGuards(JwtAuthGuard, TenantContextGuard, PermissionsGuard)
export class StudentsController {
@Get()
@RequirePermissions('students.read')
list(@ActiveContext() ctx: MeMembership) {
// ctx.schoolId + ctx.permissions[].scope drive data filtering
}
@Post()
@RequirePermissions('students.manage')
create(@ActiveContext() ctx: MeMembership) { /* ... */ }
}
Decorators:
@CurrentUser()— the authenticated principal (from the JWT).@TenantCode()— rawX-Tenant-Codeheader value (the tenantcode).@ActiveContext()— the resolved membership:schoolId,roles, andpermissions(each with itsscope).
Permission vs scope
Guards enforce permission only (Phase 1). Scope (Phase 2) is the
controller/service's responsibility: read ctx.permissions for the matched
permission's scope (school, assigned-sections, own-children, self,
none) and translate it into a query filter. Guards never see data rows.
Pluggable identity provider
Authentication is abstracted behind an IdentityProvider interface, so Keycloak
is the default but not assumed. Selected via the AUTH_PROVIDER env var:
AUTH_PROVIDER | Provider | Notes |
|---|---|---|
keycloak (default) | KeycloakIdentityProvider | Validates against realm JWKS; maps realm/resource roles |
azure-ad | AzureAdIdentityProvider | Issuer from AZURE_AD_TENANT_ID; maps oid/roles/groups |
Each provider supplies JWKS/issuer/audience validation options and maps raw JWT
claims to the canonical AuthenticatedUser. Swapping providers requires no
controller changes.
Discovery: /me
GET /api/me returns the caller's identity, platform roles/permissions, and all
active memberships. Without X-Tenant-Code it lists memberships (each with its
tenantCode) for the client to choose from; with a valid X-Tenant-Code it also
returns the resolved activeContext. This is how a client discovers which tenant
codes to send on subsequent requests.