Skip to content

Commit 03e01fc

Browse files
committed
More login synchronization issues
1 parent 59fecba commit 03e01fc

File tree

3 files changed

+40
-36
lines changed

3 files changed

+40
-36
lines changed

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
343343
registerAuthListener(deployment?.label);
344344

345345
// Update context
346-
contextManager.set("coder.authenticated", deployment !== undefined);
346+
contextManager.set("coder.authenticated", Boolean(deployment));
347347

348348
// Refresh workspaces
349349
myWorkspacesProvider.fetchAndRefresh();

src/login/loginCoordinator.ts

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,28 @@ export class LoginCoordinator {
179179
isAutoLogin: boolean,
180180
oauthSessionManager: OAuthSessionManager,
181181
): Promise<LoginResult> {
182-
const token = await this.secretsManager.getSessionToken(deployment.label);
183-
const client = CoderApi.create(deployment.url, token, this.logger);
184182
const needsToken = needToken(vscode.workspace.getConfiguration());
185-
if (!needsToken || token) {
186-
try {
187-
const user = await client.getAuthenticatedUser();
188-
// For non-token auth, we write a blank token since the `vscodessh`
189-
// command currently always requires a token file.
190-
// For token auth, we have valid access so we can just return the user here
191-
return { success: true, token: needsToken && token ? token : "", user };
192-
} catch (err) {
183+
const client = CoderApi.create(deployment.url, "", this.logger);
184+
185+
let storedToken: string | undefined;
186+
if (needsToken) {
187+
storedToken = await this.secretsManager.getSessionToken(deployment.label);
188+
if (storedToken) {
189+
client.setSessionToken(storedToken);
190+
}
191+
}
192+
193+
// Attempt authentication with current credentials (token or mTLS)
194+
try {
195+
const user = await client.getAuthenticatedUser();
196+
// Return the token that was used (empty string for mTLS since
197+
// the `vscodessh` command currently always requires a token file)
198+
return { success: true, token: storedToken ?? "", user };
199+
} catch (err) {
200+
if (needsToken) {
201+
// For token auth: silently continue to prompt for new credentials
202+
} else {
203+
// For mTLS: show error and abort (no credentials to prompt for)
193204
const message = getErrorMessage(err, "no response from the server");
194205
if (isAutoLogin) {
195206
this.logger.warn("Failed to log in to Coder server:", message);
@@ -203,7 +214,6 @@ export class LoginCoordinator {
203214
},
204215
);
205216
}
206-
// Invalid certificate, most likely.
207217
return { success: false };
208218
}
209219
}
@@ -212,12 +222,8 @@ export class LoginCoordinator {
212222
switch (authMethod) {
213223
case "oauth":
214224
return this.loginWithOAuth(client, oauthSessionManager, deployment);
215-
case "legacy": {
216-
const initialToken =
217-
token ||
218-
(await this.secretsManager.getSessionToken(deployment.label));
219-
return this.loginWithToken(client, initialToken);
220-
}
225+
case "legacy":
226+
return this.loginWithToken(client);
221227
case undefined:
222228
return { success: false }; // User aborted
223229
}
@@ -226,10 +232,7 @@ export class LoginCoordinator {
226232
/**
227233
* Session token authentication flow.
228234
*/
229-
private async loginWithToken(
230-
client: CoderApi,
231-
initialToken: string | undefined,
232-
): Promise<LoginResult> {
235+
private async loginWithToken(client: CoderApi): Promise<LoginResult> {
233236
const url = client.getAxiosInstance().defaults.baseURL;
234237
if (!url) {
235238
throw new Error("No base URL set on REST client");
@@ -246,7 +249,6 @@ export class LoginCoordinator {
246249
title: "Coder API Key",
247250
password: true,
248251
placeHolder: "Paste your API key.",
249-
value: initialToken,
250252
ignoreFocusOut: true,
251253
validateInput: async (value) => {
252254
if (!value) {
@@ -315,10 +317,15 @@ export class LoginCoordinator {
315317
user,
316318
};
317319
} catch (error) {
318-
this.logger.error("OAuth authentication failed:", error);
319-
vscode.window.showErrorMessage(
320-
`OAuth authentication failed: ${getErrorMessage(error, "Unknown error")}`,
321-
);
320+
const title = "OAuth authentication failed";
321+
this.logger.error(title, error);
322+
if (error instanceof CertificateError) {
323+
error.showNotification(title);
324+
} else {
325+
vscode.window.showErrorMessage(
326+
`${title}: ${getErrorMessage(error, "Unknown error")}`,
327+
);
328+
}
322329
return { success: false };
323330
}
324331
}

src/oauth/sessionManager.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ export class OAuthSessionManager implements vscode.Disposable {
132132
stored: tokens.deployment_url,
133133
current: this.deployment.url,
134134
});
135-
await this.clearOAuthState();
135+
this.clearInMemoryTokens();
136+
await this.secretsManager.clearOAuthData(this.deployment.label);
136137
return;
137138
}
138139

@@ -144,6 +145,7 @@ export class OAuthSessionManager implements vscode.Disposable {
144145
required_scopes: DEFAULT_OAUTH_SCOPES,
145146
},
146147
);
148+
this.clearInMemoryTokens();
147149
await this.secretsManager.clearOAuthTokens(this.deployment.label);
148150
return;
149151
}
@@ -152,13 +154,6 @@ export class OAuthSessionManager implements vscode.Disposable {
152154
this.logger.info(`Loaded stored OAuth tokens for ${this.deployment.label}`);
153155
}
154156

155-
private async clearOAuthState(): Promise<void> {
156-
this.clearInMemoryTokens();
157-
if (this.deployment) {
158-
await this.secretsManager.clearOAuthData(this.deployment.label);
159-
}
160-
}
161-
162157
private clearInMemoryTokens(): void {
163158
this.storedTokens = undefined;
164159
this.refreshPromise = null;
@@ -714,13 +709,15 @@ export class OAuthSessionManager implements vscode.Disposable {
714709
// Revoke refresh token (which also invalidates access token per RFC 7009)
715710
if (this.storedTokens?.refresh_token) {
716711
try {
712+
// TODO what if other windows are using this?
713+
// We should only revoke if we are clearing the OAuth data
717714
await this.revokeToken(this.storedTokens.refresh_token);
718715
} catch (error) {
719716
this.logger.warn("Token revocation failed during logout:", error);
720717
}
721718
}
722719

723-
await this.clearOAuthState();
720+
this.clearInMemoryTokens();
724721
this.deployment = undefined;
725722

726723
this.logger.info("OAuth logout complete");

0 commit comments

Comments
 (0)