Skip to content

Commit b2662ca

Browse files
author
Joseph de Clerck
committed
Add support for device_code flow by selecting a checkbox
1 parent 00c220a commit b2662ca

File tree

9 files changed

+92
-15
lines changed

9 files changed

+92
-15
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getLogger } from '../../shared/logger/logger'
1414
import { CancellationError } from '../../shared/utilities/timeoutUtils'
1515
import { ssoAuthHelpUrl } from '../../shared/constants'
1616
import { openUrl } from '../../shared/utilities/vsCodeUtils'
17+
import { copyToClipboard } from '../../shared/utilities/messages'
1718
import { ToolkitError } from '../../shared/errors'
1819
import { builderIdStartUrl } from './constants'
1920

@@ -100,6 +101,7 @@ export function truncateStartUrl(startUrl: string) {
100101
type Authorization = { readonly verificationUri: string; readonly userCode: string }
101102

102103
export const proceedToBrowser = localize('AWS.auth.loginWithBrowser.proceedToBrowser', 'Proceed To Browser')
104+
export const copyUrl = localize('AWS.auth.loginWithBrowser.copyLoginUrl', 'Copy authentication URL')
103105

104106
export async function openSsoPortalLink(startUrl: string, authorization: Authorization): Promise<boolean> {
105107
/**
@@ -122,13 +124,18 @@ export async function openSsoPortalLink(startUrl: string, authorization: Authori
122124
authorization.userCode
123125
)
124126

127+
const options = [proceedToBrowser, copyUrl]
128+
125129
while (true) {
126130
// TODO: add the 'Help' item back once we have a suitable URL
127131
// const resp = await vscode.window.showInformationMessage(title, options, copyCode, localizedText.help)
128-
const resp = await vscode.window.showInformationMessage(title, { modal: true, detail }, proceedToBrowser)
132+
const resp = await vscode.window.showInformationMessage(title, { modal: true, detail }, ...options)
129133
switch (resp) {
130134
case proceedToBrowser:
131135
return openSsoUrl(makeConfirmCodeUrl(authorization))
136+
case copyUrl:
137+
await copyToClipboard(makeConfirmCodeUrl(authorization).toString(true))
138+
return true
132139
case localizedText.help:
133140
await tryOpenHelpUrl(ssoAuthHelpUrl)
134141
continue

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,15 @@ export abstract class SsoAccessTokenProvider {
295295
* Device code flow is neccessary when:
296296
* 1. We are in a workspace connected through ssh (codecatalyst, etc)
297297
* 2. We are connected to a remote backend through the web browser (code server, openshift dev spaces)
298+
* 3. User has explicitly requested device code flow via the login page toggle
298299
*
299-
* Since we are unable to serve the final authorization page
300+
* Since we are unable to serve the final authorization page in cases 1 and 2,
301+
* or the user may not have federation access in case 3
300302
*/
303+
// Check for user preference first, then fall back to the current logic
304+
if (globals.globalState.get<boolean>('aws.forceDeviceCodeFlow')) {
305+
return true
306+
}
301307
return getExtRuntimeContext().extensionHost === 'remote'
302308
}
303309
) {
@@ -458,7 +464,8 @@ export class DeviceFlowAuthorization extends SsoAccessTokenProvider {
458464
clientId: registration.clientId,
459465
clientSecret: registration.clientSecret,
460466
})
461-
467+
// Reset forceDeviceCodeFlow if toggled
468+
await globals.globalState.update('aws.forceDeviceCodeFlow', undefined)
462469
const openBrowserAndWaitUntilComplete = async () => {
463470
if (!(await openSsoPortalLink(this.profile.startUrl, authorization))) {
464471
throw new CancellationError('user')

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,24 @@ export class AmazonQLoginWebview extends CommonAuthWebview {
7878
})
7979
}
8080

