Fix UnauthorizedException: Unauthorized in NestJS
This error occurs when a NestJS guard returns false or throws, blocking access to a protected route. Common causes include missing or invalid JWT tokens, expired sessions, or misconfigured guard logic. Fix it by verifying the token is sent correctly in the Authorization header and the guard validates it properly.
Reading the Stack Trace
Here's what each line means:
- at JwtAuthGuard.handleRequest (src/auth/jwt-auth.guard.ts:14:13): Your custom guard's handleRequest at line 14 threw UnauthorizedException because the JWT validation failed.
- at GuardsConsumer.tryActivate (node_modules/@nestjs/core/guards/guards-consumer.js:15:39): NestJS's guard execution pipeline invoked the guard before reaching the route handler.
- at RouterExplorer.executePipeline (node_modules/@nestjs/core/router/router-execution-context.js:173:23): The router pipeline executed guards before pipes and interceptors, blocking the request early.
Common Causes
1. Missing Authorization header
The client does not include the Bearer token in the request headers, so the guard has no token to validate.
// Client request missing Authorization header
fetch('/api/profile', { method: 'GET' });
2. Expired JWT token
The token's exp claim has passed, causing the passport strategy to reject it.
const token = jwt.sign({ userId: 1 }, secret, { expiresIn: '1h' });
// Token used 2 hours later - expired
3. JWT secret mismatch between sign and verify
The secret used to sign tokens differs from the one configured in the passport strategy.
// Auth module uses 'old-secret' for signing
// JwtStrategy uses process.env.JWT_SECRET which is 'new-secret'
The Fix
Add public route support via a custom decorator so not every route requires authentication. Improve the error message by using passport's info object to explain why authentication failed (expired, malformed, missing).
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user) {
if (err || !user) {
throw new UnauthorizedException();
}
return user;
}
}
import { Injectable, UnauthorizedException, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
handleRequest(err: any, user: any, info: any) {
if (err || !user) {
const message = info?.message || 'Invalid or missing authentication token';
throw new UnauthorizedException(message);
}
return user;
}
}
Testing the Fix
import { Test, TestingModule } from '@nestjs/testing';
import { JwtAuthGuard } from './jwt-auth.guard';
import { Reflector } from '@nestjs/core';
import { UnauthorizedException, ExecutionContext } from '@nestjs/common';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [JwtAuthGuard, Reflector],
}).compile();
guard = module.get<JwtAuthGuard>(JwtAuthGuard);
reflector = module.get<Reflector>(Reflector);
});
it('throws UnauthorizedException when user is null', () => {
expect(() => guard.handleRequest(null, null, { message: 'jwt expired' }))
.toThrow(UnauthorizedException);
});
it('returns user when authentication succeeds', () => {
const user = { id: 1, email: 'test@example.com' };
expect(guard.handleRequest(null, user, null)).toEqual(user);
});
it('includes info message in the exception', () => {
expect(() => guard.handleRequest(null, null, { message: 'jwt expired' }))
.toThrow('jwt expired');
});
});
Run your tests:
npm test
Pushing Through CI/CD
git checkout -b fix/nestjs-guard-unauthorized,git add src/auth/jwt-auth.guard.ts src/auth/__tests__/jwt-auth.guard.spec.ts,git commit -m "fix: improve JWT guard error messages and add public route support",git push origin fix/nestjs-guard-unauthorized
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- run: npm run build
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
npm install bugstack-sdk
Step 2: Initialize
import { initBugStack } from 'bugstack-sdk'
initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Verify the JWT secret is consistent between signing and verification.
- Ensure clients send the Authorization: Bearer <token> header.
- Add descriptive error messages to the guard's handleRequest.
- Run tests to verify guard behavior for valid and invalid tokens.
- Open a PR, merge after CI, and test authentication in staging.
Frequently Asked Questions
BugStack runs the fix through your existing test suite, generates additional edge-case tests, and validates that no other modules are affected before marking it safe to deploy.
BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.
Create a @Public() decorator using SetMetadata and check for it in canActivate(). Routes decorated with @Public() will bypass JWT validation.
Issue a refresh token alongside the access token. When the access token expires, the client sends the refresh token to get a new access token without re-authenticating.