Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ab47a46
Add webviews for Amazon Q IAM credentials option and form
liramon1 Jun 5, 2025
eeebd0b
Implement IAM setup function
liramon1 Jun 6, 2025
61c1138
Make AuthUtils session switch between SsoLogin, IamLogin, and undefined
liramon1 Jun 9, 2025
146a95f
Start implementing IamLogin class
liramon1 Jun 10, 2025
eb0be34
Remove iamSessions from profiles
liramon1 Jun 11, 2025
a241b1c
Implement updateIamProfile and change STS references to IAM
liramon1 Jun 13, 2025
e923bad
Implement _getIamCredential and its callers
liramon1 Jun 13, 2025
2dabb43
fix(amazonq): delete iam profile when logout
yuxianrz Jun 16, 2025
ff181f4
start modifying auth2 consumers
liramon1 Jun 16, 2025
f61bcee
Merge remote-tracking branch 'origin/feature/flare-mega' into feature…
liramon1 Jun 16, 2025
eb3526a
undo unnecessary changes
liramon1 Jun 17, 2025
849006b
undo more unnecessary changes
liramon1 Jun 18, 2025
1968d41
fix logout bug
liramon1 Jun 18, 2025
06c5ad8
Fix bug where profile failed to be retrieved after signing out and ba…
liramon1 Jun 19, 2025
60ffc92
Fix region profile selector not triggering
liramon1 Jun 19, 2025
e1cfdc3
Add support for IAM credentials to region profile manager
liramon1 Jun 19, 2025
422d085
feat: remember iam access key
yuxianrz Jun 19, 2025
424a1af
Revert unnecessary region profile changes
liramon1 Jun 20, 2025
9dbe490
Add limited IAM support for inline chat
liramon1 Jun 20, 2025
f044eb6
Revert CredentialChangedKind for backwards compatibility
liramon1 Jun 20, 2025
7506ecc
fix: fix missing autofill after disabling extension
yuxianrz Jun 25, 2025
ce07bd2
Re-add session restore for IAM
liramon1 Jun 30, 2025
4d2b8b0
Add session token input to login flow
liramon1 Jul 1, 2025
5830716
Add session tokens to auth2 logic
liramon1 Jul 1, 2025
ef3745b
Replace profile deletion with iam invalidation
liramon1 Jul 1, 2025
1ec8508
Rename loginOnInvalidToken
liramon1 Jul 1, 2025
0382876
Rename getIamCredential options
liramon1 Jul 2, 2025
f45c92a
Remove assumeRole option
liramon1 Jul 2, 2025
b730331
feat: enable sts invalidation
yuxianrz Jul 2, 2025
6b5689a
Remove unused imports
liramon1 Jul 2, 2025
8ff6754
Add role ARN field to IAM credentials form
liramon1 Jul 3, 2025
e7f5d2a
feat: enable sts autorefresh
yuxianrz Jul 3, 2025
ee2f240
Update error codes in getIamCredential
liramon1 Jul 3, 2025
38d1389
Make secret key field a password input
liramon1 Jul 3, 2025
fd060c1
Autofill role arn field
liramon1 Jul 3, 2025
84f8310
fix: refreshed sts credentials can be used
yuxianrz Jul 3, 2025
1cd7745
Fix error statements
liramon1 Jul 7, 2025
dbbb4d6
Make errorNotification use show error instead of info message
liramon1 Jul 7, 2025
a4edd1f
Fix 'Connecting to IAM' UI bug
liramon1 Jul 7, 2025
423a73e
Revert empty profile
liramon1 Jul 9, 2025
c1dd5cb
fix unit tests
yuxianrz Jul 9, 2025
08b5856
add revert empty profile
yuxianrz Jul 9, 2025
3aaf863
Revert "Re-add session restore for IAM"
yuxianrz Jul 9, 2025
3336ec9
revert changes in lanuch.json and workspace
yuxianrz Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aws-toolkit-vscode.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
"settings": {
"typescript.tsdk": "node_modules/typescript/lib",
},
}
}
2 changes: 1 addition & 1 deletion packages/amazonq/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"env": {
"SSMDOCUMENT_LANGUAGESERVER_PORT": "6010",
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080"
"WEBPACK_DEVELOPER_SERVER": "http://localhost:8080",
// Below allows for overrides used during development
// "__AMAZONQLSP_PATH": "${workspaceFolder}/../../../language-servers/app/aws-lsp-codewhisperer-runtimes/out/agent-standalone.js",
// "__AMAZONQLSP_UI": "${workspaceFolder}/../../../language-servers/chat-client/build/amazonq-ui.js"
Expand Down
4 changes: 2 additions & 2 deletions packages/amazonq/src/extensionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
const state = AuthUtil.instance.getAuthState()

if (AuthUtil.instance.isConnected() && !(AuthUtil.instance.isSsoSession() || isSageMaker())) {
getLogger().error('Current Amazon Q connection is not SSO')
if (AuthUtil.instance.isConnected() && !(AuthUtil.instance.isSsoSession() || AuthUtil.instance.isIamSession() || isSageMaker())) {
getLogger().error('Current Amazon Q connection is not SSO nor IAM')
}

return {
Expand Down
15 changes: 13 additions & 2 deletions packages/amazonq/src/inlineChat/provider/inlineChatProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export class InlineChatProvider {
private async generateResponse(
triggerPayload: TriggerPayload & { projectContextQueryLatencyMs?: number },
triggerID: string
) {
): Promise<GenerateAssistantResponseCommandOutput | undefined> {
const triggerEvent = this.triggerEventsStorage.getTriggerEvent(triggerID)
if (triggerEvent === undefined) {
return
Expand Down Expand Up @@ -182,7 +182,18 @@ export class InlineChatProvider {
let response: GenerateAssistantResponseCommandOutput | undefined = undefined
session.createNewTokenSource()
try {
response = await session.chatSso(request)
if (AuthUtil.instance.isSsoSession()) {
response = await session.chatSso(request)
} else {
// Call sendMessage because Q Developer Streaming Client does not have generateAssistantResponse
const { sendMessageResponse, ...rest } = await session.chatIam(request)
// Convert sendMessageCommandOutput to GenerateAssistantResponseCommandOutput
response = {
generateAssistantResponseResponse: sendMessageResponse,
conversationId: session.sessionIdentifier,
...rest
}
}
getLogger().info(
`response to tab: ${tabID} conversationID: ${session.sessionIdentifier} requestID: ${response.$metadata.requestId} metadata: %O`,
response.$metadata
Expand Down
11 changes: 10 additions & 1 deletion packages/amazonq/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ export async function startLanguageServer(
},
credentials: {
providesBearerToken: true,
// Add IAM credentials support
providesIamCredentials: true,
supportsAssumeRole: true,
},
},
/**
Expand Down Expand Up @@ -211,9 +214,10 @@ export async function startLanguageServer(

/** All must be setup before {@link AuthUtil.restore} otherwise they may not trigger when expected */
AuthUtil.instance.regionProfileManager.onDidChangeRegionProfile(async () => {
const activeProfile = AuthUtil.instance.regionProfileManager.activeRegionProfile
void pushConfigUpdate(client, {
type: 'profile',
profileArn: AuthUtil.instance.regionProfileManager.activeRegionProfile?.arn,
profileArn: activeProfile?.arn,
})
})

Expand Down Expand Up @@ -286,6 +290,11 @@ async function postStartLanguageServer(
sso: {
startUrl: AuthUtil.instance.connection?.startUrl,
},
// Add IAM credentials metadata
iam: {
region: AuthUtil.instance.connection?.region,
accesskey: AuthUtil.instance.connection?.accessKey,
},
}
})

Expand Down
2 changes: 1 addition & 1 deletion packages/amazonq/test/e2e/amazonq/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export async function loginToIdC() {
)
}

await AuthUtil.instance.login(startUrl, region)
await AuthUtil.instance.login_sso(startUrl, region)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ describe('RegionProfileManager', async function () {

async function setupConnection(type: 'builderId' | 'idc') {
if (type === 'builderId') {
await AuthUtil.instance.login(constants.builderIdStartUrl, region)
await AuthUtil.instance.login_sso(constants.builderIdStartUrl, region)
assert.ok(AuthUtil.instance.isSsoSession())
assert.ok(AuthUtil.instance.isBuilderIdConnection())
} else if (type === 'idc') {
await AuthUtil.instance.login(enterpriseSsoStartUrl, region)
await AuthUtil.instance.login_sso(enterpriseSsoStartUrl, region)
assert.ok(AuthUtil.instance.isSsoSession())
assert.ok(AuthUtil.instance.isIdcConnection())
}
Expand Down
77 changes: 47 additions & 30 deletions packages/amazonq/test/unit/codewhisperer/util/authUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ describe('AuthUtil', async function () {

describe('Auth state', function () {
it('login with BuilderId', async function () {
await auth.login(constants.builderIdStartUrl, constants.builderIdRegion)
await auth.login_sso(constants.builderIdStartUrl, constants.builderIdRegion)
assert.ok(auth.isConnected())
assert.ok(auth.isBuilderIdConnection())
})

it('login with IDC', async function () {
await auth.login('https://example.awsapps.com/start', 'us-east-1')
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')
assert.ok(auth.isConnected())
assert.ok(auth.isIdcConnection())
})

it('identifies internal users', async function () {
await auth.login(constants.internalStartUrl, 'us-east-1')
await auth.login_sso(constants.internalStartUrl, 'us-east-1')
assert.ok(auth.isInternalAmazonUser())
})

Expand All @@ -55,7 +55,7 @@ describe('AuthUtil', async function () {

describe('Token management', function () {
it('can get token when connected with SSO', async function () {
await auth.login(constants.builderIdStartUrl, constants.builderIdRegion)
await auth.login_sso(constants.builderIdStartUrl, constants.builderIdRegion)
const token = await auth.getToken()
assert.ok(token)
})
Expand All @@ -68,14 +68,14 @@ describe('AuthUtil', async function () {

describe('getTelemetryMetadata', function () {
it('returns valid metadata for BuilderId connection', async function () {
await auth.login(constants.builderIdStartUrl, constants.builderIdRegion)
await auth.login_sso(constants.builderIdStartUrl, constants.builderIdRegion)
const metadata = await auth.getTelemetryMetadata()
assert.strictEqual(metadata.credentialSourceId, 'awsId')
assert.strictEqual(metadata.credentialStartUrl, constants.builderIdStartUrl)
})

it('returns valid metadata for IDC connection', async function () {
await auth.login('https://example.awsapps.com/start', 'us-east-1')
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')
const metadata = await auth.getTelemetryMetadata()
assert.strictEqual(metadata.credentialSourceId, 'iamIdentityCenter')
assert.strictEqual(metadata.credentialStartUrl, 'https://example.awsapps.com/start')
Expand All @@ -96,37 +96,38 @@ describe('AuthUtil', async function () {
})

it('returns BuilderId forms when using BuilderId', async function () {
await auth.login(constants.builderIdStartUrl, constants.builderIdRegion)
await auth.login_sso(constants.builderIdStartUrl, constants.builderIdRegion)
const forms = await auth.getAuthFormIds()
assert.deepStrictEqual(forms, ['builderIdCodeWhisperer'])
})

it('returns IDC forms when using IDC without SSO account access', async function () {
const session = (auth as any).session
sinon.stub(session, 'getProfile').resolves({
session && sinon.stub(session, 'getProfile').resolves({
ssoSession: {
settings: {
sso_registration_scopes: ['codewhisperer:*'],
},
},
})

await auth.login('https://example.awsapps.com/start', 'us-east-1')
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')
const forms = await auth.getAuthFormIds()
assert.deepStrictEqual(forms, ['identityCenterCodeWhisperer'])
})

it('returns IDC forms with explorer when using IDC with SSO account access', async function () {
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')
const session = (auth as any).session
sinon.stub(session, 'getProfile').resolves({

session && sinon.stub(session, 'getProfile').resolves({
ssoSession: {
settings: {
sso_registration_scopes: ['codewhisperer:*', 'sso:account:access'],
},
},
})

await auth.login('https://example.awsapps.com/start', 'us-east-1')
const forms = await auth.getAuthFormIds()
assert.deepStrictEqual(forms.sort(), ['identityCenterCodeWhisperer', 'identityCenterExplorer'].sort())
})
Expand Down Expand Up @@ -178,7 +179,7 @@ describe('AuthUtil', async function () {
})

it('updates bearer token when state is refreshed', async function () {
await auth.login(constants.builderIdStartUrl, 'us-east-1')
await auth.login_sso(constants.builderIdStartUrl, 'us-east-1')

await (auth as any).stateChangeHandler({ state: 'refreshed' })

Expand All @@ -187,7 +188,7 @@ describe('AuthUtil', async function () {
})

it('cleans up when connection expires', async function () {
await auth.login(constants.builderIdStartUrl, 'us-east-1')
await auth.login_sso(constants.builderIdStartUrl, 'us-east-1')

await (auth as any).stateChangeHandler({ state: 'expired' })

Expand All @@ -197,13 +198,15 @@ describe('AuthUtil', async function () {
it('deletes bearer token when disconnected', async function () {
await (auth as any).stateChangeHandler({ state: 'notConnected' })

assert.ok(mockLspAuth.deleteBearerToken.called)
if (auth.isSsoSession(auth.session)){
assert.ok(mockLspAuth.deleteBearerToken.called)
}
})

it('updates bearer token and restores profile on reconnection', async function () {
const restoreProfileSelectionSpy = sinon.spy(regionProfileManager, 'restoreProfileSelection')

await auth.login('https://example.awsapps.com/start', 'us-east-1')
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')

await (auth as any).stateChangeHandler({ state: 'connected' })

Expand All @@ -215,7 +218,7 @@ describe('AuthUtil', async function () {
const invalidateProfileSpy = sinon.spy(regionProfileManager, 'invalidateProfile')
const clearCacheSpy = sinon.spy(regionProfileManager, 'clearCache')

await auth.login('https://example.awsapps.com/start', 'us-east-1')
await auth.login_sso('https://example.awsapps.com/start', 'us-east-1')

await (auth as any).stateChangeHandler({ state: 'expired' })

Expand Down Expand Up @@ -280,12 +283,16 @@ describe('AuthUtil', async function () {
await auth.migrateSsoConnectionToLsp('test-client')

assert.ok(memento.update.calledWith('auth.profiles', undefined))
assert.ok(!auth.session.updateProfile?.called)
assert.ok(!auth.session?.updateProfile?.called)
})

it('proceeds with migration if LSP token check throws', async function () {
memento.get.returns({ profile1: validProfile })
mockLspAuth.getSsoToken.rejects(new Error('Token check failed'))

if (!(auth as any).session){
auth.session = new auth2.SsoLogin(auth.profileName, auth.lspAuth, auth.eventEmitter)
}
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()

await auth.migrateSsoConnectionToLsp('test-client')
Expand All @@ -297,22 +304,24 @@ describe('AuthUtil', async function () {
it('migrates valid SSO connection', async function () {
memento.get.returns({ profile1: validProfile })

const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()
if ((auth as any).session) {
const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()

await auth.migrateSsoConnectionToLsp('test-client')
await auth.migrateSsoConnectionToLsp('test-client')

assert.ok(updateProfileStub.calledOnce)
assert.ok(memento.update.calledWith('auth.profiles', undefined))
assert.ok(updateProfileStub.calledOnce)
assert.ok(memento.update.calledWith('auth.profiles', undefined))

const files = await fs.readdir(cacheDir)
assert.strictEqual(files.length, 2) // Should have both the token and registration file

// Verify file contents were preserved
const newFiles = files.map((f) => path.join(cacheDir, f[0]))
for (const file of newFiles) {
const content = await fs.readFileText(file)
const parsed = JSON.parse(content)
assert.ok(parsed.test === 'registration' || parsed.test === 'token')
const files = await fs.readdir(cacheDir)
assert.strictEqual(files.length, 2) // Should have both the token and registration file

// Verify file contents were preserved
const newFiles = files.map((f) => path.join(cacheDir, f[0]))
for (const file of newFiles) {
const content = await fs.readFileText(file)
const parsed = JSON.parse(content)
assert.ok(parsed.test === 'registration' || parsed.test === 'token')
}
}
})

Expand Down Expand Up @@ -351,6 +360,10 @@ describe('AuthUtil', async function () {
}
memento.get.returns(mockProfiles)

if (!(auth as any).session){
auth.session = new auth2.SsoLogin(auth.profileName, auth.lspAuth, auth.eventEmitter)
}

const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()

await auth.migrateSsoConnectionToLsp('test-client')
Expand All @@ -376,6 +389,10 @@ describe('AuthUtil', async function () {
}
memento.get.returns(mockProfiles)

if (!(auth as any).session){
auth.session = new auth2.SsoLogin(auth.profileName, auth.lspAuth, auth.eventEmitter)
}

const updateProfileStub = sinon.stub((auth as any).session, 'updateProfile').resolves()

await auth.migrateSsoConnectionToLsp('test-client')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('showConnectionPrompt', function () {
})

it('can select connect to AwsBuilderId', async function () {
sinon.stub(AuthUtil.instance, 'login').resolves()
sinon.stub(AuthUtil.instance, 'login_sso').resolves()

getTestWindow().onDidShowQuickPick(async (picker) => {
await picker.untilReady()
Expand All @@ -44,7 +44,7 @@ describe('showConnectionPrompt', function () {

it('connectToAwsBuilderId calls AuthUtil login with builderIdStartUrl', async function () {
sinon.stub(vscode.commands, 'executeCommand')
const loginStub = sinon.stub(AuthUtil.instance, 'login').resolves()
const loginStub = sinon.stub(AuthUtil.instance, 'login_sso').resolves()

await awsIdSignIn()

Expand Down
Loading
Loading