Skip to content

Commit 0ddbf4a

Browse files
authored
fix(amazonq): handle existing Flare connection when migrating SSO connections (#7363)
## Problem Auth migration to LSP is not handled gracefully when a user downgrades and upgrades to auth on LSP multiple times, causing users to be logged out if they upgrade a second time ## Solution In the auth migration script, call the LSP identity server to check if a token is available. If the token is available, don't migrate the auth connection. If no token is available, migrate. Added unit tests for the case. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent e411922 commit 0ddbf4a

File tree

3 files changed

+93
-45
lines changed

3 files changed

+93
-45
lines changed

packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ describe('AuthUtil', async function () {
225225
})
226226

227227
describe('migrateSsoConnectionToLsp', function () {
228+
let mockLspAuth: any
228229
let memento: any
229230
let cacheDir: string
230231
let fromRegistrationFile: string
@@ -250,6 +251,9 @@ describe('AuthUtil', async function () {
250251
sinon.stub(mementoUtils, 'getEnvironmentSpecificMemento').returns(memento)
251252
sinon.stub(cache, 'getCacheDir').returns(cacheDir)
252253

254+
mockLspAuth = (auth as any).lspAuth
255+
mockLspAuth.getSsoToken.resolves(undefined)
256+
253257
fromTokenFile = cache.getTokenCacheFile(cacheDir, 'profile1')
254258
const registrationKey = {
255259
startUrl: validProfile.startUrl,
@@ -269,6 +273,27 @@ describe('AuthUtil', async function () {
269273
sinon.restore()
270274
})
271275

276+
it('skips migration if LSP token exists', async function () {
277+
memento.get.returns({ profile1: validProfile })
278+
mockLspAuth.getSsoToken.resolves({ token: 'valid-token' })
279+
280+
await auth.migrateSsoConnectionToLsp('test-client')
281+
282+
assert.ok(memento.update.calledWith('auth.profiles', undefined))
283+
assert.ok(!auth.session.updateProfile?.called)
284+
})
285+
286+
it('proceeds with migration if LSP token check throws', async function () {
287+
memento.get.returns({ profile1: validProfile })
288+
mockLspAuth.getSsoToken.rejects(new Error('Token check failed'))
289+
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()
290+
291+
await auth.migrateSsoConnectionToLsp('test-client')
292+
293+
assert.ok(updateProfileStub.calledOnce)
294+
assert.ok(memento.update.calledWith('auth.profiles', undefined))
295+
})
296+
272297
it('migrates valid SSO connection', async function () {
273298
memento.get.returns({ profile1: validProfile })
274299

packages/core/src/codewhisperer/util/authUtil.ts

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { getEnvironmentSpecificMemento } from '../../shared/utilities/mementos'
3939
import { getCacheDir, getFlareCacheFileName, getRegistrationCacheFile, getTokenCacheFile } from '../../auth/sso/cache'
4040
import { notifySelectDeveloperProfile } from '../region/utils'
4141
import { once } from '../../shared/utilities/functionUtils'
42+
import { CancellationTokenSource, SsoTokenSourceKind } from '@aws/language-server-runtimes/server-interface'
4243

4344
const localize = nls.loadMessageBundle()
4445

@@ -64,6 +65,8 @@ export interface IAuthProvider {
6465
*/
6566
export class AuthUtil implements IAuthProvider {
6667
public readonly profileName = VSCODE_EXTENSION_ID.amazonq
68+
protected logger = getLogger('amazonqAuth')
69+
6770
public readonly regionProfileManager: RegionProfileManager
6871

6972
// IAM login currently not supported
@@ -277,7 +280,7 @@ export class AuthUtil implements IAuthProvider {
277280
}
278281

279282
private async cacheChangedHandler(event: cacheChangedEvent) {
280-
getLogger().debug(`Auth: Cache change event received: ${event}`)
283+
this.logger.debug(`Cache change event received: ${event}`)
281284
if (event === 'delete') {
282285
await this.logout()
283286
} else if (event === 'create') {
@@ -291,7 +294,7 @@ export class AuthUtil implements IAuthProvider {
291294
await this.lspAuth.updateBearerToken(params!)
292295
return
293296
} else {
294-
getLogger().info(`codewhisperer: connection changed to ${e.state}`)
297+
this.logger.info(`codewhisperer: connection changed to ${e.state}`)
295298
await this.refreshState(e.state)
296299
}
297300
}
@@ -402,58 +405,77 @@ export class AuthUtil implements IAuthProvider {
402405

403406
if (!profiles) {
404407
return
405-
} else {
406-
getLogger().info(`codewhisperer: checking for old SSO connections`)
407-
for (const [id, p] of Object.entries(profiles)) {
408-
if (p.type === 'sso' && hasExactScopes(p.scopes ?? [], amazonQScopes)) {
409-
toImport = p
410-
profileId = id
411-
if (p.metadata.connectionState === 'valid') {
412-
break
413-
}
414-
}
415-
}
408+
}
416409

417-
if (toImport && profileId) {
418-
getLogger().info(`codewhisperer: migrating SSO connection to LSP identity server...`)
410+
try {
411+
// Try go get token from LSP auth. If available, skip migration and delete old auth profile
412+
const token = await this.lspAuth.getSsoToken(
413+
{
414+
kind: SsoTokenSourceKind.IamIdentityCenter,
415+
profileName: this.profileName,
416+
},
417+
false,
418+
new CancellationTokenSource().token
419+
)
420+
if (token) {
421+
this.logger.info('existing LSP auth connection found. Skipping migration')
422+
await memento.update(key, undefined)
423+
return
424+
}
425+
} catch {
426+
this.logger.info('unable to get token from LSP auth, proceeding migration')
427+
}
419428

420-
const registrationKey = {
421-
startUrl: toImport.startUrl,
422-
region: toImport.ssoRegion,
423-
scopes: amazonQScopes,
429+
this.logger.info('checking for old SSO connections')
430+
for (const [id, p] of Object.entries(profiles)) {
431+
if (p.type === 'sso' && hasExactScopes(p.scopes ?? [], amazonQScopes)) {
432+
toImport = p
433+
profileId = id
434+
if (p.metadata.connectionState === 'valid') {
435+
break
424436
}
437+
}
438+
}
425439

426-
await this.session.updateProfile(registrationKey)
440+
if (toImport && profileId) {
441+
this.logger.info('migrating SSO connection to LSP identity server...')
427442

428-
const cacheDir = getCacheDir()
443+
const registrationKey = {
444+
startUrl: toImport.startUrl,
445+
region: toImport.ssoRegion,
446+
scopes: amazonQScopes,
447+
}
429448

430-
const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey)
431-
const toRegistrationFile = path.join(
432-
cacheDir,
433-
getFlareCacheFileName(
434-
JSON.stringify({
435-
region: toImport.ssoRegion,
436-
startUrl: toImport.startUrl,
437-
tool: clientName,
438-
})
439-
)
440-
)
449+
await this.session.updateProfile(registrationKey)
441450

442-
const fromTokenFile = getTokenCacheFile(cacheDir, profileId)
443-
const toTokenFile = path.join(cacheDir, getFlareCacheFileName(this.profileName))
451+
const cacheDir = getCacheDir()
444452

445-
try {
446-
await fs.rename(fromRegistrationFile, toRegistrationFile)
447-
await fs.rename(fromTokenFile, toTokenFile)
448-
getLogger().debug('Successfully renamed registration and token files')
449-
} catch (err) {
450-
getLogger().error(`Failed to rename files during migration: ${err}`)
451-
throw err
452-
}
453-
454-
await memento.update(key, undefined)
455-
getLogger().info(`codewhisperer: successfully migrated SSO connection to LSP identity server`)
453+
const fromRegistrationFile = getRegistrationCacheFile(cacheDir, registrationKey)
454+
const toRegistrationFile = path.join(
455+
cacheDir,
456+
getFlareCacheFileName(
457+
JSON.stringify({
458+
region: toImport.ssoRegion,
459+
startUrl: toImport.startUrl,
460+
tool: clientName,
461+
})
462+
)
463+
)
464+
465+
const fromTokenFile = getTokenCacheFile(cacheDir, profileId)
466+
const toTokenFile = path.join(cacheDir, getFlareCacheFileName(this.profileName))
467+
468+
try {
469+
await fs.rename(fromRegistrationFile, toRegistrationFile)
470+
await fs.rename(fromTokenFile, toTokenFile)
471+
this.logger.debug('Successfully renamed registration and token files')
472+
} catch (err) {
473+
this.logger.error(`Failed to rename files during migration: ${err}`)
474+
throw err
456475
}
476+
477+
this.logger.info('successfully migrated SSO connection to LSP identity server')
478+
await memento.update(key, undefined)
457479
}
458480
}
459481
}

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type LogTopic =
2121
| 'nextEditPrediction'
2222
| 'resourceCache'
2323
| 'telemetry'
24+
| 'amazonqAuth'
2425

2526
class ErrorLog {
2627
constructor(

0 commit comments

Comments
 (0)