Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/mcp-auth/src/auth/token-verifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
29 changes: 28 additions & 1 deletion packages/mcp-auth/src/auth/token-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ReturnType<typeof createRemoteJWKSet>>();

/**
* Creates an instance of TokenVerifier.
* @param authServers The complete configuration of all authorization servers trusted by the
Expand Down Expand Up @@ -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);
};
}

Expand Down Expand Up @@ -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<typeof createRemoteJWKSet> {
const cached = this.jwksCache.get(jwksUri);
if (cached) {
return cached;
}

const getKey = createRemoteJWKSet(new URL(jwksUri), options);
this.jwksCache.set(jwksUri, getKey);
return getKey;
}
}