Skip to content

Commit 52f40c2

Browse files
authored
feat(auth): add console credentials sign-in option to login webview (#8401)
## Problem Users can use a beginner-friendly interface to authenticate with AWS Console credentials to obtain temporary credentials, especially for new AWS users. This GUI-based offers alternative to `aws login` command-line authentication. Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sign-in.html ## Solution - Add "Console credentials - recommended" option to login webview - Restrict profile name input to alphanumeric, underscore, and hyphen characters (following [profile name pattern](https://github.com/keenwilson/aws-toolkit-vscode/blob/89739bc176c28321f64cd672664014d1ddfed533/packages/core/src/auth/consoleSessionUtils.ts#L48)) - Show "Opening AWS sign-in in your default browser..." during authentication - Redirect to explorer view upon successful sign-in Note: - The UI flow follows the same pattern as IAM credentials setup, with these key differences: - Console credentials form takes profile name and region (optional) - IAM credentials form takes access key and secret key - Different telemetry emitted for credential source ID: - Console credentials: 'consoleCredentials' - IAM credentials: 'sharedCredentials' - Telemetry for credential source ID is tracked via aws/aws-toolkit-common#1108 - AWS CLI returns exit code 255 if browser-based authentication is not completed, this prevents partial/incomplete authentication states - Reuse `fromLoginCredentials` provider instance to prevent multiple credential resolution attempts and maintain consistent refresh behavior at [resolveProviderWithCancel](https://github.com/aws/aws-toolkit-vscode/blob/eb11eb59318ab83a1f609e472eab760ea38201d1/packages/core/src/auth/providers/sharedCredentialsProvider.ts#L256) in sharedCredentialsProvider ## UI Changes - Added "Console credentials - recommended" as first option in login selection <img width="1122" height="633" alt="1-start" src="https://github.com/user-attachments/assets/3d46b1ee-9730-4834-ac64-328a5b92227c" /> - Created profile name input with validation for letters, numbers, - and _ - Made region selection optional with us-east-1 default <img width="1122" height="631" alt="4-console-profile" src="https://github.com/user-attachments/assets/f1acfffb-40b6-4f7e-a87a-96da6b0ff59d" /> - Shows clear guidance during browser authentication flow <img width="1150" height="765" alt="Opening AWS sign-in in your default browser." src="https://github.com/user-attachments/assets/9f04fea4-0980-4eef-9b3f-e5c2caa9fbc5" /> - Attempt to update AWS CLI if the version < 2.32.0 <img width="1086" height="710" alt="Screenshot 2025-12-11 at 4 31 17 PM" src="https://github.com/user-attachments/assets/77cbc5b6-b238-4db1-bc21-d178081bc298" /> ### Known Issue: Windows PATH Environment After AWS CLI Installation When installing or updating AWS CLI v2 through the toolkit on Windows machine within a managed enterprise or workspace environment, the installation may appear successful, but users receive the error: ``` [error] aws.toolkit.auth.consoleLogin: Error: Failed to verify or install AWS CLI [CliInstallFailed] -> Error: Could not verify installed CLIs ``` This typically occurs because the installer successfully places the necessary files in the default directory (`C:\Program Files\Amazon\AWSCLIV2\`), but security policies or user permissions within the workspace prevent the installer from correctly or immediately updating the system's PATH environment variable. The command prompt doesn't know where to look for the `aws.exe` file. You can verify the installation using the full path and contact your IT support to add the installation path (`C:\Program Files\Amazon\AWSCLIV2\`) to the System variables `PATH` environment variable. ```powershell "C:\Program Files\Amazon\AWSCLIV2\aws.exe" --version ``` --- - 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 1771f0a commit 52f40c2

File tree

13 files changed

+305
-72
lines changed

13 files changed

+305
-72
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"scan-licenses": "ts-node ./scripts/scan-licenses.ts"
4343
},
4444
"devDependencies": {
45-
"@aws-toolkits/telemetry": "^1.0.338",
45+
"@aws-toolkits/telemetry": "^1.0.341",
4646
"@playwright/browser-chromium": "^1.43.1",
4747
"@stylistic/eslint-plugin": "^2.11.0",
4848
"@types/he": "^1.2.3",

packages/core/src/auth/consoleSessionUtils.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const localize = nls.loadMessageBundle()
99

1010
import { getLogger } from '../shared/logger/logger'
1111
import { ChildProcess } from '../shared/utilities/processUtils'
12-
import { getOrInstallCli } from '../shared/utilities/cliUtils'
12+
import { getOrInstallCli, updateAwsCli } from '../shared/utilities/cliUtils'
13+
import { CancellationError } from '../shared/utilities/timeoutUtils'
1314
import { ToolkitError } from '../shared/errors'
1415
import { telemetry } from '../shared/telemetry/telemetry'
1516
import { Auth } from './auth'
@@ -24,46 +25,45 @@ import { createRegionPrompter } from '../shared/ui/common/region'
2425
* @param region Optional AWS region. If not provided, user will be prompted.
2526
*/
2627
export async function authenticateWithConsoleLogin(profileName?: string, region?: string): Promise<void> {
27-
await telemetry.auth_consoleLoginCommand.run(async (span) => {
28-
span.record({ authConsoleLoginStarted: true }) // Track entry into flow (raw count)
29-
const logger = getLogger()
28+
const logger = getLogger()
3029

31-
// Prompt for profile name if not provided
32-
if (!profileName) {
33-
const profileNameInput = await vscode.window.showInputBox({
34-
prompt: localize('AWS.message.prompt.consoleLogin.profileName', 'Enter a name for this profile'),
35-
placeHolder: localize('AWS.message.placeholder.consoleLogin.profileName', 'profile-name'),
36-
validateInput: (value) => {
37-
if (!value || value.trim().length === 0) {
38-
return localize(
39-
'AWS.message.error.consoleLogin.emptyProfileName',
40-
'Profile name cannot be empty'
41-
)
42-
}
43-
if (/\s/.test(value)) {
44-
return localize(
45-
'AWS.message.error.consoleLogin.spacesInProfileName',
46-
'Profile name cannot contain spaces'
47-
)
48-
}
49-
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
50-
return localize(
51-
'AWS.message.error.consoleLogin.invalidCharacters',
52-
'Profile name can only contain letters, numbers, underscores, and hyphens'
53-
)
54-
}
55-
return undefined
56-
},
30+
// Prompt for profile name if not provided
31+
if (!profileName) {
32+
const profileNameInput = await vscode.window.showInputBox({
33+
prompt: localize('AWS.message.prompt.consoleLogin.profileName', 'Enter a name for this profile'),
34+
placeHolder: localize('AWS.message.placeholder.consoleLogin.profileName', 'profile-name'),
35+
validateInput: (value) => {
36+
if (!value || value.trim().length === 0) {
37+
return localize('AWS.message.error.consoleLogin.emptyProfileName', 'Profile name cannot be empty')
38+
}
39+
if (/\s/.test(value)) {
40+
return localize(
41+
'AWS.message.error.consoleLogin.spacesInProfileName',
42+
'Profile name cannot contain spaces'
43+
)
44+
}
45+
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
46+
return localize(
47+
'AWS.message.error.consoleLogin.invalidCharacters',
48+
'Profile name can only contain letters, numbers, underscores, and hyphens'
49+
)
50+
}
51+
return undefined
52+
},
53+
})
54+
55+
if (!profileNameInput) {
56+
throw new ToolkitError('User cancelled entering profile', {
57+
cancelled: true,
5758
})
59+
}
5860

59-
if (!profileNameInput) {
60-
throw new ToolkitError('User cancelled entering profile', {
61-
cancelled: true,
62-
})
63-
}
61+
profileName = profileNameInput.trim()
62+
}
6463

65-
profileName = profileNameInput.trim()
66-
}
64+
// After user interaction has occurred, we can safely emit telemetry
65+
await telemetry.auth_consoleLoginCommand.run(async (span) => {
66+
span.record({ authConsoleLoginStarted: true }) // Track entry into flow (raw count)
6767

6868
// Prompt for region if not provided
6969
if (!region) {
@@ -216,7 +216,8 @@ export async function authenticateWithConsoleLogin(profileName?: string, region?
216216
void vscode.window.showErrorMessage(
217217
localize(
218218
'AWS.message.error.consoleLogin.signinServiceError',
219-
'The command was successfully parsed and a request was made to the AWS Sign-in service but the service returned an error. Please try again.'
219+
'Unable to sign in with console credentials in "{0}". Please try another region.',
220+
region
220221
)
221222
)
222223
throw new ToolkitError('AWS Sign-in service returned an error', {
@@ -225,6 +226,21 @@ export async function authenticateWithConsoleLogin(profileName?: string, region?
225226
exitCode: result.exitCode,
226227
},
227228
})
229+
} else if (result.exitCode === 252) {
230+
// AWS CLI is outdated, attempt to update
231+
try {
232+
await updateAwsCli()
233+
// Retry the login command after successful update
234+
return await authenticateWithConsoleLogin(profileName, region)
235+
} catch (err) {
236+
if (CancellationError.isUserCancelled(err)) {
237+
throw new ToolkitError('User cancelled updating AWS CLI', {
238+
cancelled: true,
239+
})
240+
}
241+
logger.error('Failed to update AWS CLI: %O', err)
242+
throw ToolkitError.chain(err, 'AWS CLI update failed')
243+
}
228244
} else {
229245
// Show generic error message
230246
void vscode.window.showErrorMessage(

packages/core/src/auth/providers/sharedCredentialsProvider.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export class SharedCredentialsProvider implements CredentialsProvider {
9090
if (hasProps(this.profile, SharedCredentialsKeys.SSO_START_URL)) {
9191
return 'ssoProfile'
9292
} else if (hasProps(this.profile, SharedCredentialsKeys.CONSOLE_SESSION)) {
93-
// @ts-ignore wait for https://github.com/aws/aws-toolkit-common/commit/f3a7c462000b1261ffce2b31ef00e99f73255d48
9493
return 'consoleSessionProfile'
9594
} else if (this.isCredentialSource(credentialSources.EC2_INSTANCE_METADATA)) {
9695
return 'ec2Metadata'
@@ -358,7 +357,7 @@ export class SharedCredentialsProvider implements CredentialsProvider {
358357

359358
if (hasProps(this.profile, SharedCredentialsKeys.CONSOLE_SESSION)) {
360359
logger.verbose(
361-
`Profile ${this.profileName} contains ${SharedCredentialsKeys.CONSOLE_SESSION} - treating as regular Shared Credentials`
360+
`Profile ${this.profileName} contains ${SharedCredentialsKeys.CONSOLE_SESSION} - treating as Console Credentials`
362361
)
363362

364363
return this.makeConsoleSessionCredentialsProvider()
@@ -398,15 +397,16 @@ export class SharedCredentialsProvider implements CredentialsProvider {
398397

399398
private makeConsoleSessionCredentialsProvider() {
400399
const defaultRegion = this.getDefaultRegion() ?? 'us-east-1'
400+
const baseProvider = fromLoginCredentials({
401+
profile: this.profileName,
402+
clientConfig: {
403+
region: this.getDefaultRegion() ?? 'us-east-1',
404+
},
405+
})
406+
401407
return async () => {
402408
try {
403-
const provider = fromLoginCredentials({
404-
profile: this.profileName,
405-
clientConfig: {
406-
region: defaultRegion,
407-
},
408-
})
409-
return await provider()
409+
return await baseProvider()
410410
} catch (error) {
411411
getLogger().error(
412412
'Console login authentication failed for profile %s in region %s: %O',
@@ -418,8 +418,22 @@ export class SharedCredentialsProvider implements CredentialsProvider {
418418
if (
419419
error instanceof Error &&
420420
(error.message.includes('Your session has expired') ||
421-
error.message.includes('Failed to load a token for session'))
421+
error.message.includes('Failed to load a token for session') ||
422+
error.message.includes('Failed to load token from'))
422423
) {
424+
// Ask for user confirmation before refreshing
425+
const response = await vscode.window.showInformationMessage(
426+
`Unable to use your console credentials for profile "${this.profileName}". Would you like to refresh it?`,
427+
'Refresh',
428+
'Cancel'
429+
)
430+
431+
if (response !== 'Refresh') {
432+
throw ToolkitError.chain(error, 'User cancelled console credentials token refresh.', {
433+
code: 'LoginSessionRefreshCancelled',
434+
cancelled: true,
435+
})
436+
}
423437
getLogger().info('Re-authenticating using console credentials for profile %s', this.profileName)
424438
// Execute the console login command with the existing profile and region
425439
try {
@@ -440,14 +454,8 @@ export class SharedCredentialsProvider implements CredentialsProvider {
440454
'Authentication completed for profile %s, refreshing credentials...',
441455
this.profileName
442456
)
443-
// Retry with fresh credentials
444-
const refreshedProvider = fromLoginCredentials({
445-
profile: this.profileName,
446-
clientConfig: {
447-
region: defaultRegion,
448-
},
449-
})
450-
return await refreshedProvider()
457+
// Use the same provider instance but get fresh credentials
458+
return await baseProvider()
451459
}
452460
throw ToolkitError.chain(error, `Failed to get console credentials`, {
453461
code: 'FromLoginCredentialProviderError',

packages/core/src/login/webview/vue/amazonq/backend_amazonq.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
210210
return []
211211
}
212212

213+
override startConsoleCredentialSetup(profileName: string, region: string): Promise<AuthError | undefined> {
214+
throw new Error('Method not implemented.')
215+
}
216+
213217
override startIamCredentialSetup(
214218
profileName: string,
215219
accessKey: string,

packages/core/src/login/webview/vue/backend.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export abstract class CommonAuthWebview extends VueWebview {
171171
return Auth.instance.authenticateData(data)
172172
}
173173

174+
abstract startConsoleCredentialSetup(profileName: string, region: string): Promise<AuthError | undefined>
175+
174176
abstract startIamCredentialSetup(
175177
profileName: string,
176178
accessKey: string,

0 commit comments

Comments
 (0)