diff --git a/packages/mcp-auth/src/auth/token-verifier.test.ts b/packages/mcp-auth/src/auth/token-verifier.test.ts index d26cfac..832050a 100644 --- a/packages/mcp-auth/src/auth/token-verifier.test.ts +++ b/packages/mcp-auth/src/auth/token-verifier.test.ts @@ -135,6 +135,27 @@ describe('TokenVerifier', () => { }); expect(verifyJwtMock).toHaveBeenCalledWith(token); }); + + it('should reuse the same remote JWK Set instance for the same JWKS URI', async () => { + const mockGetKey = vi.fn(); + vi.mocked(jose.createRemoteJWKSet).mockReturnValue(mockGetKey); + vi.mocked(createVerifyJwt).mockReturnValue(vi.fn()); + + const token = await createJwt({ + iss: 'https://trusted.issuer.com', + client_id: 'client12345', + }); + + const tokenVerifier = new TokenVerifier(authServers); + const verifyJwtFunction = tokenVerifier.createVerifyJwtFunction({}); + + // Call twice with same issuer + await verifyJwtFunction(token); + await verifyJwtFunction(token); + + // Should only create once due to caching + expect(jose.createRemoteJWKSet).toHaveBeenCalledTimes(1); + }); }); describe('getJwtIssuerValidator', () => { diff --git a/packages/mcp-auth/src/auth/token-verifier.ts b/packages/mcp-auth/src/auth/token-verifier.ts index ef7588f..a6e1f29 100644 --- a/packages/mcp-auth/src/auth/token-verifier.ts +++ b/packages/mcp-auth/src/auth/token-verifier.ts @@ -43,6 +43,13 @@ export type GetTokenVerifierOptions = { * verification functions and validating token issuers based on that context. */ export class TokenVerifier { + /** + * Cache for remote JWK Set instances, keyed by JWKS URI. + * This ensures we reuse the same instance for the same URI, allowing jose's + * internal caching (cooldownDuration, cacheMaxAge) to work effectively. + */ + private readonly jwksCache = new Map>(); + /** * Creates an instance of TokenVerifier. * @param authServers The complete configuration of all authorization servers trusted by the @@ -75,7 +82,8 @@ export class TokenVerifier { }); } - return createVerifyJwt(createRemoteJWKSet(new URL(jwksUri), remoteJwkSet), jwtVerify)(token); + const getKey = this.getOrCreateRemoteJWKSet(jwksUri, remoteJwkSet); + return createVerifyJwt(getKey, jwtVerify)(token); }; } @@ -130,4 +138,23 @@ export class TokenVerifier { private getAuthServerMetadataByIssuer(issuer: string) { return this.authServers.find(({ metadata }) => metadata.issuer === issuer)?.metadata; } + + /** + * Gets an existing remote JWK Set instance from the cache, or creates a new one if not cached. + * Caching the instance allows jose's internal mechanisms (cooldownDuration, cacheMaxAge) to + * work effectively, reducing redundant HTTP requests to the JWKS endpoint. + */ + private getOrCreateRemoteJWKSet( + jwksUri: string, + options?: RemoteJWKSetOptions + ): ReturnType { + const cached = this.jwksCache.get(jwksUri); + if (cached) { + return cached; + } + + const getKey = createRemoteJWKSet(new URL(jwksUri), options); + this.jwksCache.set(jwksUri, getKey); + return getKey; + } }