@@ -9,11 +9,32 @@ import { logger } from '../utils/logger.js';
99 * Used when the MCP server is running in 'separate' mode.
1010 */
1111export 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 ;
0 commit comments