Skip to content

Commit be60aee

Browse files
committed
feat(auth): add console credentials sign-in option to login webview
1 parent ae29517 commit be60aee

7 files changed

Lines changed: 180 additions & 18 deletions

File tree

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

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -398,15 +398,16 @@ export class SharedCredentialsProvider implements CredentialsProvider {
398398

399399
private makeConsoleSessionCredentialsProvider() {
400400
const defaultRegion = this.getDefaultRegion() ?? 'us-east-1'
401+
const baseProvider = fromLoginCredentials({
402+
profile: this.profileName,
403+
clientConfig: {
404+
region: this.getDefaultRegion() ?? 'us-east-1',
405+
},
406+
})
407+
401408
return async () => {
402409
try {
403-
const provider = fromLoginCredentials({
404-
profile: this.profileName,
405-
clientConfig: {
406-
region: defaultRegion,
407-
},
408-
})
409-
return await provider()
410+
return await baseProvider()
410411
} catch (error) {
411412
getLogger().error(
412413
'Console login authentication failed for profile %s in region %s: %O',
@@ -440,14 +441,8 @@ export class SharedCredentialsProvider implements CredentialsProvider {
440441
'Authentication completed for profile %s, refreshing credentials...',
441442
this.profileName
442443
)
443-
// Retry with fresh credentials
444-
const refreshedProvider = fromLoginCredentials({
445-
profile: this.profileName,
446-
clientConfig: {
447-
region: defaultRegion,
448-
},
449-
})
450-
return await refreshedProvider()
444+
// Use the same provider instance but get fresh credentials
445+
return await baseProvider()
451446
}
452447
throw ToolkitError.chain(error, `Failed to get console credentials`, {
453448
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,

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

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@
123123
:itemType="LoginOption.ENTERPRISE_SSO"
124124
class="selectable-item bottomMargin"
125125
></SelectableItem>
126+
<SelectableItem
127+
v-if="app === 'TOOLKIT'"
128+
@toggle="toggleItemSelection"
129+
:isSelected="selectedLoginOption === LoginOption.CONSOLE_CREDENTIAL"
130+
:itemId="LoginOption.CONSOLE_CREDENTIAL"
131+
:itemText="'Use credentials from the AWS Console'"
132+
:itemTitle="'Console credentials - recommended'"
133+
:itemType="LoginOption.CONSOLE_CREDENTIAL"
134+
class="selectable-item bottomMargin"
135+
></SelectableItem>
126136
<SelectableItem
127137
v-if="app === 'TOOLKIT'"
128138
@toggle="toggleItemSelection"
@@ -220,9 +230,27 @@
220230

221231
<template v-if="stage === 'AUTHENTICATING'">
222232
<div class="auth-container-section">
223-
<div v-if="app === 'TOOLKIT' && profileName.length > 0" class="header bottomMargin">
233+
<div
234+
v-if="app === 'TOOLKIT' && profileName.length > 0 && previousStage === 'AWS_PROFILE'"
235+
class="header bottomMargin"
236+
>
224237
Connecting to IAM...
225238
</div>
239+
<div
240+
v-else-if="app === 'TOOLKIT' && profileName.length > 0 && previousStage === 'CONSOLE_PROFILE'"
241+
class="bottomMargin"
242+
>
243+
<div class="header">Opening AWS sign-in in your default browser...</div>
244+
<div class="hint">
245+
If you're already signed in to AWS Console, you can select that session. Or follow the steps to
246+
sign in with your AWS account.
247+
</div>
248+
<a
249+
class="hint"
250+
href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sign-in.html#cli-configure-sign-in-prerequisites"
251+
>Need help? Check setup requirements</a
252+
>
253+
</div>
226254
<div v-else class="header bottomMargin">Authenticating in browser...</div>
227255
<button class="continue-button" v-on:click="handleCancelButton()">Cancel</button>
228256
</div>
@@ -276,6 +304,56 @@
276304
Continue
277305
</button>
278306
</template>
307+
<template v-if="stage === 'CONSOLE_PROFILE'">
308+
<button class="back-button bottomMargin" @click="handleBackButtonClick">
309+
<svg width="13" height="11" viewBox="0 0 13 11" fill="none" xmlns="http://www.w3.org/2000/svg">
310+
<path
311+
d="M4.98667 0.0933332L5.73333 0.786666L1.57333 4.94667H12.0267V5.96H1.57333L5.73333 10.0667L4.98667 10.8133L0.0266666 5.8V5.10667L4.98667 0.0933332Z"
312+
fill="#21A2FF"
313+
/>
314+
</svg>
315+
</button>
316+
<div class="header">Use your AWS Console credentials</div>
317+
<div class="hint">
318+
(Recommended) Use your AWS Console sign-in to get secure, temporary credentials for local development.
319+
No need to create or manage access keys.
320+
</div>
321+
<a class="hint" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sign-in.html"
322+
>Learn More</a
323+
>
324+
<div class="title topMargin">Profile Name</div>
325+
<div class="hint">Use letters, numbers, underscores, or hyphens</div>
326+
<input
327+
class="iamInput bottomMargin"
328+
type="text"
329+
id="profileName"
330+
name="profileName"
331+
v-model="profileName"
332+
placeholder="profile-name"
333+
:pattern="'^[a-zA-Z0-9_\\-]+$'"
334+
@keydown.enter="handleContinueClick()"
335+
@keydown="preventInvalidChars"
336+
/>
337+
<div class="title topMargin">(Optional) Region</div>
338+
<select
339+
class="regionSelect"
340+
id="regions"
341+
name="regions"
342+
v-model="selectedRegion"
343+
@change="handleRegionInput($event)"
344+
>
345+
<option v-for="region in regions" :key="region.id" :value="region.id">
346+
{{ `${region.name} (${region.id})` }}
347+
</option>
348+
</select>
349+
<button
350+
class="continue-button"
351+
:disabled="shouldDisableConsoleSessionContinue()"
352+
v-on:click="handleContinueClick()"
353+
>
354+
Continue
355+
</button>
356+
</template>
279357
</div>
280358
</template>
281359
<script lang="ts">
@@ -290,7 +368,7 @@ import { ssoUrlFormatRegex, ssoUrlFormatMessage, urlInvalidFormatMessage } from
290368
const client = WebviewClientFactory.create<CommonAuthWebview>()
291369
292370
/** Where the user is currently in the builder id setup process */
293-
type Stage = 'START' | 'SSO_FORM' | 'CONNECTED' | 'AUTHENTICATING' | 'AWS_PROFILE'
371+
type Stage = 'START' | 'SSO_FORM' | 'CONNECTED' | 'AUTHENTICATING' | 'AWS_PROFILE' | 'CONSOLE_PROFILE'
294372
295373
function getCredentialId(loginOption: LoginOption) {
296374
switch (loginOption) {
@@ -300,13 +378,16 @@ function getCredentialId(loginOption: LoginOption) {
300378
return 'iamIdentityCenter'
301379
case LoginOption.IAM_CREDENTIAL:
302380
return 'sharedCredentials'
381+
case LoginOption.CONSOLE_CREDENTIAL:
382+
return 'consoleCredentials'
303383
default:
304384
return undefined
305385
}
306386
}
307387
308388
const authUiClickOptionMap = {
309389
[LoginOption.BUILDER_ID]: 'auth_builderIdOption',
390+
[LoginOption.CONSOLE_CREDENTIAL]: 'auth_consoleCredentialsOption',
310391
[LoginOption.ENTERPRISE_SSO]: 'auth_idcOption',
311392
[LoginOption.IAM_CREDENTIAL]: 'auth_credentialsOption',
312393
[LoginOption.IMPORTED_LOGINS]: 'auth_existingAuthOption',
@@ -345,6 +426,7 @@ export default defineComponent({
345426
importedLogins: [] as ImportedLogin[],
346427
selectedLoginOption: LoginOption.NONE,
347428
stage: 'START' as Stage,
429+
previousStage: '' as Stage,
348430
regions: [] as Region[],
349431
startUrlError: '',
350432
startUrlWarning: '',
@@ -445,6 +527,10 @@ export default defineComponent({
445527
} else if (this.selectedLoginOption === LoginOption.IAM_CREDENTIAL) {
446528
this.stage = 'AWS_PROFILE'
447529
this.$nextTick(() => document.getElementById('profileName')!.focus())
530+
} else if (this.selectedLoginOption === LoginOption.CONSOLE_CREDENTIAL) {
531+
this.stage = 'CONSOLE_PROFILE'
532+
this.$nextTick(() => document.getElementById('profileName')!.focus())
533+
await client.storeMetricMetadata({ awsRegion: this.selectedRegion })
448534
}
449535
} else if (this.stage === 'SSO_FORM') {
450536
if (this.shouldDisableSsoContinue()) {
@@ -463,6 +549,7 @@ export default defineComponent({
463549
if (this.shouldDisableIamContinue()) {
464550
return
465551
}
552+
this.previousStage = this.stage
466553
this.stage = 'AUTHENTICATING'
467554
const error = await client.startIamCredentialSetup(this.profileName, this.accessKey, this.secretKey)
468555
if (error) {
@@ -471,6 +558,19 @@ export default defineComponent({
471558
} else {
472559
this.stage = 'CONNECTED'
473560
}
561+
} else if (this.stage === 'CONSOLE_PROFILE') {
562+
if (this.shouldDisableConsoleSessionContinue()) {
563+
return
564+
}
565+
this.previousStage = this.stage
566+
this.stage = 'AUTHENTICATING'
567+
const error = await client.startConsoleCredentialSetup(this.profileName, this.selectedRegion)
568+
if (error) {
569+
this.stage = 'START'
570+
void client.errorNotification(error)
571+
} else {
572+
this.stage = 'CONNECTED'
573+
}
474574
}
475575
void client.emitUiClick('auth_continueButton')
476576
},
@@ -584,7 +684,7 @@ export default defineComponent({
584684
} else if (this.app === 'AMAZONQ') {
585685
this.selectedLoginOption = LoginOption.BUILDER_ID
586686
} else if (this.app === 'TOOLKIT') {
587-
this.selectedLoginOption = LoginOption.ENTERPRISE_SSO
687+
this.selectedLoginOption = LoginOption.CONSOLE_CREDENTIAL
588688
}
589689
this.$forceUpdate()
590690
},
@@ -594,6 +694,20 @@ export default defineComponent({
594694
shouldDisableIamContinue() {
595695
return this.profileName.length <= 0 || this.accessKey.length <= 0 || this.secretKey.length <= 0
596696
},
697+
shouldDisableConsoleSessionContinue() {
698+
const profilePattern = /^[a-zA-Z0-9_-]+$/
699+
return this.profileName.length <= 0 || !this.selectedRegion || !profilePattern.test(this.profileName)
700+
},
701+
preventInvalidChars(event: KeyboardEvent) {
702+
// Allow control keys (backspace, delete, arrows, etc)
703+
if (event.key.length === 1) {
704+
// Only check single characters
705+
// Only allow letters, numbers, underscore, and hyphen
706+
if (!/^[a-zA-Z0-9_\-]$/.test(event.key)) {
707+
event.preventDefault()
708+
}
709+
}
710+
},
597711
},
598712
})
599713

packages/core/src/login/webview/vue/selectableItem.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@
1212
@mouseout="isHovering = false"
1313
>
1414
<div class="icon">
15+
<svg
16+
v-if="itemType === LoginOption.CONSOLE_CREDENTIAL"
17+
width="16"
18+
height="16"
19+
viewBox="0 0 16 16"
20+
fill="none"
21+
xmlns="http://www.w3.org/2000/svg"
22+
>
23+
<path
24+
class="svg-path"
25+
fill-rule="evenodd"
26+
clip-rule="evenodd"
27+
d="M1.50024 1H14.5002L15.0002 1.5V4.5V13.5L14.5002 14H1.50024L1.00024 13.5V4.5V1.5L1.50024 1ZM2.00024 5V13H14.0002V5H2.00024ZM2.00024 4H14.0002V2H2.00024V4Z"
28+
/>
29+
</svg>
1530
<svg
1631
v-if="itemType === LoginOption.BUILDER_ID"
1732
width="16"

packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,37 @@ export class ToolkitLoginWebview extends CommonAuthWebview {
8686
})
8787
}
8888

89+
async startConsoleCredentialSetup(profileName: string, region: string): Promise<AuthError | undefined> {
90+
getLogger().debug(`called startConsoleCredentialSetup()`)
91+
const runAuth = async () => {
92+
try {
93+
// Execute AWS CLI login command
94+
await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region)
95+
96+
// Hide auth view and show resource explorer
97+
await setContext('aws.explorer.showAuthView', false)
98+
await this.showResourceExplorer()
99+
100+
return undefined // Success case returns undefined
101+
} catch (err) {
102+
getLogger().error('Failed setting up authentication with console credentials : %O', err)
103+
return {
104+
id: this.id,
105+
text: err instanceof Error ? err.message : String(err),
106+
}
107+
}
108+
}
109+
const result = await runAuth()
110+
this.storeMetricMetadata({
111+
// @ts-ignore wait for https://github.com/aws/aws-toolkit-common/pull/1108
112+
credentialSourceId: 'consoleCredentials',
113+
authEnabledFeatures: 'awsExplorer',
114+
...this.getResultForMetrics(result),
115+
})
116+
this.emitAuthMetric()
117+
return result
118+
}
119+
89120
async startIamCredentialSetup(
90121
profileName: string,
91122
accessKey: string,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type AuthFlowState = (typeof AuthFlowStates)[keyof typeof AuthFlowStates]
3636
export enum LoginOption {
3737
NONE,
3838
BUILDER_ID,
39+
CONSOLE_CREDENTIAL,
3940
ENTERPRISE_SSO,
4041
IAM_CREDENTIAL,
4142
IMPORTED_LOGINS,

0 commit comments

Comments
 (0)