Authentication Testing with Supertest — Automated Auth Endpoint Verification
In this tutorial, you will learn about Authentication Testing with Supertest. We cover key concepts, practical examples, and best practices to help you master this topic.
Supertest enables automated testing of Express authentication endpoints by simulating HTTP requests with various auth headers, tokens, and credentials, verifying correct status codes and error responses.
What You'll Learn
Testing login endpoints with valid and invalid credentials, JWT token generation and verification tests, protected route access control tests, refresh token flows, and negative test cases.
Why It Matters
Authentication is the most security-critical part of your API. Automated tests ensure auth logic works correctly after every change, preventing regressions that could expose protected endpoints.
Real-World Use
Stripe's auth middleware has 95%+ test coverage. GitHub runs auth tests in CI on every PR. Durga Antivirus Pro uses Supertest to verify all auth endpoints before deploying to production.
Code Example: Testing Login Endpoint
const request = require('supertest');
const app = require('../app');
const jwt = require('jsonwebtoken');
describe('POST /api/auth/login', () => {
it('should return 200 with tokens for valid credentials', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'correct-password' })
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toHaveProperty('access_token');
expect(res.body).toHaveProperty('refresh_token');
expect(res.body.expires_in).toBe(900);
// Verify the JWT is valid
const decoded = jwt.verify(
res.body.access_token,
process.env.JWT_SECRET
);
expect(decoded.sub).toBe('analyst');
expect(decoded).toHaveProperty('iat');
expect(decoded).toHaveProperty('exp');
});
it('should return 401 for invalid password', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'wrong-password' })
.expect(401);
expect(res.body).toHaveProperty('error');
expect(res.body.error).toBe('Invalid credentials');
});
it('should return 401 for missing username', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ password: 'correct-password' })
.expect(401);
});
it('should return 401 for non-existent user', async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'nonexistent', password: 'anything' })
.expect(401);
});
it('should return 429 after too many failed attempts', async () => {
for (let i = 0; i < 5; i++) {
await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'wrong' });
}
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'correct-password' })
.expect(429);
expect(res.body.error).toBe('account_locked');
});
});
Code Example: Testing Protected Routes
describe('GET /api/v1/threats', () => {
let validToken;
let expiredToken;
let malformedToken;
beforeAll(() => {
validToken = jwt.sign(
{ sub: 'analyst', roles: ['analyst'], scope: 'threat:read' },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
expiredToken = jwt.sign(
{ sub: 'analyst', roles: ['analyst'] },
process.env.JWT_SECRET,
{ expiresIn: '0s' }
);
malformedToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.invalid';
});
it('should return 200 with valid token', async () => {
const res = await request(app)
.get('/api/v1/threats')
.set('Authorization', `Bearer ${validToken}`)
.expect(200);
expect(res.body).toHaveProperty('threats');
});
it('should return 401 without auth header', async () => {
const res = await request(app)
.get('/api/v1/threats')
.expect(401);
expect(res.body.error).toBe('UNAUTHORIZED');
});
it('should return 401 with expired token', async () => {
const res = await request(app)
.get('/api/v1/threats')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
expect(res.body.error).toBe('TOKEN_EXPIRED');
});
it('should return 401 with malformed token', async () => {
const res = await request(app)
.get('/api/v1/threats')
.set('Authorization', `Bearer ${malformedToken}`)
.expect(401);
expect(res.body.error).toBe('INVALID_TOKEN');
});
it('should return 401 with wrong auth scheme', async () => {
const res = await request(app)
.get('/api/v1/threats')
.set('Authorization', `Basic ${Buffer.from('user:pass').toString('base64')}`)
.expect(401);
});
});
Code Example: Testing Refresh Token Flow
describe('POST /api/auth/refresh', () => {
let refreshToken;
beforeAll(async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'correct-password' });
refreshToken = res.body.refresh_token;
});
it('should return new tokens with valid refresh token', async () => {
const res = await request(app)
.post('/api/auth/refresh')
.send({ refresh_token: refreshToken })
.expect(200);
expect(res.body).toHaveProperty('access_token');
expect(res.body).toHaveProperty('refresh_token');
// New refresh token should be different (rotation)
expect(res.body.refresh_token).not.toBe(refreshToken);
});
it('should reject reused refresh token (rotation)', async () => {
// The previous test consumed the original refreshToken
const res = await request(app)
.post('/api/auth/refresh')
.send({ refresh_token: refreshToken })
.expect(401);
expect(res.body.error).toMatch(/revoked|invalid|theft/i);
});
it('should reject missing refresh token', async () => {
const res = await request(app)
.post('/api/auth/refresh')
.send({})
.expect(401);
});
});
describe('POST /api/auth/logout', () => {
it('should revoke the refresh token', async () => {
const loginRes = await request(app)
.post('/api/auth/login')
.send({ username: 'analyst', password: 'correct-password' });
await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${loginRes.body.access_token}`)
.expect(200);
// Refreshing should now fail
const refreshRes = await request(app)
.post('/api/auth/refresh')
.send({ refresh_token: loginRes.body.refresh_token })
.expect(401);
});
});
Common Mistakes
1. Not Testing Token Expiry
Expired token handling is often overlooked. Generate tokens with expiresIn: '0s' to test immediate expiry. Ensure the error message distinguishes expired from invalid.
2. Hardcoding Tokens in Tests
Tokens will expire and break tests. Generate tokens dynamically in beforeAll using the same JWT library and secret as the application.
3. Not Testing Negative Cases
Tests should cover: no auth header, wrong auth scheme, malformed token, expired token, revoked token, wrong audience, insufficient scopes.
4. Testing Auth Without Isolation
Auth tests should not depend on database state. Mock the database layer for auth tests. Use in-memory stores or test containers for end-to-end tests.
5. Ignoring Rate Limiting in Tests
Rate-limited endpoints will fail after repeated requests. Reset rate limiters between tests or configure higher limits for the test environment.
Practice Questions
- Why should auth tests never use hardcoded tokens?
- How do you test token expiry in Supertest?
- What negative auth test cases are most important?
- How do you test refresh token rotation?
- Why should rate limiting be tested separately from auth logic?
Answers:
- Hardcoded tokens expire, breaking tests. Generate tokens in beforeAll with a future expiry date. This ensures tests are reproducible.
- Generate a token with expiresIn: '0s' or a past exp claim. Verify the endpoint returns 401 with TOKEN_EXPIRED error.
- Missing header, expired token, wrong signature, revoked token, wrong audience, insufficient scope, malformed token, replayed refresh token.
- Obtain a refresh token, use it once, then try to use it again. The second attempt should fail with 401, confirming rotation invalidated the old token.
- Rate limiting is a separate concern. Test it in dedicated rate limit tests. Auth tests should have rate limiting disabled or set to high thresholds.
Challenge: Build a complete Supertest test suite for an Express authentication system covering: login (valid/invalid/locked), protected routes (valid/expired/malformed/missing token), refresh token flow (valid/replay/rotation), and logout.
FAQ
Mini Project
Build a comprehensive Supertest test suite for an Express authentication API covering: login with valid/invalid credentials, protected route access with various token states, refresh token rotation verification, and a CI pipeline that runs auth tests on every commit.
What's Next
Now complete the API Authentication Capstone Project that combines everything you have learned into a working authentication system.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro