Skip to content

Commit 287f12b

Browse files
fix(auth): network error may invalidate SSO token (#4582)
* auth: handle networking issues during sso validation Problem: We were having issues due to network issues, such as no internet connected, the SSO auth token would become invalid. Solution: We had some code that validated the token, but if it failed due to network issues it would invalidate it. Now, when we validate the token and there is a network error, we will keep the state as-is instead of setting the connection to invalid. The user will instead see an error message indicating network issues. Signed-off-by: Nikolas Komonen <[email protected]> * rename: getToken -> getSsoToken Imo this makes it easier to understand as I skim over the code since 'SSO' aligns it with with the other naming in this file. Signed-off-by: Nikolas Komonen <[email protected]> * refactor: remove unused parameter This parameter is not used anywhere Signed-off-by: Nikolas Komonen <[email protected]> * add test + remove duplicate code Signed-off-by: Nikolas Komonen <[email protected]> --------- Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 35e2f8a commit 287f12b

File tree

4 files changed

+53
-27
lines changed

4 files changed

+53
-27
lines changed

packages/core/src/auth/auth.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class Auth implements AuthService, ConnectionManager {
139139
private readonly store: ProfileStore,
140140
private readonly iamProfileProvider = CredentialsProviderManager.getInstance(),
141141
private readonly createSsoClient = SsoClient.create.bind(SsoClient),
142-
private readonly createTokenProvider = createFactoryFunction(SsoAccessTokenProvider)
142+
private readonly createSsoTokenProvider = createFactoryFunction(SsoAccessTokenProvider)
143143
) {}
144144

145145
#activeConnection: Mutable<StatefulConnection> | undefined
@@ -169,7 +169,7 @@ export class Auth implements AuthService, ConnectionManager {
169169
public async reauthenticate({ id }: Pick<Connection, 'id'>): Promise<Connection> {
170170
const profile = this.store.getProfileOrThrow(id)
171171
if (profile.type === 'sso') {
172-
const provider = this.getTokenProvider(id, profile)
172+
const provider = this.getSsoTokenProvider(id, profile)
173173
await this.authenticate(id, () => provider.createToken())
174174

175175
return this.getSsoConnection(id, profile)
@@ -184,14 +184,13 @@ export class Auth implements AuthService, ConnectionManager {
184184
public async useConnection({ id }: Pick<SsoConnection, 'id'>): Promise<SsoConnection>
185185
public async useConnection({ id }: Pick<IamConnection, 'id'>): Promise<IamConnection>
186186
public async useConnection({ id }: Pick<Connection, 'id'>): Promise<Connection> {
187+
await this.refreshConnectionState({ id })
188+
187189
const profile = this.store.getProfile(id)
188190
if (profile === undefined) {
189191
throw new Error(`Connection does not exist: ${id}`)
190192
}
191-
getLogger().info(`auth: validating connection before using it: ${id}`)
192-
const validated = await this.validateConnection(id, profile)
193-
const conn =
194-
validated.type === 'sso' ? this.getSsoConnection(id, validated) : this.getIamConnection(id, validated)
193+
const conn = profile.type === 'sso' ? this.getSsoConnection(id, profile) : this.getIamConnection(id, profile)
195194

196195
this.#activeConnection = conn
197196
this.#onDidChangeActiveConnection.fire(conn)
@@ -253,7 +252,7 @@ export class Auth implements AuthService, ConnectionManager {
253252
this.store,
254253
id,
255254
profile,
256-
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile))
255+
this.createSsoClient(profile.ssoRegion, this.getSsoTokenProvider(id, profile))
257256
)
258257
)
259258
.catch(err => {
@@ -276,7 +275,7 @@ export class Auth implements AuthService, ConnectionManager {
276275
}
277276

278277
const id = uuid.v4()
279-
const tokenProvider = this.getTokenProvider(id, {
278+
const tokenProvider = this.getSsoTokenProvider(id, {
280279
...profile,
281280
metadata: { connectionState: 'unauthenticated' },
282281
})
@@ -344,10 +343,10 @@ export class Auth implements AuthService, ConnectionManager {
344343
if (profile.type === 'iam') {
345344
throw new ToolkitError('Auth: Cannot force expire an IAM connection')
346345
}
347-
const provider = this.getTokenProvider(conn.id, profile)
346+
const provider = this.getSsoTokenProvider(conn.id, profile)
348347
await provider.invalidate()
349348
// updates the state of the connection
350-
await this.validateConnection(conn.id, profile)
349+
await this.refreshConnectionState(conn)
351350
}
352351

353352
public async getConnection(connection: Pick<Connection, 'id'>): Promise<Connection | undefined> {
@@ -460,7 +459,7 @@ export class Auth implements AuthService, ConnectionManager {
460459
const profile = this.store.getProfileOrThrow(id)
461460

462461
if (profile.type === 'sso') {
463-
const provider = this.getTokenProvider(id, profile)
462+
const provider = this.getSsoTokenProvider(id, profile)
464463
const client = this.createSsoClient(profile.ssoRegion, provider)
465464

466465
if (opt?.skipGlobalLogout !== true) {
@@ -506,7 +505,7 @@ export class Auth implements AuthService, ConnectionManager {
506505
private async validateConnection<T extends Profile>(id: Connection['id'], profile: StoredProfile<T>) {
507506
const runCheck = async () => {
508507
if (profile.type === 'sso') {
509-
const provider = this.getTokenProvider(id, profile)
508+
const provider = this.getSsoTokenProvider(id, profile)
510509
if ((await provider.getToken()) === undefined) {
511510
getLogger().info(`auth: Connection is not valid: ${id} `)
512511
return this.updateConnectionState(id, 'invalid')
@@ -540,10 +539,13 @@ export class Auth implements AuthService, ConnectionManager {
540539
}
541540
}
542541

543-
return runCheck().catch(err => this.handleValidationError(id, err))
542+
return runCheck().catch(err => this.handleSsoTokenError(id, err))
544543
}
545544

546-
private async handleValidationError(id: Connection['id'], err: unknown) {
545+
private async handleSsoTokenError(id: Connection['id'], err: unknown) {
546+
// Bubble-up networking issues so we don't treat the session as invalid
547+
this.throwOnNetworkError(err)
548+
547549
this.#validationErrors.set(id, UnknownError.cast(err))
548550
getLogger().info(`auth: Handling validation error of connection: ${id}`)
549551
return this.updateConnectionState(id, 'invalid')
@@ -579,7 +581,7 @@ export class Auth implements AuthService, ConnectionManager {
579581
throw new Error(`Source profile for "${id}" is not an SSO profile`)
580582
}
581583

582-
const tokenProvider = this.getTokenProvider(profile.ssoSession, sourceProfile)
584+
const tokenProvider = this.getSsoTokenProvider(profile.ssoSession, sourceProfile)
583585
const credentialsProvider = new SsoCredentialsProvider(
584586
fromString(id),
585587
this.createSsoClient(sourceProfile.ssoRegion, tokenProvider),
@@ -630,7 +632,7 @@ export class Auth implements AuthService, ConnectionManager {
630632
: [identifier, createSsoProfile(region, startUrl, scopesCodeCatalyst)]
631633
}
632634

633-
private getTokenProvider(id: Connection['id'], profile: StoredProfile<SsoProfile>) {
635+
private getSsoTokenProvider(id: Connection['id'], profile: StoredProfile<SsoProfile>) {
634636
// XXX: Use the token created by Dev Environments if and only if the profile is strictly
635637
// for CodeCatalyst, as indicated by its set of scopes. A consequence of these semantics is
636638
// that any profile will be coerced to use this token if that profile exclusively contains
@@ -642,7 +644,7 @@ export class Auth implements AuthService, ConnectionManager {
642644

643645
const tokenIdentifier = shouldUseSoftwareStatement ? this.detectSsoSessionNameForCodeCatalyst() : id
644646

645-
return this.createTokenProvider(
647+
return this.createSsoTokenProvider(
646648
{
647649
identifier: tokenIdentifier,
648650
startUrl: profile.startUrl,
@@ -671,7 +673,7 @@ export class Auth implements AuthService, ConnectionManager {
671673
id: Connection['id'],
672674
profile: StoredProfile<SsoProfile>
673675
): SsoConnection & StatefulConnection {
674-
const provider = this.getTokenProvider(id, profile)
676+
const provider = this.getSsoTokenProvider(id, profile)
675677

676678
return {
677679
id,
@@ -692,7 +694,7 @@ export class Auth implements AuthService, ConnectionManager {
692694

693695
return result
694696
} catch (err) {
695-
await this.handleValidationError(id, err)
697+
await this.handleSsoTokenError(id, err)
696698
throw err
697699
}
698700
}
@@ -717,18 +719,29 @@ export class Auth implements AuthService, ConnectionManager {
717719
private async _getToken(id: Connection['id'], provider: SsoAccessTokenProvider): Promise<SsoToken> {
718720
const token = await provider.getToken().catch(err => {
719721
// Bubble-up networking issues so we don't treat the session as invalid
720-
if (isNetworkError(err)) {
721-
throw new ToolkitError('Failed to refresh connection due to networking issues', {
722-
cause: err,
723-
})
724-
}
722+
this.throwOnNetworkError(err)
725723

726724
this.#validationErrors.set(id, err)
727725
})
728726

729727
return token ?? this.handleInvalidCredentials(id, () => provider.createToken())
730728
}
731729

730+
/**
731+
* Auth processes can fail if there are network issues, and we do not
732+
* want to intepret these failures as invalid/expired auth tokens.
733+
*
734+
* We use this to check if the given error is network related and then
735+
* throw, expecting the caller to not change the state of the connection.
736+
*/
737+
private throwOnNetworkError(e: unknown) {
738+
if (isNetworkError(e)) {
739+
throw new ToolkitError('Failed to update connection due to networking issues', {
740+
cause: e,
741+
})
742+
}
743+
}
744+
732745
private readonly getCredentials = keyedDebounce(this._getCredentials.bind(this))
733746
private async _getCredentials(id: Connection['id'], provider: CredentialsProvider): Promise<Credentials> {
734747
const credentials = await this.getCachedCredentials(provider)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ export class SsoAccessTokenProvider {
108108
}
109109
}
110110

111-
public async createToken(identityProvider?: (token: SsoToken) => Promise<string>): Promise<SsoToken> {
111+
public async createToken(): Promise<SsoToken> {
112112
const access = await this.runFlow()
113-
const identity = (await identityProvider?.(access.token)) ?? this.tokenCacheKey
113+
const identity = this.tokenCacheKey
114114
await this.cache.token.save(identity, access)
115115
await setSessionCreationDate(this.tokenCacheKey, new Date())
116116

packages/core/src/shared/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ export class PermissionsError extends ToolkitError {
527527
}
528528
}
529529

530-
export function isNetworkError(err?: unknown) {
530+
export function isNetworkError(err?: unknown): err is Error & { code: string } {
531531
if (!hasCode(err)) {
532532
return false
533533
}

packages/core/src/test/credentials/auth.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,19 @@ describe('Auth', function () {
240240
assert.strictEqual(actual.cause, expected)
241241
assert.strictEqual(auth.getConnectionState(conn), 'valid')
242242
})
243+
244+
it('connection is not invalidated when networking issue during connection refresh', async function () {
245+
const networkError = new ToolkitError('test', { code: 'ETIMEDOUT' })
246+
const expectedError = new ToolkitError('Failed to update connection due to networking issues', {
247+
cause: networkError,
248+
})
249+
const conn = await auth.createConnection(ssoProfile)
250+
auth.getTestTokenProvider(conn)?.getToken.rejects(networkError)
251+
const actual = await auth.refreshConnectionState(conn).catch(e => e)
252+
assert.ok(actual instanceof ToolkitError)
253+
assert.deepStrictEqual(actual, expectedError)
254+
assert.strictEqual(auth.getConnectionState(conn), 'valid')
255+
})
243256
})
244257

245258
describe('Linked Connections', function () {

0 commit comments

Comments
 (0)