@@ -59,6 +59,15 @@ export abstract class SsoAccessTokenProvider {
5959 private static logIfChanged = onceChanged ( ( s : string ) => getLogger ( ) . info ( s ) )
6060 private readonly className = 'SsoAccessTokenProvider'
6161
62+ /**
63+ * Prevents concurrent token refresh operations.
64+ * Maps tokenCacheKey to an in-flight refresh promise.
65+ */
66+ private static refreshPromises = new Map <
67+ string ,
68+ Promise < { token : SsoToken ; registration : ClientRegistration ; region : string ; startUrl : string } >
69+ > ( )
70+
6271 public static set authSource ( val : string ) {
6372 SsoAccessTokenProvider . _authSource = val
6473 }
@@ -98,6 +107,8 @@ export abstract class SsoAccessTokenProvider {
98107 }
99108
100109 public async getToken ( ) : Promise < SsoToken | undefined > {
110+ getLogger ( ) . warn ( `getToken: CALLED for ${ this . tokenCacheKey } ` )
111+
101112 const data = await this . cache . token . load ( this . tokenCacheKey )
102113 SsoAccessTokenProvider . logIfChanged (
103114 indent (
@@ -108,15 +119,43 @@ export abstract class SsoAccessTokenProvider {
108119 true
109120 )
110121 )
122+
111123 if ( ! data || ! isExpired ( data . token ) ) {
124+ getLogger ( ) . debug ( `getToken: token is valid, returning cached token (key=${ this . tokenCacheKey } )` )
112125 return data ?. token
113126 }
114127
128+ getLogger ( ) . info (
129+ `getToken: bearer token expired (expires at ${ data . token . expiresAt } ), attempting refresh (key=${ this . tokenCacheKey } )`
130+ )
131+
115132 if ( data . registration && ! isExpired ( data . registration ) && hasProps ( data . token , 'refreshToken' ) ) {
116- const refreshed = await this . refreshToken ( data . token , data . registration )
133+ getLogger ( ) . info ( `getToken: refresh token available, calling refreshToken() (key=${ this . tokenCacheKey } )` )
134+ // Check if a refresh is already in progress for this token
135+ const existingRefresh = SsoAccessTokenProvider . refreshPromises . get ( this . tokenCacheKey )
136+ if ( existingRefresh ) {
137+ getLogger ( ) . debug (
138+ 'SsoAccessTokenProvider: Token refresh already in progress, waiting for existing refresh'
139+ )
140+ const refreshed = await existingRefresh
141+ return refreshed . token
142+ }
117143
118- return refreshed . token
144+ // Start a new refresh and store the promise
145+ const refreshPromise = this . refreshToken ( data . token , data . registration )
146+ SsoAccessTokenProvider . refreshPromises . set ( this . tokenCacheKey , refreshPromise )
147+
148+ try {
149+ const refreshed = await refreshPromise
150+ return refreshed . token
151+ } finally {
152+ // Clean up the promise from the map once complete (success or failure)
153+ SsoAccessTokenProvider . refreshPromises . delete ( this . tokenCacheKey )
154+ }
119155 } else {
156+ getLogger ( ) . warn (
157+ `getToken: cannot refresh - registration expired or no refresh token available (key=${ this . tokenCacheKey } )`
158+ )
120159 await this . invalidate ( 'allCacheExpired' )
121160 }
122161 }
@@ -171,11 +210,29 @@ export abstract class SsoAccessTokenProvider {
171210 }
172211
173212 try {
213+ // TEST: Log when refresh starts WITH STACK TRACE
214+ const stack = new Error ( ) . stack
215+ ?. split ( '\n' )
216+ . slice ( 2 , 12 ) // Skip first 2 lines (Error + refreshToken itself), take next 10
217+ . map ( ( line ) => line . trim ( ) )
218+ . join ( '\n ' )
219+ getLogger ( ) . warn (
220+ `refreshToken: Starting OIDC API call for ${ this . tokenCacheKey } \n CALL STACK:\n ${ stack } `
221+ )
222+
174223 const clientInfo = selectFrom ( registration , 'clientId' , 'clientSecret' )
224+ getLogger ( ) . debug ( `refreshToken: calling OIDC createToken API (key=${ this . tokenCacheKey } )` )
175225 const response = await this . oidc . createToken ( { ...clientInfo , ...token , grantType : refreshGrantType } )
226+
227+ getLogger ( ) . warn ( `refreshToken: got response, now saving to cache...` )
228+
176229 const refreshed = this . formatToken ( response , registration )
230+ getLogger ( ) . debug ( `refreshToken: saving refreshed token to cache (key=${ this . tokenCacheKey } )` )
177231 await this . cache . token . save ( this . tokenCacheKey , refreshed )
178232
233+ getLogger ( ) . info (
234+ `refreshToken: token refresh successful (key=${ this . tokenCacheKey } , new expiry=${ response . expiresAt } )`
235+ )
179236 telemetry . aws_refreshCredentials . emit ( {
180237 result : 'Succeeded' ,
181238 requestId : response . requestId ,
@@ -184,6 +241,10 @@ export abstract class SsoAccessTokenProvider {
184241
185242 return refreshed
186243 } catch ( err ) {
244+ getLogger ( ) . error (
245+ `refreshToken: token refresh failed (key=${ this . tokenCacheKey } ): ${ getErrorMsg ( err as unknown as Error ) } `
246+ )
247+
187248 if ( err instanceof DiskCacheError ) {
188249 /**
189250 * Background:
@@ -197,6 +258,9 @@ export abstract class SsoAccessTokenProvider {
197258 * to the logs where the error was logged. Hopefully they can use this information to fix the issue,
198259 * or at least hint for them to provide the logs in a bug report.
199260 */
261+ getLogger ( ) . warn (
262+ `refreshToken: DiskCacheError during refresh, not invalidating session (key=${ this . tokenCacheKey } )`
263+ )
200264 void DiskCacheErrorMessage . instance . showMessageThrottled ( err )
201265 } else if ( ! isNetworkError ( err ) ) {
202266 const reason = getTelemetryReason ( err )
0 commit comments