Skip to content

Commit bef805d

Browse files
authWebview: Builder Id sign in for CC + CW (#3465)
What this change includes Users can signup/signin to builder ID for CC or CW They will be guided through the existing signin flow CC and CW signin must be done individually. User will need to complete signin for each. When signed in, a 'signout' option is shown, and if clicked all of BuilderID will be signed out and CC + CW will be updated in the UI Listening to events from the backend. Eg: when a user interacts with builder id auth using an alternative to the webview, the webview will update with the latest status. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 1a5bdcb commit bef805d

18 files changed

+450
-54
lines changed

src/codecatalyst/auth.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export class CodeCatalystAuthenticationProvider {
7070
return this.secondaryAuth.isUsingSavedConnection
7171
}
7272

73+
public isConnectionValid(): boolean {
74+
return this.activeConnection !== undefined && !this.secondaryAuth.isConnectionExpired
75+
}
76+
7377
// Get rid of this? Not sure where to put PAT code.
7478
public async getPat(client: CodeCatalystClient, username = client.identity.name): Promise<string> {
7579
const stored = await this.storage.getPat(username)
@@ -225,9 +229,13 @@ export class CodeCatalystAuthenticationProvider {
225229
}
226230
}
227231

228-
private static instance: CodeCatalystAuthenticationProvider
232+
static #instance: CodeCatalystAuthenticationProvider | undefined
233+
234+
public static get instance(): CodeCatalystAuthenticationProvider | undefined {
235+
return CodeCatalystAuthenticationProvider.#instance
236+
}
229237

230238
public static fromContext(ctx: Pick<vscode.ExtensionContext, 'secrets' | 'globalState'>) {
231-
return (this.instance ??= new this(new CodeCatalystAuthStorage(ctx.secrets), ctx.globalState))
239+
return (this.#instance ??= new this(new CodeCatalystAuthStorage(ctx.secrets), ctx.globalState))
232240
}
233241
}

src/codecatalyst/explorer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ConnectedDevEnv, getDevfileLocation, getThisDevEnv } from './model'
1717
import * as codecatalyst from './model'
1818
import { getLogger } from '../shared/logger'
1919

