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()));
|
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', () => {
|
describe('JWT verification — RS256 JWKS + HS256 fallback', () => {
|
||||||
it('should fall back to HS256 when JWKS unavailable', async () => {
|
it('should fall back to HS256 when JWKS unavailable', async () => {
|
||||||
mockConfig.PLATFORM_JWKS_URL = undefined;
|
mockConfig.PLATFORM_JWKS_URL = undefined;
|
||||||
|
const token = await makeHS256Token({
|
||||||
const secret = new TextEncoder().encode(mockConfig.JWT_SECRET as string);
|
|
||||||
const token = await new SignJWT({
|
|
||||||
sub: 'user-456',
|
sub: 'user-456',
|
||||||
email: 'fallback@example.com',
|
email: 'fallback@example.com',
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
type: 'access',
|
type: 'access',
|
||||||
})
|
});
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
|
||||||
.setIssuedAt()
|
|
||||||
.setExpirationTime('1h')
|
|
||||||
.sign(secret);
|
|
||||||
|
|
||||||
const payload = await extractAuth({
|
const payload = await extractAuth({
|
||||||
headers: { authorization: `Bearer ${token}` },
|
headers: { authorization: `Bearer ${token}` },
|
||||||
@ -84,4 +87,37 @@ describe('JWT verification — RS256 JWKS + HS256 fallback', () => {
|
|||||||
expect(payload.sub).toBe('user-123');
|
expect(payload.sub).toBe('user-123');
|
||||||
expect(payload.role).toBe('user');
|
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)
|
// Lazy-init JWKS client (cached, auto-refreshed by jose)
|
||||||
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
|
||||||
|
let jwksUrl: string | undefined;
|
||||||
|
|
||||||
function getJWKS(): ReturnType<typeof createRemoteJWKSet> | null {
|
function getJWKS(): ReturnType<typeof createRemoteJWKSet> | null {
|
||||||
if (jwks) return jwks;
|
|
||||||
const url = config.PLATFORM_JWKS_URL;
|
const url = config.PLATFORM_JWKS_URL;
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
|
if (jwks && jwksUrl === url) return jwks;
|
||||||
jwks = createRemoteJWKSet(new URL(url));
|
jwks = createRemoteJWKSet(new URL(url));
|
||||||
|
jwksUrl = url;
|
||||||
return jwks;
|
return jwks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user