import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; import { SignJWT, generateKeyPair, exportJWK } from 'jose'; import { createServer, type Server } from 'node:http'; const { mockConfig } = vi.hoisted(() => ({ mockConfig: { JWT_SECRET: 'test-hs256-secret-at-least-32-chars-long', PLATFORM_JWKS_URL: undefined as string | undefined, } as Record, })); vi.mock('./config.js', () => ({ config: new Proxy({} as Record, { get: (_target, prop: string) => mockConfig[prop], }), })); import { extractAuth, requireRole } from './auth.js'; let rsaKeys: Awaited>; let server: Server; let jwksPort: number; beforeAll(async () => { rsaKeys = await generateKeyPair('RS256'); const publicJwk = await exportJWK(rsaKeys.publicKey); publicJwk.alg = 'RS256'; publicJwk.use = 'sig'; publicJwk.kid = 'test-key-1'; server = createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ keys: [publicJwk] })); }); await new Promise(resolve => server.listen(0, resolve)); const addr = server.address(); jwksPort = typeof addr === 'object' && addr ? addr.port : 0; }); afterAll(async () => { await new Promise(resolve => server.close(() => resolve())); }); function makeHS256Token(claims: Record) { const secret = new TextEncoder().encode(mockConfig.JWT_SECRET as string); return new SignJWT(claims) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('1h') .sign(secret); } describe('JWT verification — RS256 JWKS + HS256 fallback', () => { it('should fall back to HS256 when JWKS unavailable', async () => { mockConfig.PLATFORM_JWKS_URL = undefined; const token = await makeHS256Token({ sub: 'user-456', email: 'fallback@example.com', role: 'admin', type: 'access', }); const payload = await extractAuth({ headers: { authorization: `Bearer ${token}` }, }); expect(payload.sub).toBe('user-456'); expect(payload.role).toBe('admin'); }); it('should accept RS256 token verified via JWKS', async () => { mockConfig.PLATFORM_JWKS_URL = `http://localhost:${jwksPort}`; const token = await new SignJWT({ sub: 'user-123', email: 'test@example.com', role: 'user', type: 'access', }) .setProtectedHeader({ alg: 'RS256', kid: 'test-key-1' }) .setIssuedAt() .setExpirationTime('1h') .sign(rsaKeys.privateKey); const payload = await extractAuth({ headers: { authorization: `Bearer ${token}` }, }); expect(payload.sub).toBe('user-123'); expect(payload.role).toBe('user'); }); it('should throw UnauthorizedError when no Bearer token', async () => { await expect(extractAuth({ headers: {} })).rejects.toThrow('Unauthorized'); await expect( extractAuth({ headers: { authorization: 'Basic abc' } }), ).rejects.toThrow('Unauthorized'); }); it('should reject non-access token type', async () => { mockConfig.PLATFORM_JWKS_URL = undefined; const token = await makeHS256Token({ sub: 'user-1', type: 'refresh' }); await expect( extractAuth({ headers: { authorization: `Bearer ${token}` } }), ).rejects.toThrow('Invalid or expired token'); }); it('should enforce role via requireRole', async () => { mockConfig.PLATFORM_JWKS_URL = undefined; const token = await makeHS256Token({ sub: 'user-1', role: 'user', type: 'access', }); const req = { headers: { authorization: `Bearer ${token}` } }; const payload = await requireRole(req, 'user'); expect(payload.sub).toBe('user-1'); await expect(requireRole(req, 'admin')).rejects.toThrow( 'Insufficient permissions', ); }); });