fix(auth): JWKS URL-tracking singleton + expanded test coverage

This commit is contained in:
saravanakumardb1 2026-03-12 11:22:04 -07:00
parent 8e2a1b37c3
commit 4ffe7569b0
2 changed files with 47 additions and 9 deletions

View File

@ -41,21 +41,24 @@ afterAll(async () => {
await new Promise<void>(resolve => server.close(() => resolve()));
});
function makeHS256Token(claims: Record<string, unknown>) {
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 secret = new TextEncoder().encode(mockConfig.JWT_SECRET as string);
const token = await new SignJWT({
const token = await makeHS256Token({
sub: 'user-456',
email: 'fallback@example.com',
role: 'admin',
type: 'access',
})
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
});
const payload = await extractAuth({
headers: { authorization: `Bearer ${token}` },
@ -84,4 +87,37 @@ describe('JWT verification — RS256 JWKS + HS256 fallback', () => {
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',
);
});
});

View File

@ -12,12 +12,14 @@ export type { AuthPayload };
// Lazy-init JWKS client (cached, auto-refreshed by jose)
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
let jwksUrl: string | undefined;
function getJWKS(): ReturnType<typeof createRemoteJWKSet> | null {
if (jwks) return jwks;
const url = config.PLATFORM_JWKS_URL;
if (!url) return null;
if (jwks && jwksUrl === url) return jwks;
jwks = createRemoteJWKSet(new URL(url));
jwksUrl = url;
return jwks;
}