Skip to content

Commit ba3c0f6

Browse files
committed
fix(auth): prevent concurrent token refresh and adding necessary logs
1 parent e369ff3 commit ba3c0f6

File tree

1 file changed

+66
-2
lines changed

1 file changed

+66
-2
lines changed

packages/core/src/auth/sso/ssoAccessTokenProvider.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)