Skip to content

Commit efa1ee3

Browse files
committed
Improve UX
1 parent 6b0adbe commit efa1ee3

File tree

4 files changed

+95
-68
lines changed

4 files changed

+95
-68
lines changed

src/commands.ts

Lines changed: 27 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,15 @@ import { type SecretsManager } from "./core/secretsManager";
1919
import { CertificateError } from "./error";
2020
import { getGlobalFlags } from "./globalFlags";
2121
import { type Logger } from "./logging/logger";
22-
import { OAuthMetadataClient } from "./oauth/metadataClient";
2322
import { type OAuthSessionManager } from "./oauth/sessionManager";
24-
import { maybeAskAgent, maybeAskUrl } from "./promptUtils";
23+
import { maybeAskAgent, maybeAskUrl, maybeAskAuthMethod } from "./promptUtils";
2524
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";
2625
import {
2726
AgentTreeItem,
2827
type OpenableTreeItem,
2928
WorkspaceTreeItem,
3029
} from "./workspace/workspacesProvider";
3130

32-
type AuthMethod = "oauth" | "legacy";
33-
3431
export class Commands {
3532
private readonly vscodeProposed: typeof vscode;
3633
private readonly logger: Logger;
@@ -92,7 +89,7 @@ export class Commands {
9289
// Try to get a token from the user, if we need one, and their user.
9390
const autoLogin = args?.autoLogin === true;
9491

95-
const res = await this.maybeAskToken(url, args?.token, autoLogin);
92+
const res = await this.attemptLogin(url, args?.token, autoLogin);
9693
if (!res) {
9794
return; // The user aborted, or unable to auth.
9895
}
@@ -136,12 +133,12 @@ export class Commands {
136133
}
137134

138135
/**
139-
* If necessary, ask for a token, and keep asking until the token has been
140-
* validated. Return the token and user that was fetched to validate the
141-
* token. Null means the user aborted or we were unable to authenticate with
142-
* mTLS (in the latter case, an error notification will have been displayed).
136+
* Attempt to authenticate using OAuth, token, or mTLS. If necessary, prompts
137+
* for authentication method and credentials. Returns the token and user upon
138+
* successful authentication. Null means the user aborted or authentication
139+
* failed (in which case an error notification will have been displayed).
143140
*/
144-
private async maybeAskToken(
141+
private async attemptLogin(
145142
url: string,
146143
token: string | undefined,
147144
isAutoLogin: boolean,
@@ -174,58 +171,18 @@ export class Commands {
174171
}
175172
}
176173

177-
// Check if server supports OAuth
178-
const supportsOAuth = await this.checkOAuthSupport(client);
179-
180-
let choice: AuthMethod | undefined = "legacy";
181-
if (supportsOAuth) {
182-
choice = await this.askAuthMethod();
183-
}
184-
185-
if (choice === "oauth") {
186-
return this.loginWithOAuth(client);
187-
} else if (choice === "legacy") {
188-
const initialToken =
189-
token || (await this.secretsManager.getSessionToken());
190-
return this.loginWithToken(client, initialToken);
174+
const authMethod = await maybeAskAuthMethod(client);
175+
switch (authMethod) {
176+
case "oauth":
177+
return this.loginWithOAuth(client);
178+
case "legacy": {
179+
const initialToken =
180+
token || (await this.secretsManager.getSessionToken());
181+
return this.loginWithToken(client, initialToken);
182+
}
183+
case undefined:
184+
return null; // User aborted
191185
}
192-
193-
return null; // User aborted.
194-
}
195-
196-
private async checkOAuthSupport(client: CoderApi): Promise<boolean> {
197-
const metadataClient = new OAuthMetadataClient(
198-
client.getAxiosInstance(),
199-
this.logger,
200-
);
201-
return metadataClient.checkOAuthSupport();
202-
}
203-
204-
/**
205-
* Ask user to choose between OAuth and legacy API token authentication.
206-
*/
207-
private async askAuthMethod(): Promise<AuthMethod | undefined> {
208-
const choice = await vscode.window.showQuickPick(
209-
[
210-
{
211-
label: "$(key) OAuth (Recommended)",
212-
detail: "Secure authentication with automatic token refresh",
213-
value: "oauth" as const,
214-
},
215-
{
216-
label: "$(lock) API Token",
217-
detail: "Use a manually created API key",
218-
value: "legacy" as const,
219-
},
220-
],
221-
{
222-
title: "Choose Authentication Method",
223-
placeHolder: "How would you like to authenticate?",
224-
ignoreFocusOut: true,
225-
},
226-
);
227-
228-
return choice?.value;
229186
}
230187

231188
private async loginWithToken(
@@ -297,7 +254,15 @@ export class Commands {
297254
try {
298255
this.logger.info("Starting OAuth authentication");
299256

300-
const tokenResponse = await this.oauthSessionManager.login(client);
257+
const tokenResponse = await vscode.window.withProgress(
258+
{
259+
location: vscode.ProgressLocation.Notification,
260+
title: "Authenticating",
261+
cancellable: false,
262+
},
263+
async (progress) =>
264+
await this.oauthSessionManager.login(client, progress),
265+
);
301266

302267
// Validate token by fetching user
303268
client.setSessionToken(tokenResponse.access_token);

src/oauth/metadataClient.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export class OAuthMetadataClient {
2626
/**
2727
* Check if a server supports OAuth by attempting to fetch the well-known endpoint.
2828
*/
29-
async checkOAuthSupport(): Promise<boolean> {
29+
public static async checkOAuthSupport(
30+
axiosInstance: AxiosInstance,
31+
): Promise<boolean> {
3032
try {
31-
await this.axiosInstance.get(OAUTH_DISCOVERY_ENDPOINT);
32-
this.logger.debug("Server supports OAuth");
33+
await axiosInstance.get(OAUTH_DISCOVERY_ENDPOINT);
3334
return true;
34-
} catch (error) {
35-
this.logger.debug("Server does not support OAuth:", error);
35+
} catch {
3636
return false;
3737
}
3838
}

src/oauth/sessionManager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,10 @@ export class OAuthSessionManager implements vscode.Disposable {
263263
* @param client CoderApi instance for the deployment to authenticate against
264264
* @returns TokenResponse containing access token and optional refresh token
265265
*/
266-
async login(client: CoderApi): Promise<TokenResponse> {
266+
async login(
267+
client: CoderApi,
268+
progress: vscode.Progress<{ message?: string; increment?: number }>,
269+
): Promise<TokenResponse> {
267270
const baseUrl = client.getAxiosInstance().defaults.baseURL;
268271
if (!baseUrl) {
269272
throw new Error("CoderApi instance has no base URL set");
@@ -282,13 +285,16 @@ export class OAuthSessionManager implements vscode.Disposable {
282285
const metadata = await metadataClient.getMetadata();
283286

284287
// Only register the client on login
288+
progress.report({ message: "registering client...", increment: 10 });
285289
const registration = await this.registerClient(axiosInstance, metadata);
286290

291+
progress.report({ message: "waiting for authorization...", increment: 30 });
287292
const { code, verifier } = await this.startAuthorization(
288293
metadata,
289294
registration,
290295
);
291296

297+
progress.report({ message: "exchanging token...", increment: 30 });
292298
const tokenResponse = await this.exchangeToken(
293299
code,
294300
verifier,
@@ -297,6 +303,7 @@ export class OAuthSessionManager implements vscode.Disposable {
297303
registration,
298304
);
299305

306+
progress.report({ increment: 30 });
300307
this.logger.info("OAuth login flow completed successfully");
301308

302309
return tokenResponse;

src/promptUtils.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { type WorkspaceAgent } from "coder/site/src/api/typesGenerated";
22
import * as vscode from "vscode";
33

4+
import { type CoderApi } from "./api/coderApi";
45
import { type MementoManager } from "./core/mementoManager";
6+
import { OAuthMetadataClient } from "./oauth/metadataClient";
7+
8+
type AuthMethod = "oauth" | "legacy";
59

610
/**
711
* Find the requested agent if specified, otherwise return the agent if there
@@ -129,3 +133,54 @@ export async function maybeAskUrl(
129133
}
130134
return url;
131135
}
136+
137+
export async function maybeAskAuthMethod(
138+
client: CoderApi,
139+
): Promise<AuthMethod | undefined> {
140+
// Check if server supports OAuth with progress indication
141+
const supportsOAuth = await vscode.window.withProgress(
142+
{
143+
location: vscode.ProgressLocation.Notification,
144+
title: "Checking authentication methods",
145+
cancellable: false,
146+
},
147+
async () => {
148+
return await OAuthMetadataClient.checkOAuthSupport(
149+
client.getAxiosInstance(),
150+
);
151+
},
152+
);
153+
154+
if (supportsOAuth) {
155+
return await askAuthMethod();
156+
} else {
157+
return "legacy";
158+
}
159+
}
160+
161+
/**
162+
* Ask user to choose between OAuth and legacy API token authentication.
163+
*/
164+
async function askAuthMethod(): Promise<AuthMethod | undefined> {
165+
const choice = await vscode.window.showQuickPick(
166+
[
167+
{
168+
label: "OAuth (Recommended)",
169+
description: "Secure authentication with automatic token refresh",
170+
value: "oauth" as const,
171+
},
172+
{
173+
label: "Session Token (Legacy)",
174+
description: "Generate and paste a session token manually",
175+
value: "legacy" as const,
176+
},
177+
],
178+
{
179+
title: "Select authentication method",
180+
placeHolder: "How would you like to authenticate?",
181+
ignoreFocusOut: true,
182+
},
183+
);
184+
185+
return choice?.value;
186+
}

0 commit comments

Comments
 (0)