Broken Access Control in Nestjs with Bearer Tokens
Broken Access Control in Nestjs with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Broken Access Control (BAC) in a NestJS application using Bearer tokens occurs when authorization checks are missing, incomplete, or bypassed, allowing a user to access or modify resources that should be restricted. Even when Bearer tokens are used for authentication, the API must still enforce role-based or attribute-based authorization at every endpoint; relying only on token validation is insufficient.
In NestJS, routes and controllers often rely on Guards (e.g., AuthGuard and RolesGuard) and interceptors to enforce permissions. If these guards are not applied consistently, or if role checks are implemented incorrectly (for example, checking roles only at the controller level but not per action), an authenticated user with a valid Bearer token can perform unauthorized actions such as reading other users’ data, escalating privileges, or invoking admin-only operations.
Common implementation gaps that lead to BAC with Bearer tokens include:
- Missing or misconfigured @ApplyGuards() or Reflector-based authorization checks that result in some endpoints being unguarded.
- Overly broad roles assigned to tokens (e.g., a token carrying both user and admin roles without proper scoping).
- Insecure direct object references (IDOR) where an endpoint uses an object ID from the request (e.g., userId from params) without verifying that the requesting token’s subject owns or is allowed to access that object.
- Failure to validate token scope or context on the backend, allowing a token issued for one purpose to be reused across endpoints with different authorization requirements.
These issues are especially relevant when endpoints do not re-validate ownership or permissions on the server. For example, an endpoint like GET /users/:userId/profile that only checks that a Bearer token is valid, but does not ensure that the authenticated user’s ID matches the :userId parameter, enables IDOR/BOLA through a valid token. Because the authentication layer (Bearer token validation) is separated from the authorization layer (role and ownership checks), developers must explicitly wire both together; omitting this wiring is what creates the broken access control condition.
Real-world attack patterns mirror the OWASP API Top 10 Broken Access Control category and can be tested by scanners that perform BOLA/IDOR checks alongside authentication and authorization checks. A scanner can send the same authenticated request with different resource IDs or with a token belonging to a lower-privilege role to observe whether access is improperly granted.
Bearer Tokens-Specific Remediation in Nestjs — concrete code fixes
To remediate Broken Access Control when using Bearer tokens in NestJS, combine robust authentication with explicit, per-action authorization, and ensure ownership checks for object-level permissions. Below are concrete code examples that demonstrate a secure approach.
1) Enforce authentication and role-based authorization with Guards
Use AuthGuard to validate the Bearer token and a custom RolesGuard to enforce role-based access. Apply guards globally or per-controller/endpoint, and avoid unguarded routes.
// auth/token-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class TokenAuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise | Observable {
const roles = this.reflector.get('roles', context.getHandler());
const request = context.switchToHttp().getRequest();
const token = this.extractBearerToken(request);
if (!token) {
return false;
}
// Validate token and attach user to request (pseudo)
const user = validateBearerToken(token);
if (!user) {
return false;
}
request.user = user;
if (!roles) return true;
return roles.some((role) => user.roles.includes(role));
}
private extractBearerToken(request: any): string | null {
const authHeader = request.headers.authorization;
if (typeof authHeader !== 'string') return null;
const [type, token] = authHeader.split(' ');
return type.toLowerCase() === 'bearer' ? token : null;
}
// placeholder: implement secure token validation (JWKS, introspection, etc.)
private validateBearerToken(token: string): any {
// e.g., call auth server or verify JWT signature and claims
return { id: 'user-123', roles: ['user'] };
}
}
// auth/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const roles = context.getArgByIndex(2) || [];
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.roles) return false;
return roles.some((role) => user.roles.includes(role));
}
}
// app.controller.ts
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { TokenAuthGuard } from './auth/token-auth.guard';
import { RolesGuard } from './auth/roles.guard';
@Controller('projects')
@UseGuards(TokenAuthGuard, RolesGuard)
export class ProjectsController {
@Get()
findAll(@Request() req) {
// Only users with 'viewer' or 'admin' can reach this if roles guard enforces it
return req.user.projects;
}
@Get(':id')
@UseGuards(TokenAuthGuard) // apply auth, ownership check inside handler or via interceptor
findOne(@Request() req, @Param('id') id: string) {
// Enforce ownership: ensure req.user can access this project
const project = getProjectById(id);
if (!project || !userCanAccess(req.user, project)) {
throw new ForbiddenException('Access denied');
}
return project;
}
}
2) Apply per-action authorization and object-level ownership checks
Do not rely on controller-level guards alone. Re-validate ownership inside handlers or use an interceptor/context to ensure the requesting user owns the resource. For endpoints that act on a specific resource (e.g., /projects/:projectId), confirm that the resource’s owner matches the token’s subject.
// auth/ownership.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const OWNER_RESOURCE = 'ownerResource';
export const RequireOwner = () => SetMetadata(OWNER_RESOURCE, true);
// auth/ownership.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { OWNER_RESOURCE } from './ownership.decorator';
@Injectable()
export class OwnershipInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
const isOwnerRequired = context.getHandler().getMetadata().some((m: any) => m.key === OWNER_RESOURCE);
if (!isOwnerRequired) {
return next.handle();
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const resourceId = context.switchToHttp().getRequest().params.id;
return new Observable((subscriber) => {
const resource = getResourceById(resourceId);
if (resource && userCanAccessResource(user, resource)) {
subscriber.next(resource);
subscriber.complete();
} else {
subscriber.error(new ForbiddenException('Access denied to resource'));
}
});
}
}
// projects.controller.ts
import { Body, Controller, Get, Param, Post, UseInterceptors, UseGuards } from '@nestjs/common';
import { TokenAuthGuard } from './auth/token-auth.guard';
import { RolesGuard } from './auth/roles.guard';
import { RequireOwner, OwnershipInterceptor } from './auth/ownership.interceptor';
@Controller('projects')
@UseGuards(TokenAuthGuard, RolesGuard)
export class ProjectsController {
@Get(':id')
@UseInterceptors(OwnershipInterceptor)
getProject(@Param('id') id: string) {
// Will only proceed if interceptor confirms ownership
return fetchProjectWithOwnershipCheck(id);
}
}
3) Validate and scope Bearer tokens
Ensure tokens carry minimal necessary scopes/roles and validate them on each request. Avoid issuing long-lived tokens with broad permissions. Use token introspection or validate signed JWTs with a JWKS, and enforce audience (aud) and issuer (iss) claims to prevent token misuse across services.
4) Enforce rate limiting and anomaly detection
Apply rate limiting to authentication and sensitive endpoints to mitigate brute-force or token-replay attempts. Monitor for unusual patterns (e.g., many requests with the same token across different resource IDs), which may indicate IDOR probing.
By combining authenticated Bearer token validation with explicit role checks and per-resource ownership verification, you reduce the risk of Broken Access Control in NestJS APIs. These practices align with the principles behind checks performed by scanners that test authentication, BOLA/IDOR, and privilege escalation.