Understanding Authentication Testing in Jest
Authentication testing verifies that your application correctly handles user identity, authorization, and access control flows. When using Jest—the widely adopted JavaScript testing framework—you need to simulate and validate three dominant authentication patterns: JSON Web Tokens (JWT), session-based authentication, and OAuth integration with third-party providers. This tutorial walks you through complete, practical testing strategies for each approach.
What Is Jest Authentication Testing?
Jest authentication testing means writing automated tests that verify your application's authentication layer behaves correctly under various conditions. This includes testing login endpoints, token validation middleware, session persistence, OAuth callback handlers, and authorization guards. Rather than manually clicking through your app, you write test suites that programmatically send requests, mock external services, and assert that the right HTTP status codes, headers, cookies, and database states result from each operation.
At its core, authentication testing with Jest involves:
- Unit tests for individual functions like token generation, password hashing, and session serialization
- Integration tests for middleware chains, route handlers, and database interactions
- End-to-end tests that simulate complete login flows across multiple requests
Why Authentication Testing Matters
Authentication is the gateway to your application. A bug in this layer can expose sensitive user data, allow unauthorized access, or lock legitimate users out. Consider these real-world risks that proper testing prevents:
- Token expiration bugs that keep sessions alive indefinitely or expire them prematurely
- Signature verification failures that accept forged or tampered JWTs
- OAuth redirect manipulation that could lead to account takeover
- Session fixation attacks where an attacker reuses a session ID across users
- CSRF vulnerabilities in session-cookie implementations
- Race conditions in refresh token rotation logic
Beyond security, authentication testing ensures your user experience remains consistent. A failing auth flow means lost users and frustrated customers. Automated tests catch regressions when you refactor middleware or upgrade dependencies like jsonwebtoken, passport, or express-session.
Setting Up Your Jest Testing Environment
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before diving into authentication-specific tests, ensure your Jest configuration supports the runtime features you need. Create a jest.config.js file or add configuration to your package.json.
// jest.config.js
module.exports = {
testEnvironment: 'node',
setupFilesAfterSetup: ['<rootDir>/test-setup.js'],
testTimeout: 10000,
clearMocks: true,
restoreMocks: true,
coveragePathIgnorePatterns: [
'/node_modules/',
'/dist/'
]
};
For projects using TypeScript, add ts-jest as the preset. For Express or NestJS applications, you may want to use the Node test environment. If your auth flow involves browser-based cookies (like session cookies), consider using a library like supertest paired with a cookie jar.
npm install --save-dev jest supertest @types/jest ts-jest
npm install jsonwebtoken bcrypt express-session passport cookie-parser
JWT Authentication Testing
How JWT Authentication Works
JWT authentication relies on stateless tokens. The server issues a signed token containing claims (like user ID, role, expiration) after successful credential verification. The client stores this token—typically in memory or localStorage—and sends it with each request via the Authorization: Bearer <token> header. The server verifies the token's signature and extracts claims without needing a database lookup, making it ideal for distributed systems and APIs.
Testing JWT Token Generation
Start by testing the function responsible for creating tokens. This is a pure unit test—no HTTP requests needed. You'll mock the jsonwebtoken library's sign method or test it directly with a real secret.
// services/auth.service.js
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET || 'test-secret';
function generateToken(user) {
const payload = {
sub: user.id,
email: user.email,
role: user.role,
iat: Math.floor(Date.now() / 1000)
};
return jwt.sign(payload, SECRET, {
expiresIn: '15m',
algorithm: 'HS256'
});
}
function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['HS256'] });
}
module.exports = { generateToken, verifyToken };
// __tests__/auth.service.test.js
const jwt = require('jsonwebtoken');
const { generateToken, verifyToken } = require('../services/auth.service');
describe('JWT Token Generation', () => {
const mockUser = {
id: 'user-123',
email: 'alice@example.com',
role: 'admin'
};
test('generates a token containing correct claims', () => {
const token = generateToken(mockUser);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
const decoded = jwt.decode(token);
expect(decoded.sub).toBe('user-123');
expect(decoded.email).toBe('alice@example.com');
expect(decoded.role).toBe('admin');
});
test('generates a token with iat claim', () => {
const token = generateToken(mockUser);
const decoded = jwt.decode(token);
expect(decoded.iat).toBeDefined();
expect(typeof decoded.iat).toBe('number');
});
test('generates a token that verifies successfully', () => {
const token = generateToken(mockUser);
const decoded = verifyToken(token);
expect(decoded.sub).toBe('user-123');
expect(decoded.email).toBe('alice@example.com');
});
});
Testing JWT Verification and Expiration
Token verification must reject expired tokens, tokens with invalid signatures, and tokens with tampered payloads. Write tests that create tokens with controlled expiration times using Jest's fake timers.
describe('JWT Token Verification', () => {
test('rejects an expired token', () => {
// Create a token that expires immediately in the past
const expiredToken = jwt.sign(
{ sub: 'user-123', email: 'alice@example.com' },
'test-secret',
{ expiresIn: '0s' }
);
// Advance time past the expiration
jest.useFakeTimers();
jest.advanceTimersByTime(1000);
expect(() => {
verifyToken(expiredToken);
}).toThrow(jwt.TokenExpiredError);
jest.useRealTimers();
});
test('rejects a token with invalid signature', () => {
const validToken = generateToken({ id: 'user-123', email: 'alice@example.com', role: 'user' });
// Tamper with the token payload by appending characters
const tamperedToken = validToken.slice(0, -5) + 'xxxxx';
expect(() => {
jwt.verify(tamperedToken, 'test-secret', { algorithms: ['HS256'] });
}).toThrow(jwt.JsonWebTokenError);
});
test('rejects a token signed with a different secret', () => {
const foreignToken = jwt.sign(
{ sub: 'user-123' },
'wrong-secret',
{ algorithm: 'HS256' }
);
expect(() => {
verifyToken(foreignToken);
}).toThrow(jwt.JsonWebTokenError);
});
test('rejects null or undefined tokens', () => {
expect(() => verifyToken(null)).toThrow();
expect(() => verifyToken(undefined)).toThrow();
expect(() => verifyToken('')).toThrow();
});
});
Testing JWT Middleware in Express
Middleware that guards routes by verifying JWTs is critical. Use supertest to send HTTP requests with and without valid tokens, then assert the response status codes.
// middleware/auth.middleware.js
const { verifyToken } = require('../services/auth.service');
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or malformed Authorization header' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
}
return res.status(403).json({ error: 'Invalid token', code: 'TOKEN_INVALID' });
}
}
module.exports = { authenticate };
// __tests__/auth.middleware.test.js
const request = require('supertest');
const express = require('express');
const jwt = require('jsonwebtoken');
const { authenticate } = require('../middleware/auth.middleware');
// Build a minimal Express app for testing
function createTestApp() {
const app = express();
app.use(express.json());
// Protected route
app.get('/protected', authenticate, (req, res) => {
res.json({ user: req.user, message: 'Access granted' });
});
// Another protected route with role check
app.get('/admin', authenticate, (req, res) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
res.json({ message: 'Welcome admin' });
});
return app;
}
describe('JWT Authentication Middleware', () => {
const app = createTestApp();
const SECRET = process.env.JWT_SECRET || 'test-secret';
test('returns 401 when no Authorization header is present', async () => {
const res = await request(app)
.get('/protected')
.expect(401);
expect(res.body.error).toContain('Missing');
});
test('returns 401 when Authorization header is malformed', async () => {
const res = await request(app)
.get('/protected')
.set('Authorization', 'Basic sometoken')
.expect(401);
expect(res.body.error).toContain('malformed');
});
test('returns 200 with user data when token is valid', async () => {
const token = jwt.sign(
{ sub: 'user-456', email: 'bob@example.com', role: 'user' },
SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const res = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.user.sub).toBe('user-456');
expect(res.body.user.email).toBe('bob@example.com');
expect(res.body.message).toBe('Access granted');
});
test('returns 403 when user role is insufficient', async () => {
const token = jwt.sign(
{ sub: 'user-789', email: 'charlie@example.com', role: 'user' },
SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const res = await request(app)
.get('/admin')
.set('Authorization', `Bearer ${token}`)
.expect(403);
expect(res.body.error).toBe('Insufficient permissions');
});
test('returns 200 when admin accesses admin route', async () => {
const token = jwt.sign(
{ sub: 'user-999', email: 'admin@example.com', role: 'admin' },
SECRET,
{ expiresIn: '15m', algorithm: 'HS256' }
);
const res = await request(app)
.get('/admin')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body.message).toBe('Welcome admin');
});
test('returns 401 for expired token', async () => {
const expiredToken = jwt.sign(
{ sub: 'user-000', email: 'expired@example.com', role: 'user' },
SECRET,
{ expiresIn: '0s', algorithm: 'HS256' }
);
const res = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${expiredToken}`)
.expect(401);
expect(res.body.code).toBe('TOKEN_EXPIRED');
});
});
Testing Refresh Token Rotation
Modern JWT implementations often use short-lived access tokens paired with longer-lived refresh tokens. Testing refresh logic ensures tokens rotate securely and old tokens become invalid after rotation.
// __tests__/refresh-token.test.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Simulated refresh token store
const refreshTokenStore = new Map();
function issueTokens(user) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
'access-secret',
{ expiresIn: '5m', algorithm: 'HS256' }
);
const refreshToken = crypto.randomBytes(32).toString('hex');
refreshTokenStore.set(refreshToken, {
userId: user.id,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 days
});
return { accessToken, refreshToken };
}
function rotateRefreshToken(oldRefreshToken) {
const stored = refreshTokenStore.get(oldRefreshToken);
if (!stored || Date.now() > stored.expiresAt) {
throw new Error('Invalid or expired refresh token');
}
// Invalidate old token
refreshTokenStore.delete(oldRefreshToken);
// Issue new tokens
const newAccessToken = jwt.sign(
{ sub: stored.userId },
'access-secret',
{ expiresIn: '5m', algorithm: 'HS256' }
);
const newRefreshToken = crypto.randomBytes(32).toString('hex');
refreshTokenStore.set(newRefreshToken, {
userId: stored.userId,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000
});
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
describe('Refresh Token Rotation', () => {
beforeEach(() => {
refreshTokenStore.clear();
});
test('issues valid access and refresh tokens', () => {
const tokens = issueTokens({ id: 'user-1', role: 'user' });
const decoded = jwt.verify(tokens.accessToken, 'access-secret', { algorithms: ['HS256'] });
expect(decoded.sub).toBe('user-1');
expect(refreshTokenStore.has(tokens.refreshToken)).toBe(true);
});
test('rotation invalidates old refresh token', () => {
const initial = issueTokens({ id: 'user-2', role: 'user' });
const rotated = rotateRefreshToken(initial.refreshToken);
// Old token should be gone
expect(refreshTokenStore.has(initial.refreshToken)).toBe(false);
// New token should exist
expect(refreshTokenStore.has(rotated.refreshToken)).toBe(true);
// Access token should be new
const decoded = jwt.verify(rotated.accessToken, 'access-secret', { algorithms: ['HS256'] });
expect(decoded.sub).toBe('user-2');
});
test('rejects reuse of old refresh token after rotation', () => {
const initial = issueTokens({ id: 'user-3', role: 'user' });
rotateRefreshToken(initial.refreshToken);
expect(() => {
rotateRefreshToken(initial.refreshToken);
}).toThrow('Invalid or expired refresh token');
});
test('rejects expired refresh token', () => {
const initial = issueTokens({ id: 'user-4', role: 'user' });
// Manually expire the token
const stored = refreshTokenStore.get(initial.refreshToken);
stored.expiresAt = Date.now() - 1000;
expect(() => {
rotateRefreshToken(initial.refreshToken);
}).toThrow('Invalid or expired refresh token');
});
});
Session-Based Authentication Testing
How Session Authentication Works
Session authentication stores user identity on the server, linked to a session identifier sent to the client as a cookie. When a user logs in, the server creates a session object (containing user ID, role, and other data) and sends a Set-Cookie header with a session ID. The browser automatically includes this cookie in subsequent requests, and the server looks up the session to identify the user. This approach keeps sensitive data server-side and is the traditional model for server-rendered web applications.
Testing Session Creation and Persistence
Use supertest with a cookie jar to simulate browser-like cookie handling across multiple requests. The supertest agent feature automatically stores and sends cookies.
// app.js (Express with session-based auth)
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();
// Session configuration
app.use(session({
secret: 'session-secret-test',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // set true in production
maxAge: 60 * 60 * 1000 // 1 hour
}
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mock user database
const users = new Map();
users.set('alice@example.com', {
id: 'user-alice',
email: 'alice@example.com',
password: 'hashed-password-placeholder',
name: 'Alice'
});
// Passport local strategy
passport.use(new LocalStrategy(
{ usernameField: 'email' },
(email, password, done) => {
const user = users.get(email);
if (!user) return done(null, false, { message: 'User not found' });
// In real app, compare hashed passwords here
if (password !== 'correct-password') {
return done(null, false, { message: 'Invalid password' });
}
return done(null, user);
}
));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => {
// In real app, fetch from database
for (const [email, user] of users) {
if (user.id === id) return done(null, user);
}
done(null, null);
});
// Login route
app.post('/login', passport.authenticate('local'), (req, res) => {
res.json({ user: req.user, message: 'Logged in successfully' });
});
// Protected route
app.get('/dashboard', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ user: req.user, sessionId: req.sessionID });
});
// Logout route
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) return res.status(500).json({ error: 'Logout failed' });
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
});
module.exports = app;
// __tests__/session-auth.test.js
const request = require('supertest');
const app = require('../app');
describe('Session-Based Authentication', () => {
test('rejects access to protected route when not logged in', async () => {
const res = await request(app)
.get('/dashboard')
.expect(401);
expect(res.body.error).toBe('Not authenticated');
});
test('successful login creates a session cookie', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'alice@example.com', password: 'correct-password' })
.expect(200);
expect(res.body.user.name).toBe('Alice');
expect(res.body.message).toBe('Logged in successfully');
// Check that Set-Cookie header is present
expect(res.headers['set-cookie']).toBeDefined();
// The cookie should be httpOnly
const cookieHeader = Array.isArray(res.headers['set-cookie'])
? res.headers['set-cookie'].find(c => c.includes('connect.sid'))
: res.headers['set-cookie'];
expect(cookieHeader).toContain('connect.sid');
expect(cookieHeader).toContain('HttpOnly');
});
test('maintains session across multiple requests', async () => {
const agent = request.agent(app);
// Login
const loginRes = await agent
.post('/login')
.send({ email: 'alice@example.com', password: 'correct-password' })
.expect(200);
expect(loginRes.body.user.name).toBe('Alice');
// Access protected route with the same agent (cookie jar persists)
const dashRes = await agent
.get('/dashboard')
.expect(200);
expect(dashRes.body.user.email).toBe('alice@example.com');
expect(dashRes.body.sessionId).toBeDefined();
});
test('logout destroys session and clears cookie', async () => {
const agent = request.agent(app);
// Login first
await agent
.post('/login')
.send({ email: 'alice@example.com', password: 'correct-password' })
.expect(200);
// Logout
const logoutRes = await agent
.post('/logout')
.expect(200);
expect(logoutRes.body.message).toBe('Logged out');
// Try accessing protected route after logout
const dashRes = await agent
.get('/dashboard')
.expect(401);
expect(dashRes.body.error).toBe('Not authenticated');
});
test('rejects login with incorrect password', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'alice@example.com', password: 'wrong-password' })
.expect(401);
expect(res.body.message).toBeDefined();
});
test('rejects login for non-existent user', async () => {
const res = await request(app)
.post('/login')
.send({ email: 'ghost@example.com', password: 'whatever' })
.expect(401);
expect(res.body.message).toBe('User not found');
});
test('different agents have separate sessions', async () => {
const agent1 = request.agent(app);
const agent2 = request.agent(app);
// Both agents log in
await agent1
.post('/login')
.send({ email: 'alice@example.com', password: 'correct-password' })
.expect(200);
await agent2
.post('/login')
.send({ email: 'alice@example.com', password: 'correct-password' })
.expect(200);
// Both can access dashboard but have different session IDs
const res1 = await agent1.get('/dashboard').expect(200);
const res2 = await agent2.get('/dashboard').expect(200);
expect(res1.body.sessionId).not.toBe(res2.body.sessionId);
});
});
Testing CSRF Protection in Sessions
When using session cookies, Cross-Site Request Forgery (CSRF) protection is essential. Test that state-changing requests without proper CSRF tokens are rejected.
// __tests__/csrf-protection.test.js
const request = require('supertest');
const express = require('express');
const session = require('express-session');
const csrf = require('csurf');
describe('CSRF Protection with Sessions', () => {
let app;
let csrfProtection;
beforeAll(() => {
app = express();
app.use(session({
secret: 'test-secret',
resave: false,
saveUninitialized: false
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
csrfProtection = csrf({ cookie: false });
// Route to get CSRF token
app.get('/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Protected POST route
app.post('/update-profile', csrfProtection, (req, res) => {
res.json({ message: 'Profile updated' });
});
// Error handler for CSRF failures
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({ error: 'CSRF token missing or invalid' });
}
next(err);
});
});
test('rejects POST request without CSRF token', async () => {
const agent = request.agent(app);
const res = await agent
.post('/update-profile')
.send({ name: 'New Name' })
.expect(403);
expect(res.body.error).toContain('CSRF');
});
test('accepts POST request with valid CSRF token', async () => {
const agent = request.agent(app);
// First get a CSRF token
const tokenRes = await agent
.get('/csrf-token')
.expect(200);
const csrfToken = tokenRes.body.csrfToken;
expect(csrfToken).toBeDefined();
// Then use it in a POST request
const res = await agent
.post('/update-profile')
.send({ name: 'New Name', _csrf: csrfToken })
.expect(200);
expect(res.body.message).toBe('Profile updated');
});
});
OAuth Integration Testing
How OAuth Integration Works
OAuth allows users to authenticate via third-party providers like Google, GitHub, or Facebook. The flow typically involves redirecting users to the provider's authorization page, receiving a callback with an authorization code, exchanging that code for access and refresh tokens, and then using those tokens to fetch the user's profile. Testing OAuth is challenging because you don't want to rely on live third-party services in your test suite—they're slow, rate-limited, and unpredictable.
Mocking OAuth Provider Responses
The key to testing OAuth is mocking the external HTTP calls. Use Jest's manual mocks or libraries like nock to intercept requests to the OAuth provider's endpoints and return controlled responses.
// services/oauth.service.js
const axios = require('axios');
class OAuthService {
constructor(config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.redirectUri = config.redirectUri;
this.tokenEndpoint = config.tokenEndpoint;
this.userInfoEndpoint = config.userInfoEndpoint;
}
getAuthorizationUrl(state) {
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: this.redirectUri,
response_type: 'code',
scope: 'openid email profile',
state
});
return `https://auth.example.com/authorize?${params.toString()}`;
}
async exchangeCodeForTokens(code) {
const response = await axios.post(this.tokenEndpoint, {
grant_type: 'authorization_code',
code,
redirect_uri: this.redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret
}, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresIn: response.data.expires_in
};
}
async fetchUserProfile(accessToken) {
const response = await axios.get(this.userInfoEndpoint, {
headers: { Authorization: `Bearer ${accessToken}` }
});
return {
id: response.data.id,
email: response.data.email,
name: response.data.name,
avatar: response.data.picture
};
}
}
module.exports = OAuthService;
// __tests__/oauth.service.test.js
const nock = require('nock');
const OAuthService = require('../services/oauth.service');
// Install nock: npm install --save-dev nock
describe('OAuth Service', () => {
const config = {
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
redirectUri: 'http://localhost:3000/callback',
tokenEndpoint: 'https://auth.example.com/oauth/token',
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo'
};
let oauthService;
beforeEach(() => {
oauthService = new OAuthService(config);
// Ensure all external requests are intercepted
nock.disableNetConnect();
// Allow localhost connections for supertest
nock.enableNetConnect('127.0.0.1');
});
afterEach(() => {
nock.cleanAll();
nock.enableNetConnect();
});
test('generates correct authorization URL', () => {
const url = oauthService.getAuthorizationUrl('state-abc123');
expect(url).toContain('https://auth.example.com/authorize');
expect(url).toContain('client_id=test-client-id');
expect(url).toContain('redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback');
expect(url).toContain('response_type=code');
expect(url).toContain('state=state-abc123');
expect(url).toContain('scope=openid%20email%20profile');
});
test('exchanges authorization code for tokens', async () => {
nock('https://auth.example.com')
.post('/oauth/token', body => {
return body.grant_type === 'authorization_code'
&& body.code === 'test-auth-code'
&& body.client_id === 'test-client-id';
})
.reply(200, {
access_token: 'mock-access-token-xyz',
refresh_token: 'mock-refresh-token-xyz',
expires_in: 3600,
token_type: 'Bearer'
});
const tokens = await oauthService.exchangeCodeForTokens('test-auth-code');
expect(tokens.accessToken).toBe('mock-access-token-xyz');
expect(tokens.refreshToken).toBe('mock-refresh-token-xyz');
expect(tokens.expiresIn).toBe(3600);
});
test('fetches user profile with access token', async () => {
nock('https://auth.example.com', {
reqheaders: { authorization: 'Bearer mock-access-token' }
})
.get('/oauth/userinfo')
.reply(200, {
id: 'provider-user-456',
email: 'jane@example.com',
name: 'Jane Doe',
picture: 'https://example.com/avatar.jpg'
});
const profile = await oauthService.fetchUserProfile('mock-access-token');
expect(profile.id).toBe('provider-user-456');
expect(profile.email).toBe('jane@example.com');
expect(profile.name).toBe('Jane Doe');
expect(profile.avatar).toBe('https://example.com/avatar.jpg');
});
test('handles token endpoint error gracefully', async () => {
nock('https://auth.example.com')
.post('/oauth/token')
.reply(400, {
error: 'invalid_grant',
error_description: 'Authorization code has expired'
});
await expect(
oauthService.exchangeCodeForTokens('expired-code')
).rejects.toThrow();
});
test('handles userinfo endpoint returning 401', async () => {
nock('https://auth.example.com')
.get('/oauth/userinfo')
.reply(401, { error: 'Invalid token' });
await expect(
oauthService.fetchUserProfile('invalid-token')
).rejects.toThrow();
});
});
Testing the OAuth Callback Handler
The callback route is where your application receives the authorization code, exchanges it, fetches the user profile, and creates or updates a local user record. Test this entire flow in integration.
// routes/oauth.routes.js
const express = require('express');
const router = express.Router();
const OAuthService = require('../services/oauth.service');
// In-memory user store for testing
const userStore = new Map();
const oauthService = new OAuthService({
clientId: process.env.OAUTH_CLIENT_ID || 'test-client-id',
clientSecret: process.env.OAUTH_CLIENT_SECRET || 'test-secret',
redirectUri: 'http://localhost:3000/oauth/callback',
tokenEndpoint: 'https://auth.example.com/oauth/token',
userInfoEndpoint: 'https://auth.example.com/oauth/userinfo'
});
router.get('/login', (req, res) => {
const state = Math.random().toString(36).substring(2);
// Store state for CSRF protection
req.session.oauthState = state;
const url = oauthService.getAuthorizationUrl(state);
res.redirect(url);
});
router.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state to prevent CSRF
if (state !== req.session.oauthState) {
return res.status(403).json({ error: 'Invalid state parameter' });
}
try {
// Exchange code for tokens
const tokens = await oauthService.exchangeCodeForTokens(code);
// Fetch user profile
const profile = await oauthService.fetchUserProfile(tokens.accessToken);
// Find or create user
let user = userStore.get(profile.email);
if (!user) {
user = {
id: `local-${Date.now()}`,
email: profile.email,
name: profile.name,
avatar: profile.avatar,
providerId: profile.id,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
};
userStore.set(profile