fix(auth): JWKS URL-tracking singleton + expanded test coverage
This commit is contained in:
parent
3d2ce9325f
commit
82e7a9c367
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user