81-
async startEnterpriseSetup(startUrl: string, region: string): Promise<AuthError | undefined> {
82-
getLogger().debug(`called startEnterpriseSetup() with startUrl: '${startUrl}', region: '${region}'`)
81+
async startEnterpriseSetup(
82+
startUrl: string,
83+
region: string,
84+
app: string,
85+
useDeviceCodeFlow?: boolean
86+
): Promise<AuthError | undefined> {
87+
getLogger().debug(
88+
`called startEnterpriseSetup() with startUrl: '${startUrl}', region: '${region}', useDeviceCodeFlow: ${useDeviceCodeFlow}`
89+
)
8390
await globals.globalState.update('recentSso', {
8491
startUrl: startUrl,
8592
region: region,
8693
})
94+
// Store the device code flow preference if it's set
95+
if (useDeviceCodeFlow) {
96+
await globals.globalState.update('aws.forceDeviceCodeFlow', true)
97+
}
98+
8799
return await this.ssoSetup('startCodeWhispererEnterpriseSetup', async () => {
88100
this.storeMetricMetadata({
89101
credentialStartUrl: startUrl,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ export abstract class CommonAuthWebview extends VueWebview {
165165

166166
abstract startBuilderIdSetup(app: string): Promise<AuthError | undefined>
167167

168-
abstract startEnterpriseSetup(startUrl: string, region: string, app: string): Promise<AuthError | undefined>
168+
abstract startEnterpriseSetup(
169+
startUrl: string,
170+
region: string,
171+
app: string,
172+
useDeviceCodeFlow?: boolean
173+
): Promise<AuthError | undefined>
169174

170175
async getAuthenticatedCredentialsError(data: StaticProfile): Promise<StaticProfileKeyErrorMessage | undefined> {
171176
return Auth.instance.authenticateData(data)

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,19 @@
208208
{{ `${region.name} (${region.id})` }}
209209
</option>
210210
</select>
211+
<div class="form-group topMargin">
212+
<div class="toggle-container">
213+
<label class="toggle-label">
214+
<input
215+
type="checkbox"
216+
id="useDeviceCodeFlow"
217+
name="useDeviceCodeFlow"
218+
v-model="useDeviceCodeFlow"
219+
/>
220+
<span class="toggle-text">Use device code flow</span>
221+
</label>
222+
</div>
223+
</div>
211224
<button
212225
class="continue-button topMargin"
213226
:disabled="shouldDisableSsoContinue()"
@@ -350,6 +363,7 @@ export default defineComponent({
350363
profileName: '',
351364
accessKey: '',
352365
secretKey: '',
366+
useDeviceCodeFlow: false,
353367
}
354368
},
355369
async created() {
@@ -447,7 +461,12 @@ export default defineComponent({
447461
}
448462
this.stage = 'AUTHENTICATING'
449463
450-
const error = await client.startEnterpriseSetup(this.startUrl, this.selectedRegion, this.app)
464+
const error = await client.startEnterpriseSetup(
465+
this.startUrl,
466+
this.selectedRegion,
467+
this.app,
468+
this.useDeviceCodeFlow
469+
)
451470
if (error) {
452471
this.stage = 'START'
453472
void client.errorNotification(error)
@@ -833,8 +852,21 @@ body.vscode-light #logo-text {
833852
width: 10px;
834853
}
835854
836-
.help-link__label {
837-
margin: 0;
838-
padding: 0 0 0 2px;
855+
.toggle-container {
856+
margin-bottom: 10px;
857+
}
858+
859+
.toggle-label {
860+
display: flex;
861+
align-items: center;
862+
cursor: pointer;
863+
}
864+
865+
.toggle-label input[type='checkbox'] {
866+
margin-right: 8px;
867+
}
868+
869+
.toggle-text {
870+
font-size: var(--font-size-sm);
839871
}
840872
</style>

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,26 @@ export class ToolkitLoginWebview extends CommonAuthWebview {
4040
this.isCodeCatalystLogin = serviceToShow === 'codecatalyst'
4141
}
4242

43-
async startEnterpriseSetup(startUrl: string, region: string): Promise<AuthError | undefined> {
44-
getLogger().debug(`called startEnterpriseSetup() with startUrl: '${startUrl}', region: '${region}'`)
43+
async startEnterpriseSetup(
44+
startUrl: string,
45+
region: string,
46+
app: string,
47+
useDeviceCodeFlow?: boolean
48+
): Promise<AuthError | undefined> {
49+
getLogger().debug(
50+
`called startEnterpriseSetup() with startUrl: '${startUrl}', region: '${region}', useDeviceCodeFlow: ${useDeviceCodeFlow}`
51+
)
4552
const metadata: TelemetryMetadata = {
4653
credentialSourceId: 'iamIdentityCenter',
4754
credentialStartUrl: startUrl,
4855
isReAuth: false,
4956
}
57+
58+
// Store the device code flow preference if it's set
59+
if (useDeviceCodeFlow) {
60+
await globals.globalState.update('aws.forceDeviceCodeFlow', true)
61+
}
62+
5063
await globals.globalState.update('recentSso', {
5164
startUrl: startUrl,
5265
region: region,

packages/core/src/shared/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export type globalKey =
5050
| 'aws.amazonq.customization.overrideV2'
5151
| 'aws.amazonq.regionProfiles'
5252
| 'aws.amazonq.regionProfiles.cache'
53+
| 'aws.forceDeviceCodeFlow' // Toggle to force device code flow for authentication
5354
// Deprecated/legacy names. New keys should start with "aws.".
5455
| '#sessionCreationDates' // Legacy name from `ssoAccessTokenProvider.ts`.
5556
| 'CODECATALYST_RECONNECT'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('Amazon Q Login', function () {
6262
})
6363

6464
it('signs into IdC and emits telemetry', async function () {
65-
await backend.startEnterpriseSetup(startUrl, region)
65+
await backend.startEnterpriseSetup(startUrl, region, 'AMAZONQ')
6666

6767
assert.ok(isIdcSsoConnection(auth.activeConnection))
6868
assert.deepStrictEqual(auth.activeConnection.scopes, amazonQScopes)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('Toolkit Login', function () {
6868
})
6969

7070
it('signs into account IdC and emits telemetry', async function () {
71-
await backend.startEnterpriseSetup(startUrl, region)
71+
await backend.startEnterpriseSetup(startUrl, region, 'TOOLKIT')
7272

7373
assert.ok(isIdcSsoConnection(auth.activeConnection))
7474
assert.deepStrictEqual(auth.activeConnection.scopes, scopesSsoAccountAccess)
@@ -90,7 +90,7 @@ describe('Toolkit Login', function () {
9090
sandbox.stub(codecatalystAuth, 'isConnectionOnboarded').resolves(true)
9191
backend.setLoginService('codecatalyst')
9292

93-
await backend.startEnterpriseSetup(startUrl, region)
93+
await backend.startEnterpriseSetup(startUrl, region, 'TOOLKIT')
9494

9595
assert.ok(isIdcSsoConnection(auth.activeConnection))
9696
assert.deepStrictEqual(auth.activeConnection.scopes, defaultScopes)

0 commit comments

Comments
 (0)