Skip to content

Part 4.4 — Testing Auth Guards, JWT Strategy & Protected Routes

Site Console Site Console
4 min read Updated Dec 12, 2025 Backend Development 0 comments

Authentication is a seam in your backend where many things can go wrong: missing headers, expired tokens, malformed payloads, misconfigured guards, or incorrect authorization checks.
If you don’t test auth, you’re guessing.

In this post, you’ll learn how to test authentication and authorization end-to-end in a NestJS REST backend using Jest and Supertest—without weakening security or polluting production code.


1. The Goal: Test Behavior, Not Crypto

We are not testing JWT cryptography.
That’s a library problem.

We are testing:

  • guards block unauthenticated requests

  • valid tokens allow access

  • invalid tokens are rejected

  • user identity reaches controllers

  • authorization rules are enforced

To do this, we need test-friendly auth wiring.


2. Typical Auth Setup (Quick Context)

You likely have something like:

  • JwtAuthGuard (extends AuthGuard('jwt'))

  • JwtStrategy (Passport strategy)

  • @UseGuards(JwtAuthGuard) on routes

  • req.user populated from JWT payload

We’ll assume that structure and focus on testing it safely.


3. Protect a Route (Example)

@UseGuards(JwtAuthGuard)
@Get('me')
getProfile(@Req() req) {
  return req.user;
}

This endpoint must:

  • reject unauthenticated requests

  • return user info when authenticated

Now we test that behavior.


4. Strategy 1: Use Real JWTs in Tests (Recommended)

This gives you the highest confidence with minimal complexity.

Configure a Test JWT Secret

In .env.test:

JWT_SECRET=test-secret

Your JwtStrategy should already read from env:

secretOrKey: process.env.JWT_SECRET

5. Create a Helper to Generate Test Tokens

// test/utils/jwt.ts
import jwt from 'jsonwebtoken';

export function signTestToken(payload: { sub: number; email: string }) {
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    expiresIn: '1h',
  });
}

This mirrors production behavior—but with a test secret.


6. Test a Protected Endpoint (Unauthorized)

it('rejects requests without token', async () => {
  await request(app.getHttpServer())
    .get('/users/me')
    .expect(401);
});

If this ever passes incorrectly, your guard is broken.


7. Test a Protected Endpoint (Authorized)

First, insert a user into the test DB:

const user = await prisma.user.create({
  data: { name: 'Alice', email: 'alice@example.com' },
});

Then generate a token:

const token = signTestToken({
  sub: user.id,
  email: user.email,
});

Make the request:

const res = await request(app.getHttpServer())
  .get('/users/me')
  .set('Authorization', `Bearer ${token}`)
  .expect(200);

expect(res.body.email).toBe('alice@example.com');

This verifies:

  • JWT is parsed

  • strategy runs

  • guard allows access

  • req.user is populated correctly


8. Testing Invalid Tokens

it('rejects invalid token', async () => {
  await request(app.getHttpServer())
    .get('/users/me')
    .set('Authorization', 'Bearer invalid-token')
    .expect(401);
});

This ensures your strategy does not silently accept garbage.


9. Testing Authorization Logic (Ownership Rules)

Example: only the author can delete a post.

Service logic:

if (post.authorId !== userId) {
  throw new ForbiddenException();
}

Test it:

it('forbids deleting another user’s post', async () => {
  const alice = await prisma.user.create({
    data: { name: 'Alice', email: 'a@example.com' },
  });

  const bob = await prisma.user.create({
    data: { name: 'Bob', email: 'b@example.com' },
  });

  const post = await prisma.post.create({
    data: {
      title: 'Test',
      body: 'Body',
      authorId: alice.id,
    },
  });

  const token = signTestToken({ sub: bob.id, email: bob.email });

  await request(app.getHttpServer())
    .delete(`/posts/${post.id}`)
    .set('Authorization', `Bearer ${token}`)
    .expect(403);
});

This test guards real business rules—not just auth.


10. Strategy 2: Override Guards (When Needed)

Sometimes you want to test controller logic without auth.

NestJS allows overriding guards in tests:

.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })

Example:

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],
})
  .overrideGuard(JwtAuthGuard)
  .useValue({
    canActivate: (ctx) => {
      const req = ctx.switchToHttp().getRequest();
      req.user = { id: 1, email: 'test@example.com' };
      return true;
    },
  })
  .compile();

Use this sparingly.
Prefer real JWT tests for auth correctness.


11. Common Auth Testing Pitfalls

Avoid these mistakes:

  • ❌ mocking JWT everywhere (false confidence)

  • ❌ testing only “happy path”

  • ❌ skipping authorization tests

  • ❌ sharing tokens across tests

  • ❌ disabling guards globally in tests

Auth is a security boundary—treat it seriously.


12. What to Test (Checklist)

You should have tests for:

  • unauthenticated access → 401

  • authenticated access → 200

  • invalid token → 401

  • expired token → 401

  • forbidden action → 403

  • correct req.user injection

  • ownership enforcement

If these pass, your auth layer is solid.


13. Summary

You now know how to:

  • test JWT-protected routes with Supertest

  • generate real test tokens safely

  • validate guards and strategies

  • test authorization rules

  • override guards when appropriate

  • avoid common auth testing traps

Related

Leave a comment

Sign in to leave a comment.

Comments