Skip to content

Commit 4771c6e

Browse files
committed
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
1 parent 03deb88 commit 4771c6e

File tree

2 files changed

+92
-17
lines changed

2 files changed

+92
-17
lines changed

src/auth/external-verifier.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,32 @@ import { logger } from '../utils/logger.js';
99
* Used when the MCP server is running in 'separate' mode.
1010
*/
1111
export class ExternalAuthVerifier implements OAuthTokenVerifier {
12+
// Token validation cache: token -> { authInfo, expiresAt }
13+
private tokenCache = new Map<string, { authInfo: AuthInfo; expiresAt: number }>();
14+
15+
// Default cache TTL: 60 seconds (conservative for security)
16+
private readonly defaultCacheTTL = 60 * 1000; // milliseconds
17+
1218
/**
1319
* Creates a new external auth verifier.
1420
* @param authServerUrl Base URL of the external authorization server
1521
*/
16-
constructor(private authServerUrl: string) {}
22+
constructor(private authServerUrl: string) {
23+
// Periodically clean up expired cache entries
24+
setInterval(() => this.cleanupCache(), 60 * 1000); // Every minute
25+
}
26+
27+
/**
28+
* Removes expired entries from the cache.
29+
*/
30+
private cleanupCache(): void {
31+
const now = Date.now();
32+
for (const [token, entry] of this.tokenCache.entries()) {
33+
if (entry.expiresAt <= now) {
34+
this.tokenCache.delete(token);
35+
}
36+
}
37+
}
1738

1839
/**
1940
* Verifies an access token by calling the external auth server's introspection endpoint.
@@ -22,6 +43,16 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
2243
* @throws InvalidTokenError if the token is invalid or expired
2344
*/
2445
async verifyAccessToken(token: string): Promise<AuthInfo> {
46+
// Check cache first
47+
const cached = this.tokenCache.get(token);
48+
if (cached && cached.expiresAt > Date.now()) {
49+
logger.debug('Token validation cache hit', {
50+
token: token.substring(0, 8) + '...',
51+
expiresIn: Math.round((cached.expiresAt - Date.now()) / 1000) + 's'
52+
});
53+
return cached.authInfo;
54+
}
55+
2556
try {
2657
// Token introspection is OAuth 2.0 standard (RFC 7662) for validating tokens
2758
// The auth server checks if the token is valid and returns metadata about it
@@ -32,6 +63,10 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
3263
});
3364

3465
if (!response.ok) {
66+
// On 401/403, the token might be invalid - don't cache
67+
if (response.status === 401 || response.status === 403) {
68+
this.tokenCache.delete(token); // Clear any stale cache
69+
}
3570
logger.error('Token introspection request failed', undefined, {
3671
status: response.status,
3772
statusText: response.statusText,
@@ -60,7 +95,7 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
6095
});
6196
}
6297

63-
return {
98+
const authInfo: AuthInfo = {
6499
token,
65100
clientId: data.client_id || 'unknown',
66101
scopes: data.scope?.split(' ') || [], // Empty array if no scopes specified (permissive)
@@ -73,6 +108,26 @@ export class ExternalAuthVerifier implements OAuthTokenVerifier {
73108
aud: data.aud,
74109
},
75110
};
111+
112+
// Cache the successful introspection result
113+
// Use token expiration if available, otherwise default TTL
114+
const cacheDuration = data.exp
115+
? Math.min((data.exp * 1000) - Date.now(), this.defaultCacheTTL)
116+
: this.defaultCacheTTL;
117+
118+
if (cacheDuration > 0) {
119+
this.tokenCache.set(token, {
120+
authInfo,
121+
expiresAt: Date.now() + cacheDuration
122+
});
123+
124+
logger.debug('Token validation cached', {
125+
token: token.substring(0, 8) + '...',
126+
cacheDuration: Math.round(cacheDuration / 1000) + 's'
127+
});
128+
}
129+
130+
return authInfo;
76131
} catch (error) {
77132
if (error instanceof InvalidTokenError) {
78133
throw error;

src/index.ts

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -175,23 +175,43 @@ if (AUTH_MODE === 'integrated') {
175175
authServerUrl: AUTH_SERVER_URL
176176
});
177177

178-
// Fetch metadata from external auth server
178+
// Fetch metadata from external auth server with retry logic
179179
let authMetadata;
180-
try {
181-
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
182-
if (!authMetadataResponse.ok) {
183-
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
180+
const maxRetries = 5;
181+
const retryDelay = 3000; // 3 seconds
182+
183+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
184+
try {
185+
logger.info(`Attempting to connect to auth server (attempt ${attempt}/${maxRetries})`, {
186+
authServerUrl: AUTH_SERVER_URL
187+
});
188+
189+
const authMetadataResponse = await fetch(`${AUTH_SERVER_URL}/.well-known/oauth-authorization-server`);
190+
if (!authMetadataResponse.ok) {
191+
throw new Error(`Failed to fetch auth server metadata: ${authMetadataResponse.status} ${authMetadataResponse.statusText}`);
192+
}
193+
authMetadata = await authMetadataResponse.json();
194+
logger.info('Successfully fetched auth server metadata', {
195+
issuer: authMetadata.issuer,
196+
authorizationEndpoint: authMetadata.authorization_endpoint,
197+
tokenEndpoint: authMetadata.token_endpoint
198+
});
199+
break; // Success, exit retry loop
200+
201+
} catch (error) {
202+
if (attempt < maxRetries) {
203+
logger.info(`Failed to connect to auth server, retrying in ${retryDelay/1000} seconds...`, {
204+
attempt,
205+
maxRetries,
206+
error: (error as Error).message
207+
});
208+
await new Promise(resolve => setTimeout(resolve, retryDelay));
209+
} else {
210+
logger.error('Failed to fetch auth server metadata after all retries', error as Error);
211+
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL });
212+
process.exit(1);
213+
}
184214
}
185-
authMetadata = await authMetadataResponse.json();
186-
logger.info('Successfully fetched auth server metadata', {
187-
issuer: authMetadata.issuer,
188-
authorizationEndpoint: authMetadata.authorization_endpoint,
189-
tokenEndpoint: authMetadata.token_endpoint
190-
});
191-
} catch (error) {
192-
logger.error('Failed to fetch auth server metadata', error as Error);
193-
logger.error('Make sure the auth server is running at', undefined, { authServerUrl: AUTH_SERVER_URL });
194-
process.exit(1);
195215
}
196216

197217
// Serve resource metadata only (not auth endpoints)

0 commit comments

Comments
 (0)