Add retry logic and token caching
- Implemented retry logic (5 attempts, 3 second delay) for auth server connection - Added token validation caching with 60-second default TTL - Cache respects token expiration time if provided - Cache invalidation on 401/403 responses - Automatic cache cleanup every minute - Debug logging for cache hits and misses
This commit is contained in:
parent
03deb88ea0
commit
4771c6eb34
2 changed files with 92 additions and 17 deletions
|
|
@ -9,11 +9,32 @@ import { logger } from '../utils/logger.js';
|
||||||
* Used when the MCP server is running in 'separate' mode.
|
* Used when the MCP server is running in 'separate' mode.
|
||||||
*/
|
*/
|
||||||
export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
||||||
|
// Token validation cache: token -> { authInfo, expiresAt }
|
||||||
|
private tokenCache = new Map<string, { authInfo: AuthInfo; expiresAt: number }>();
|
||||||
|
|
||||||
|
// Default cache TTL: 60 seconds (conservative for security)
|
||||||
|
private readonly defaultCacheTTL = 60 * 1000; // milliseconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new external auth verifier.
|
* Creates a new external auth verifier.
|
||||||
* @param authServerUrl Base URL of the external authorization server
|
* @param authServerUrl Base URL of the external authorization server
|
||||||
*/
|
*/
|
||||||
constructor(private authServerUrl: string) {}
|
constructor(private authServerUrl: string) {
|
||||||
|
// Periodically clean up expired cache entries
|
||||||
|
setInterval(() => this.cleanupCache(), 60 * 1000); // Every minute
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes expired entries from the cache.
|
||||||
|
*/
|
||||||
|
private cleanupCache(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [token, entry] of this.tokenCache.entries()) {
|
||||||
|
if (entry.expiresAt <= now) {
|
||||||
|
this.tokenCache.delete(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies an access token by calling the external auth server's introspection endpoint.
|
* Verifies an access token by calling the external auth server's introspection endpoint.
|
||||||
|
|
@ -22,6 +43,16 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
||||||
* @throws InvalidTokenError if the token is invalid or expired
|
* @throws InvalidTokenError if the token is invalid or expired
|
||||||
*/
|
*/
|
||||||
async verifyAccessToken(token: string): Promise<AuthInfo> {
|
async verifyAccessToken(token: string): Promise<AuthInfo> {
|
||||||
|
// Check cache first
|
||||||
|
const cached = this.tokenCache.get(token);
|
||||||
|
if (cached && cached.expiresAt > Date.now()) {
|
||||||
|
logger.debug('Token validation cache hit', {
|
||||||
|
token: token.substring(0, 8) + '...',
|
||||||
|
expiresIn: Math.round((cached.expiresAt - Date.now()) / 1000) + 's'
|
||||||
|
});
|
||||||
|
return cached.authInfo;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Token introspection is OAuth 2.0 standard (RFC 7662) for validating tokens
|
// Token introspection is OAuth 2.0 standard (RFC 7662) for validating tokens
|
||||||
// The auth server checks if the token is valid and returns metadata about it
|
// The auth server checks if the token is valid and returns metadata about it
|
||||||
|
|
@ -32,6 +63,10 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
// On 401/403, the token might be invalid - don't cache
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
this.tokenCache.delete(token); // Clear any stale cache
|
||||||
|
}
|
||||||
logger.error('Token introspection request failed', undefined, {
|
logger.error('Token introspection request failed', undefined, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
|
|
@ -60,7 +95,7 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const authInfo: AuthInfo = {
|
||||||
token,
|
token,
|
||||||
clientId: data.client_id || 'unknown',
|
clientId: data.client_id || 'unknown',
|
||||||
scopes: data.scope?.split(' ') || [], // Empty array if no scopes specified (permissive)
|
scopes: data.scope?.split(' ') || [], // Empty array if no scopes specified (permissive)
|
||||||
|
|
@ -73,6 +108,26 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
|
||||||
aud: data.aud,
|
aud: data.aud,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cache the successful introspection result
|
||||||
|
// Use token expiration if available, otherwise default TTL
|
||||||
|
const cacheDuration = data.exp
|
||||||
|
? Math.min((data.exp * 1000) - Date.now(), this.defaultCacheTTL)
|
||||||
|
: this.defaultCacheTTL;
|
||||||
|
|
||||||
|
if (cacheDuration > 0) {
|
||||||
|
this.tokenCache.set(token, {
|
||||||
|
authInfo,
|
||||||
|
expiresAt: Date.now() + cacheDuration
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('Token validation cached', {
|
||||||
|
token: token.substring(0, 8) + '...',
|
||||||
|
cacheDuration: Math.round(cacheDuration / 1000) + 's'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return authInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof InvalidTokenError) {
|
if (error instanceof InvalidTokenError) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
50
src/index.ts
50
src/index.ts
|
|
@ -175,23 +175,43 @@ if (AUTH_MODE === 'integrated') {
|
||||||
authServerUrl: AUTH_SERVER_URL
|
authServerUrl: AUTH_SERVER_URL
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch metadata from external auth server
|
// Fetch metadata from external auth server with retry logic
|
||||||
let authMetadata;
|
let authMetadata;
|
||||||
try {
|
const maxRetries = 5;
|
||||||
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
|
const retryDelay = 3000; // 3 seconds
|
||||||
if (!authMetadataResponse.ok) {
|
|
||||||
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, {
|
||||||
|
authServerUrl: AUTH_SERVER_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
|
||||||
|
if (!authMetadataResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
|
||||||
|
}
|
||||||
|
authMetadata = await authMetadataResponse.json();
|
||||||
|
logger.info('Successfully fetched auth server metadata', {
|
||||||
|
issuer: authMetadata.issuer,
|
||||||
|
authorizationEndpoint: authMetadata.authorization_endpoint,
|
||||||
|
tokenEndpoint: authMetadata.token_endpoint
|
||||||
|
});
|
||||||
|
break; // Success, exit retry loop
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
logger.info(`Failed to connect to auth server, retrying in ${retryDelay/1000} seconds...`, {
|
||||||
|
attempt,
|
||||||
|
maxRetries,
|
||||||
|
error: (error as Error).message
|
||||||
|
});
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
|
} else {
|
||||||
|
logger.error('Failed to fetch auth server metadata after all retries', error as Error);
|
||||||
|
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
authMetadata = await authMetadataResponse.json();
|
|
||||||
logger.info('Successfully fetched auth server metadata', {
|
|
||||||
issuer: authMetadata.issuer,
|
|
||||||
authorizationEndpoint: authMetadata.authorization_endpoint,
|
|
||||||
tokenEndpoint: authMetadata.token_endpoint
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to fetch auth server metadata', error as Error);
|
|
||||||
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL });
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve resource metadata only (not auth endpoints)
|
// Serve resource metadata only (not auth endpoints)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue