Skip to content

Authentication and Authorization are two different layers

Authentication (who you are) and authorization (what you may do) stay in separate guards and services.

nestjs-auth-separate-authn-authz

Why it matters

Failure modes if this rule is ignored
StakeIf ignored
Hard to change
  • When AuthGuard both authenticates and decides permissions, you can't swap auth method (JWT → session, OAuth → API key) without touching every permission check.
Layer mixing
  • Permission checks belong inside the domain that knows the resource. Auth checks are cross-cutting infrastructure.

How to fix

Authentication = who you are. Authorization = what you can do. They don't live in the same guard, and they don't live in the same service. Mixing them is the source of the most painful security bugs.

Examples

Bad
ts
@Injectable()
export class AuthGuard implements CanActivate {
  async canActivate(ctx: ExecutionContext) {
    const req = ctx.switchToHttp().getRequest();
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) return false;
    const user = await this.jwt.verify(token);
    req.user = user;

    // ← starts knowing about domains
    const path = req.url;
    if (path.startsWith('/sites/') && !user.canManageSites) return false;
    if (path.startsWith('/studio/') && !user.isPro) return false;
    return true;
  }
}
Good
ts
// libs/shared/backend-auth/src/lib/jwt-auth.guard.ts — infrastructure
@Injectable()
export class JwtAuthGuard implements CanActivate {
  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const req = ctx.switchToHttp().getRequest();
    const token = extractToken(req);
    if (!token) throw new UnauthorizedException();
    req.user = await this.jwt.verify(token);
    return true;
  }
}

// libs/sites-management/backend-feature/src/lib/sites.controller.ts
@UseGuards(JwtAuthGuard)
@Controller('sites')
export class SitesController {
  @Get(':id')
  async get(@Param('id') id: string, @CurrentUser() user: User) {
    return this.sites.getForUser(id, user);
  }
}

// libs/sites-management/backend-feature/src/lib/sites.service.ts
async getForUser(siteId: string, user: User): Promise<Site> {
  const site = await this.repo.findById(siteId);
  if (!site) throw new NotFoundException();
  if (!this.policy.canRead(user, site)) throw new ForbiddenException();
  return site;
}

Contribute

Released under the MIT License.

esc