In a NestJS API, authorisation and input validation live in guards and pipes, and a route that misses either one is open by default unless the application is built to deny. NestJS is a popular framework for enterprise TypeScript back ends, and its decorator model makes security easy to declare but also easy to forget on one controller, one GraphQL field resolver or one WebSocket gateway. This guide explains how the request pipeline enforces identity and access, how to configure ValidationPipe, serialisation and security headers, how to handle API keys and service tokens, and what to log. It is written for developers, application security engineers and reviewers of NestJS services, and draws on the official NestJS documentation as checked on 27 October 2026.
Key takeaways
- Make authentication and authorisation global and deny by default. Register guards with
APP_GUARDand mark open routes explicitly, rather than adding@UseGuards()controller by controller. - Guards run after middleware and before pipes and interceptors. Put identity checks in guards (they see the
ExecutionContextand handler metadata), not in middleware that cannot see which handler will run. - Turn on
whitelistandforbidNonWhitelisted. ValidationPipe without them passes unexpected properties straight into services and ORMs, which is how mass assignment happens. - Check object ownership in the service, record by record. A role guard on the route answers "may this user call refund"; only the service can answer "may this user refund this order".
- Cover every transport. Global guards behave differently for hybrid microservices, GraphQL field resolvers and WebSocket connection handshakes; each needs its own check.
How identity and access work in NestJS
A NestJS request passes through middleware, then guards, then interceptors, then pipes, then the handler, then exception filters on the way out. The guards documentation states that guards execute after all middleware but before any interceptor or pipe. That order matters: a guard can read route metadata such as roles or a public flag through the Reflector, whereas middleware is context-blind and does not know which handler will execute after it calls next().
| Component | Security job | Scope options | Typical mistake |
|---|---|---|---|
| Middleware | Security headers, CSRF origin checks, request logging | Module or app level | Doing authorisation here, where handler metadata is not visible |
Guard (CanActivate) | Authentication and route-level authorisation, rate limiting | Method, controller, global (useGlobalGuards or APP_GUARD) | Applied per controller and forgotten on a new one |
| Pipe | Validation and transformation of body, query and params | Parameter, method, controller, global | ValidationPipe without whitelist; nested DTOs not validated |
| Interceptor | Response serialisation, audit logging, timeouts | Method, controller, global | Returning ORM entities with password hashes or internal fields |
| Exception filter | Safe error responses | Method, controller, global | Custom filters that echo stack traces or database errors |
Global registration and its limits
- APP_GUARD over useGlobalGuards: a guard registered with
app.useGlobalGuards()is created outside any module and cannot inject dependencies. Registering it as a provider with theAPP_GUARDtoken allows injection. The documentation notes thatAPP_GUARDcan be registered several times and every registered guard runs for each request, in registration order, so put authentication before authorisation. - Hybrid applications: in a hybrid application,
useGlobalGuards()does not set up guards for connected microservices by default. Microservice guards should throwRpcExceptionrather thanHttpException. - GraphQL: Nest runs global guards and interceptors on root resolvers only. Field resolvers that expose sensitive data need
fieldResolverEnhancersconfigured, or their own checks. - WebSockets: Nest runs no guards on a gateway's
handleConnection(), so the handshake must be authenticated separately; message handlers can then use guards.
Authentication options
Two patterns are documented. The long-standing one uses @nestjs/passport with a strategy package such as passport-jwt, plus @nestjs/jwt; the Passport recipe sets ignoreExpiration: false so expired tokens are rejected with 401, and warns that the signing key must be protected in a vault, environment variable or configuration service. The current documentation also describes a first-party @nestjs/authentication package that registers a global guard so every route requires a signed-in user unless marked @Public(), hashes passwords with scrypt from node:crypto at OWASP's recommended parameters (N=2^17, r=8, p=1), issues a __Host-sid session cookie that is HttpOnly, Secure and SameSite=Lax, and stores only hashes of session and refresh tokens. Check which pattern and version your codebase uses before applying either set of defaults.
- JWT verification: verify signature,
exp,issandaud. The authentication package requires a signing secret of at least 32 bytes for HS256, recommends an ES256 key when other services verify the tokens, and requires both issuer and audience when accepting tokens from an external identity provider, because an IdP signs every client's tokens with the same keys. - Refresh tokens: opaque, stored as hashes, single use, with reuse of a spent token revoking the whole token family.
- Roles in tokens: keep facts that change, such as roles, out of long-lived token claims and load the current user on each request.
For the token design questions behind these choices, see the OWASP API Top 10 API2 Broken Authentication Guide.
Admin roles and privileged access
The classic NestJS pattern is a RolesGuard that reads a @Roles() decorator with Reflector and compares it with request.user.roles. It works, but it fails open in one important way: a handler with no @Roles() metadata is usually allowed. The @nestjs/authorization package documented alongside the authentication package reverses this. Its AuthorizationGuard denies by default: a handler runs only when it declares a check with @Can(), @Can.Anyone() or @Public(), and the module logs at start-up every handler it will deny, which gives a ready-made migration list.
- Function level: admin routes (refunds, user management, feature flags) need an explicit permission check on the route. OWASP ASVS 5.0 V8.2.1 expects function-level access to be restricted to consumers with explicit permissions. See the OWASP API Top 10 API5 Broken Function Level Authorization Guide.
- Object level: checks that depend on the record (this order belongs to this customer, this refund is under this staff member's limit) belong in the service after the record is loaded, and list queries should filter by owner in the query itself. ASVS V8.2.2 covers data-specific access. See the OWASP API Top 10 API1 Broken Object Level Authorization Guide.
- Step-up for sensitive changes: require a verified second factor for actions such as changing an account's email address, and rotate the session on privilege changes.
- Separate admin surfaces: consider serving admin controllers from a separate application or route prefix with its own network controls, so a guard mistake is not the only barrier.
Validation, mass assignment and serialisation
| Setting | Effect | Recommended |
|---|---|---|
whitelist: true | Strips properties without validation decorators | On, globally |
forbidNonWhitelisted: true | Rejects the request instead of stripping | On for APIs, so clients learn the contract and attacks are visible in logs |
forbidUnknownValues: true | Fails validation of unknown objects immediately | On |
transform: true | Converts payloads to DTO instances and primitives to declared types | On, with explicit DTO types; or use ParseIntPipe and similar per parameter |
disableErrorMessages: true | Hides validation detail from clients | Consider in production for public APIs; keep detail in logs |
ClassSerializerInterceptor with @Exclude() | Removes marked fields from responses | On for entities; the handler must return a class instance, not a plain object |
- Mass assignment: never pass a request DTO straight into an ORM
save()orupdate(); map allowed fields explicitly. ASVS V15.3.3 asks for allowed fields per controller and action. See the OWASP API Top 10 API3 Broken Object Property Level Authorization Guide. - Nested objects: class-validator does not validate nested DTOs unless they are marked with
@ValidateNested()and a@Type()decorator, so nested payloads are a common gap. - Schema validation: Zod and other Standard Schema libraries are supported; a Zod
z.object()strips unknown properties in the same way aswhitelist. - Response shaping: return DTOs designed for output, or use
StandardSchemaSerializerInterceptor, so a new database column is not exposed by accident. - Errors: the built-in global exception filter returns a generic 500 "Internal server error" for unrecognised exceptions, and an error passed as
causetoHttpExceptionis not serialised. Custom filters should keep that behaviour.
Integrations, API tokens and service accounts
NestJS services are often called by other services, partner systems and scheduled jobs. These callers are non-human identities and need the same rigour as users.
- API keys: the authentication package's documented design is a useful benchmark: a key of the form prefix, public id and 256-bit secret; only a SHA-256 hash stored; constant-time comparison with the same work for unknown ids; shown once at creation; expiry after a year; keys that cannot create or revoke other keys; and a key never counting as a second factor. A recognisable prefix lets secret scanners find leaked keys. See the API Key Management Guide.
- Service-to-service calls: prefer short-lived tokens from an identity provider (OAuth client credentials or workload identity) verified against a JWKS URL over shared static secrets. Pin
issandaud. - Microservice transports: message-based handlers (Kafka, RabbitMQ, NATS, gRPC) are not protected by HTTP guards in hybrid apps unless configured; authenticate the transport (mTLS, broker ACLs) and validate message payloads with pipes.
- Secrets in configuration:
@nestjs/configloads environment variables through dotenv-style parsing, and itsvalidationSchemaapplies only to environment variables, not to custom configuration files. Keep JWT secrets, database credentials and encryption keys in a secrets manager and validate required values at start-up so a missing secret fails closed. - Outbound integrations: HTTP clients that call partner APIs carry credentials too; scope them, rotate them and keep them out of logs. See the Secrets Management Guide.
Logging and detection
Log at the guard and service layer, where the identity and the decision are both known. The authentication package publishes sign-ins, sign-outs, second-factor changes, reused refresh tokens, password resets and email verifications on an AuthenticationEvents stream and on node:diagnostics_channel, and the authorisation package emits events for its decisions; send them to the SIEM.
| Event | Why it matters | Alert idea |
|---|---|---|
| 401 and 403 responses by principal and route | Enumeration and broken access probing | Spike of 403s from one principal across many object IDs |
| Refresh token reuse | Token theft signal | Any occurrence |
| Repeated second-factor failures | Attacker holds the password | Lockouts per account |
| API key use from new IP ranges | Leaked integration credential | First use from a new network or country |
Validation rejections (forbidNonWhitelisted) | Mass assignment attempts | Unexpected fields such as role or isAdmin |
| Throttler limits hit | Brute force or scraping | Sustained limits on sign-in routes |
Never log tokens, API keys, password reset links or full request bodies. The authentication documentation warns that its development mail transport writes every link to the log, where anyone who reads the logs could sign in as the customer.
Metrics that show the controls are working:
- Percentage of routes covered by a global deny-by-default authorisation guard (start-up deny list trending to zero unexplained entries).
- Number of routes marked public, reviewed each release.
- Validation rejection rate and the share of DTOs with explicit output shapes.
- API keys past expiry or unused for 90 days still active.
- Mean time to revoke a leaked integration credential.
Hardening baseline
- Global guards first: register authentication, then authorisation, then
ThrottlerGuardwithAPP_GUARD. If you adopt@nestjs/authorization, importAuthenticationModulebeforeAuthorizationModule, as the documentation requires. - Global ValidationPipe:
whitelist,forbidNonWhitelisted,forbidUnknownValuesandtransformon. - Security headers: from Nest v12.1,
app.useSecurityHeaders()applies Helmet 8 defaults on Express and Fastify, removesX-Powered-Byand must be called beforeapp.init()orapp.listen(); older versions use the Helmet middleware. - CSRF for cookie sessions:
app.enableCsrfProtection()rejects cross-origin writes usingSec-Fetch-SiteandOrigin; configuretrustedOriginsand call it right afterNestFactory.create(). - CORS: call
enableCors()with an explicit origin list and an explicitmethodslist, because the Express and Fastify packages have different defaults. - Rate limiting:
@nestjs/throttlerwith shared storage such as Redis when running more than one instance, and correcttrust proxysettings. The documentation warns that on recent Fastify versions a numerictrustProxyhop count trusts no proxy, so every client behind the proxy is counted as one. - Serialisation:
ClassSerializerInterceptoror schema serialisers globally; output DTOs for sensitive entities. - Dependencies and runtime: lock files, dependency review and current Node.js. See the Node.js Security Guide and the TypeScript and JavaScript Secure Coding Guide.
Rolling it out: on an existing service, add the global authorisation guard in a log-only mode first (or read the start-up deny list), mark each route explicitly, then switch to enforce in a staging ring. Enable whitelist before forbidNonWhitelisted, watch stripped-field logs for a release, then reject. Roll back by removing the provider registration, which is a one-line change, rather than editing controllers.
Choosing an approach: small teams starting fresh gain most from the first-party authentication and authorisation packages because the safe defaults are built in. Large estates with Passport strategies, external IdPs and policy engines may keep Passport for authentication and add a deny-by-default guard that calls a central policy decision point. See the OpenID AuthZEN Authorization API Guide.
Framework mapping
| Control | OWASP ASVS 5.0 | OWASP API Top 10 2023 | NIST SP 800-53 Rev. 5 |
|---|---|---|---|
| Global guards, deny by default | V8.2.1 | API5 Broken Function Level Authorization | AC-3, AC-6 |
| Ownership checks in services | V8.2.2 | API1 Broken Object Level Authorization | AC-3 |
| ValidationPipe whitelist, output DTOs | V2.2.1, V15.3.3 | API3 Broken Object Property Level Authorization | SI-10 |
| Token verification, session cookies | V7.2.1 | API2 Broken Authentication | SC-23 |
| Throttler | n/a | API4 Unrestricted Resource Consumption | SC-5 |
| Authentication and authorisation events | n/a | n/a | AU-2, AU-12 |
In NIST CSF 2.0 terms these sit under PR.AA-05 (access permissions enforced with least privilege) and PR.PS-04 (log records generated for monitoring). Attackers exploiting a missing guard on an internet-facing API map to MITRE ATT&CK T1190 Exploit Public-Facing Application.
Common misconfigurations
- Per-controller guards:
@UseGuards(AuthGuard)on most controllers and missing on the one added last sprint. - RolesGuard that allows when no roles are set: the fail-open default of the common sample pattern.
- Trusting the route guard for object access:
GET /orders/:idchecks the customer role but never that the order belongs to the caller. - ValidationPipe without whitelist: a client sends
{"role":"admin"}and the service spreads the DTO into an update. - Unprotected side doors: GraphQL field resolvers, WebSocket handshakes and microservice handlers left outside the HTTP guard chain.
- Static JWT secrets in
.envcommitted to the repository, or tokens accepted from an external IdP without an audience check. - In-memory throttler on a multi-instance deployment, so each instance counts separately and a restart resets the count.
Practitioner checklist
- Register authentication and authorisation guards globally with
APP_GUARD, in that order, and deny routes that declare no check. - Review every
@Public()route each release. - Enforce object ownership in services and filter list queries by owner.
- Enable ValidationPipe
whitelist,forbidNonWhitelistedandforbidUnknownValuesglobally, and validate nested DTOs. - Return output DTOs or excluded fields; never return raw ORM entities.
- Verify JWT signature, expiry, issuer and audience; keep signing keys in a secrets manager.
- Issue hashed, prefixed, expiring API keys to integrations and scan for their prefix.
- Secure GraphQL field resolvers, WebSocket handshakes and microservice handlers separately.
- Enable security headers, CSRF origin checks, explicit CORS and a shared-store throttler.
- Stream authentication and authorisation events to the SIEM without logging secrets.
Standards and references
- NestJS: Guards
- NestJS: Authentication
- NestJS: Authorization
- NestJS: Pipes
- NestJS: Rate limiting
- NestJS: CSRF protection
- OWASP: API Security Top 10 2023
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: API Security Guide · Node.js Security Guide · Express.js Security Guide · API Key Management Guide