20-
const getStartedCommand = Commands.register(
20+
export const getStartedCommand = Commands.register(
2121
'aws.codecatalyst.getStarted',
2222
async (authProvider: CodeCatalystAuthenticationProvider) => {
2323
const conn = authProvider.activeConnection ?? (await authProvider.promptNotConnected())

src/codewhisperer/util/authUtil.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,24 @@ export class AuthUtil {
9292
return this.conn !== undefined && this.usingEnterpriseSSO
9393
}
9494

95+
public isBuilderIdInUse(): boolean {
96+
return this.conn !== undefined && isBuilderIdConnection(this.conn)
97+
}
98+
9599
public async connectToAwsBuilderId() {
96-
const existingConn = (await this.auth.listConnections()).find(isBuilderIdConnection)
97-
if (!existingConn) {
98-
const newConn = await this.auth.createConnection(awsBuilderIdSsoProfile)
99-
await this.secondaryAuth.useNewConnection(newConn)
100+
let conn = (await this.auth.listConnections()).find(isBuilderIdConnection)
100101

101-
return newConn
102+
if (!conn) {
103+
conn = await this.auth.createConnection(awsBuilderIdSsoProfile)
104+
} else if (!isValidCodeWhispererConnection(conn)) {
105+
conn = await this.secondaryAuth.addScopes(conn, defaultScopes)
102106
}
103107

104-
if (isValidCodeWhispererConnection(existingConn)) {
105-
await this.secondaryAuth.useNewConnection(existingConn)
106-
return existingConn
108+
if (this.auth.getConnectionState(conn) === 'invalid') {
109+
conn = await this.auth.reauthenticate(conn)
107110
}
108111

109-
return this.secondaryAuth.addScopes(existingConn, defaultScopes)
112+
return this.secondaryAuth.useNewConnection(conn)
110113
}
111114

112115
public async connectToEnterpriseSso(startUrl: string, region: string) {

src/credentials/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ const switchConnections = Commands.register('aws.auth.switchConnections', (auth:
10611061
}
10621062
})
10631063

1064-
async function signout(auth: Auth, conn: Connection | undefined = auth.activeConnection) {
1064+
export async function signout(auth: Auth, conn: Connection | undefined = auth.activeConnection) {
10651065
if (conn?.type === 'sso') {
10661066
// TODO: does deleting the connection make sense UX-wise?
10671067
// this makes it disappear from the list of available connections
Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script lang="ts">
22
import { defineComponent } from 'vue'
3+
import { AuthStatus } from './shared.vue'
4+
import { AuthFormId } from './types.vue'
35
46
export default defineComponent({
57
emits: ['auth-connection-updated'],
@@ -10,22 +12,9 @@ export default defineComponent({
1012
},
1113
})
1214
13-
export interface AuthStatus {
14-
/**
15-
* Returns true if the auth is successfully connected.
16-
*/
17-
isAuthConnected(): Promise<boolean>
18-
}
19-
2015
export class UnimplementedAuthStatus implements AuthStatus {
2116
isAuthConnected(): Promise<boolean> {
2217
return Promise.resolve(false)
2318
}
2419
}
25-
26-
export const authForms = {
27-
CREDENTIALS: 'CREDENTIALS',
28-
} as const
29-
30-
export type AuthFormId = (typeof authForms)[keyof typeof authForms]
3120
</script>
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<template>
2+
<div class="auth-form container-background border-common" id="builder-id-form">
3+
<div v-show="canShowAll">
4+
<FormTitle :isConnected="isConnected">AWS Builder ID</FormTitle>
5+
6+
<div v-if="stage === stages.START">
7+
<div class="form-section">
8+
<div>
9+
With AWS Builder ID, sign in for free without an AWS account.
10+
<a href="https://docs.aws.amazon.com/signin/latest/userguide/sign-in-aws_builder_id.html"
11+
>Read more.</a
12+
>
13+
</div>
14+
</div>
15+
16+
<div class="form-section">
17+
<button v-on:click="startSignIn()">Sign up or Sign in</button>
18+
</div>
19+
</div>
20+
21+
<div v-if="stage === stages.WAITING_ON_USER">
22+
<div class="form-section">
23+
<div>Follow instructions...</div>
24+
</div>
25+
</div>
26+
27+
<div v-if="stage === stages.CONNECTED">
28+
<div class="form-section">
29+
<div v-on:click="signout()" style="cursor: pointer; color: #75beff">Sign out</div>
30+
</div>
31+
</div>
32+
</div>
33+
</div>
34+
</template>
35+
<script lang="ts">
36+
import { PropType, defineComponent } from 'vue'
37+
import BaseAuthForm from './baseAuth.vue'
38+
import FormTitle from './formTitle.vue'
39+
import { AuthStatus } from './shared.vue'
40+
import { WebviewClientFactory } from '../../../webviews/client'
41+
import { AuthWebview } from '../show'
42+
import authForms, { AuthFormId } from './types.vue'
43+
44+
const client = WebviewClientFactory.create<AuthWebview>()
45+
46+
/** Where the user is currently in the builder id setup process */
47+
export const stages = {
48+
START: 'START',
49+
WAITING_ON_USER: 'WAITING_ON_USER',
50+
CONNECTED: 'CONNECTED',
51+
} as const
52+
type BuilderIdStage = (typeof stages)[keyof typeof stages]
53+
54+
export default defineComponent({
55+
name: 'CredentialsForm',
56+
extends: BaseAuthForm,
57+
components: { FormTitle },
58+
props: {
59+
state: {
60+
type: Object as PropType<BaseBuilderIdState>,
61+
required: true,
62+
},
63+
stages: {
64+
type: Object as PropType<typeof stages>,
65+
default: stages,
66+
},
67+
},
68+
data() {
69+
return {
70+
stage: stages.START as BuilderIdStage,
71+
isConnected: false,
72+
builderIdCode: '',
73+
canShowAll: false,
74+
}
75+
},
76+
async created() {
77+
await this.update()
78+
this.canShowAll = true
79+
},
80+
methods: {
81+
async startSignIn() {
82+
this.stage = this.stages.WAITING_ON_USER
83+
await this.state.startBuilderIdSetup()
84+
await this.update()
85+
},
86+
async update() {
87+
this.stage = await this.state.stage()
88+
this.isConnected = await this.state.isAuthConnected()
89+
this.emitAuthConnectionUpdated(this.state.id)
90+
},
91+
async signout() {
92+
await this.state.signout()
93+
94+
this.update()
95+
},
96+
},
97+
})
98+
99+
/**
100+
* Manages the state of Builder ID.
101+
*/
102+
abstract class BaseBuilderIdState implements AuthStatus {
103+
protected _stage: BuilderIdStage = stages.START
104+
105+
abstract get id(): AuthFormId
106+
protected abstract _startBuilderIdSetup(): Promise<void>
107+
abstract isAuthConnected(): Promise<boolean>
108+
109+
async startBuilderIdSetup(): Promise<void> {
110+
this._stage = stages.WAITING_ON_USER
111+
return this._startBuilderIdSetup()
112+
}
113+
114+
async stage(): Promise<BuilderIdStage> {
115+
const isAuthConnected = await this.isAuthConnected()
116+
this._stage = isAuthConnected ? stages.CONNECTED : stages.START
117+
return this._stage
118+
}
119+
120+
async signout(): Promise<void> {
121+
await client.signoutBuilderId()
122+
}
123+
}
124+
125+
export class CodeWhispererBuilderIdState extends BaseBuilderIdState {
126+
override get id(): AuthFormId {
127+
return authForms.BUILDER_ID_CODE_WHISPERER
128+
}
129+
130+
override isAuthConnected(): Promise<boolean> {
131+
return client.isCodeWhispererBuilderIdConnected()
132+
}
133+
134+
protected override _startBuilderIdSetup(): Promise<void> {
135+
return client.startCodeWhispererBuilderIdSetup()
136+
}
137+
}
138+
139+
export class CodeCatalystBuilderIdState extends BaseBuilderIdState {
140+
override get id(): AuthFormId {
141+
return authForms.BUILDER_ID_CODE_CATALYST
142+
}
143+
144+
override isAuthConnected(): Promise<boolean> {
145+
return client.isCodeCatalystBuilderIdConnected()
146+
}
147+
148+
protected override _startBuilderIdSetup(): Promise<void> {
149+
return client.startCodeCatalystBuilderIdSetup()
150+
}
151+
}
152+
</script>
153+
<style>
154+
@import './sharedAuthForms.css';
155+
@import '../shared.css';
156+
157+
#builder-id-form {
158+
width: 250px;
159+
height: fit-content;
160+
}
161+
</style>

src/credentials/vue/authForms/manageCredentials.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@
5050
</template>
5151
<script lang="ts">
5252
import { PropType, defineComponent } from 'vue'
53-
import BaseAuthForm, { AuthStatus } from './baseAuth.vue'
53+
import BaseAuthForm from './baseAuth.vue'
5454
import FormTitle from './formTitle.vue'
5555
import { SectionName, StaticProfile } from '../../types'
5656
import { WebviewClientFactory } from '../../../webviews/client'
5757
import { AuthWebview } from '../show'
58+
import { AuthStatus } from './shared.vue'
5859
5960
const client = WebviewClientFactory.create<AuthWebview>()
6061
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
<script lang="ts">
2+
import { CodeCatalystBuilderIdState, CodeWhispererBuilderIdState } from './manageBuilderId.vue'
23
import { CredentialsState } from './manageCredentials.vue'
3-
import { authForms } from './baseAuth.vue'
4+
import authForms from './types.vue'
45
56
/**
67
* The state instance of all auth forms
78
*/
89
const authFormsState = {
910
[authForms.CREDENTIALS]: new CredentialsState(),
11+
[authForms.BUILDER_ID_CODE_WHISPERER]: new CodeWhispererBuilderIdState(),
12+
[authForms.BUILDER_ID_CODE_CATALYST]: new CodeCatalystBuilderIdState(),
1013
} as const
1114
15+
export interface AuthStatus {
16+
/**
17+
* Returns true if the auth is successfully connected.
18+
*/
19+
isAuthConnected(): Promise<boolean>
20+
}
21+
1222
export default authFormsState
1323
</script>

src/credentials/vue/authForms/sharedAuthForms.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
.auth-form-title {
2323
font-size: 14px;
24+
font-weight: bold;
2425
color: #ffffff;
2526
}
2627

@@ -54,3 +55,8 @@ input[data-invalid='true']:focus {
5455
/* Ensures the border stays red even when selected */
5556
outline: none !important;
5657
}
58+
59+
/* Remove underline from anchor elements */
60+
a {
61+
text-decoration: none;
62+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
export const authForms = {
3+
CREDENTIALS: 'CREDENTIALS',
4+
BUILDER_ID_CODE_WHISPERER: 'BUILDER_ID_CODE_WHISPERER',
5+
BUILDER_ID_CODE_CATALYST: 'BUILDER_ID_CODE_CATALYST',
6+
} as const
7+
8+
export type AuthFormId = (typeof authForms)[keyof typeof authForms]
9+
10+
export default authForms
11+
</script>

0 commit comments

Comments
 (0)