Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@
from published versions since it shows up in the VS Code extension changelog
tab and is confusing to users. Add it back between releases if needed. -->

## Unreleased

### Changed

- Store session tokens in the OS keyring by default on macOS and Windows, in
addition to the extension's own storage. The `coder` CLI reads the same
entry, so once the extension has downloaded the CLI, signing in here also
signs in the CLI. Requires Coder CLI 2.29.0 or later; older CLIs and Linux
keep using a file. To opt out, set `coder.useKeyring` to `false`.
- Ask at logout whether to sign the `coder` CLI out too when it shares the
session, since anything else using that session is signed out with it.
- Pass `coder.useKeyring` to the CLI as `--use-keyring`, so the setting wins
over the `CODER_USE_KEYRING` environment variable.
- Honor `CODER_CONFIG_DIR` like `--global-config` in `coder.globalFlags`.
- Read the `coder` CLI's session only on Coder CLI 2.32.0 or later, up from
2.31.0, where the CLI checks the stored URL against the one you connect to.
- Ask before signing in with the `coder` CLI's session when it belongs to a
different user than your previous session.
- Show an error with **Open Settings** when the CLI cannot store the token at
login, and a **Show Output** button when logout cannot remove every
credential.

### Fixed

- Pass `--allow-redirects` to Coder CLI 2.38.0 or later. The extension already
follows a redirected deployment URL, and without the flag that CLI fails
`coder login`, `coder logout`, and `coder ssh` for it.

## [v1.16.2](https://github.com/coder/vscode-coder/releases/tag/v1.16.2) 2026-08-25

### Fixed
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
"ignoreSync": true
},
"coder.globalFlags": {
"markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item, in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nSupports `${env:VAR}`, `${userHome}`, and a leading `~`. For `--flag=value` items the expansion applies to the value half, so `--cfg=~/coder` works.\n\nSet `--global-config` here to point the CLI at a shared config directory (e.g. `--global-config=~/.config/coderv2` to share login/auth with the Coder CLI); requires a deployment on 2.31.0+ and is ignored when `#coder.useKeyring#` is active. The `--use-keyring` flag is ignored; use `#coder.useKeyring#` instead.\n\nFor `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here.",
"markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item, in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nSupports `${env:VAR}`, `${userHome}`, and a leading `~`. For `--flag=value` items the expansion applies to the value half, so `--cfg=~/coder` works.\n\nTo share a config directory with the `coder` CLI, add `--global-config` here (for example `--global-config=~/.config/coderv2`) or set `CODER_CONFIG_DIR`. Requires Coder CLI 2.32.0 or later. A `--use-keyring` item is ignored; use `#coder.useKeyring#` instead.\n\nFor `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here.",
"type": "array",
"items": {
"type": "string"
Expand All @@ -204,9 +204,9 @@
"ignoreSync": true
},
"coder.useKeyring": {
"markdownDescription": "Store session tokens in the OS keyring (macOS Keychain, Windows Credential Manager) instead of plaintext files. Requires CLI >= 2.29.0 (>= 2.31.0 to sync login from CLI to VS Code). This will attempt to sync between the CLI and VS Code since they share the same keyring entry. It will log you out of the CLI if you log out of the IDE, and vice versa. Has no effect on Linux.",
"markdownDescription": "Store session tokens in the OS keyring (macOS Keychain, Windows Credential Manager) instead of a file. Requires Coder CLI 2.29.0 or later; 2.32.0 or later to sign in with the CLI's existing session. Has no effect on Linux.\n\nThe keyring entry is shared with the `coder` CLI: signing in here also signs in the CLI, and signing out asks whether to sign out the CLI too.",
"type": "boolean",
"default": false,
"default": true,
"scope": "application"
},
"coder.networkThreshold.latencyMs": {
Expand Down
114 changes: 83 additions & 31 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
RECOMMENDED_SSH_SETTINGS,
applySettingOverrides,
} from "./remote/sshOverrides";
import { resolveCliAuth } from "./settings/cli";
import { isKeyringEnabled, resolveCliAuth } from "./settings/cli";
import { appendVsCodeLogs } from "./supportBundle/appendVsCodeLogs";
import {
getRemoteServerDataPath,
Expand Down Expand Up @@ -82,7 +82,7 @@ import type { CliManager } from "./core/cliManager";
import type { ServiceContainer } from "./core/container";
import type { MementoManager } from "./core/mementoManager";
import type { PathResolver } from "./core/pathResolver";
import type { SecretsManager } from "./core/secretsManager";
import type { SecretsManager, SessionAuth } from "./core/secretsManager";
import type { DeploymentManager } from "./deployment/deploymentManager";
import type { Logger } from "./logging/logger";
import type { LoginCoordinator, LoginMethod } from "./login/loginCoordinator";
Expand Down Expand Up @@ -698,24 +698,41 @@ export class Commands {
}

private async performLogout(): Promise<AuthLogoutOutcome> {
if (!this.deploymentManager.isAuthenticated()) {
const deployment = this.deploymentManager.getCurrentDeployment();
if (!this.deploymentManager.isAuthenticated() || !deployment) {
return { success: false, reason: "not_authenticated" };
}

this.logger.debug("Logging out");
const auth = await this.secretsManager.getSessionAuth(
deployment.safeHostname,
);
const signOutCli = await this.askSignOutCli(auth);
if (signOutCli === undefined) {
return { success: false, reason: "user_dismissed" };
}
// Another window may have switched deployments while the prompt was open.
if (this.deploymentManager.getCurrentDeployment()?.url !== deployment.url) {
return { success: false, reason: "not_authenticated" };
}

const deployment = this.deploymentManager.getCurrentDeployment();
this.logger.debug("Logging out");
await this.deploymentManager.clearDeployment("logout");

if (deployment) {
const cleared = await this.cliManager.clearCredentials(deployment.url);
await this.secretsManager.clearAllAuthData(deployment.safeHostname);
if (!cleared) {
vscode.window.showWarningMessage(
const cleared = await this.cliManager.clearCredentials(deployment.url, {
signOutCli,
});
await this.secretsManager.clearAllAuthData(deployment.safeHostname);
if (!cleared) {
vscode.window
.showWarningMessage(
'You\'ve been logged out of Coder, but some credentials could not be removed. Log out again to retry, or run "coder logout" in a terminal.',
);
return { success: false, reason: "cleanup_incomplete" };
}
"Show Output",
)
.then((action) => {
if (action === "Show Output") {
this.logger.show();
}
});
return { success: false, reason: "cleanup_incomplete" };
}

this.showLogoutMessage();
Expand All @@ -735,6 +752,36 @@ export class Commands {
});
}

/** Whether to sign the CLI out too. Asks when it holds this session's token; undefined when dismissed. */
private async askSignOutCli(
auth: SessionAuth | undefined,
): Promise<boolean | undefined> {
if (
!auth?.token ||
!(await this.cliManager.holdsToken(auth.url, auth.token))
) {
return false;
}
// The CLI cannot refresh an OAuth token and logout revokes it, so there is nothing to keep.
if (auth.oauth) {
return true;
}
const action = await vscodeProposed.window.showWarningMessage(
"Sign out of the Coder CLI too?",
{
useCustom: true,
modal: true,
detail: `${auth.url}\n\nThe Coder CLI is signed in with this session. Signing it out also signs out other tools that rely on it.`,
},
"Sign Out",
"Keep Signed In",
);
if (action === undefined) {
return undefined;
}
return action === "Sign Out";
}

/**
* Switch to a different deployment without clearing credentials.
* If login fails or user cancels, stays on current deployment.
Expand Down Expand Up @@ -790,7 +837,11 @@ export class Commands {
const selectedHostname = selected.hostnames[0];
const auth = await this.secretsManager.getSessionAuth(selectedHostname);
if (auth?.url) {
await this.cliManager.clearCredentials(auth.url);
const signOutCli = await this.askSignOutCli(auth);
if (signOutCli === undefined) {
return;
}
await this.cliManager.clearCredentials(auth.url, { signOutCli });
}
await this.secretsManager.clearAllAuthData(selectedHostname);
this.logger.info("Removed credentials for", selectedHostname);
Expand All @@ -803,20 +854,24 @@ export class Commands {
{
useCustom: true,
modal: true,
detail: `This will remove credentials for: ${selected.hostnames.join(", ")}\n\nYou'll need to log in again to access them.`,
detail: `This will remove credentials for: ${selected.hostnames.join(", ")}\n\nYou'll need to log in again to access them.${isKeyringEnabled(vscode.workspace.getConfiguration()) ? " This also signs the Coder CLI out where it shares a session." : ""}`,
},
"Remove All",
);
if (confirm === "Remove All") {
await Promise.all(
selected.hostnames.map(async (h) => {
const auth = await this.secretsManager.getSessionAuth(h);
if (auth?.url) {
await this.cliManager.clearCredentials(auth.url);
}
await this.secretsManager.clearAllAuthData(h);
}),
);
// One at a time: `coder logout` rewrites the whole keyring entry.
for (const h of selected.hostnames) {
const auth = await this.secretsManager.getSessionAuth(h);
if (auth?.url) {
await this.cliManager.clearCredentials(auth.url, {
signOutCli: await this.cliManager.holdsToken(
auth.url,
auth.token,
),
});
}
await this.secretsManager.clearAllAuthData(h);
}
this.logger.info(
"Removed credentials for all deployments:",
selected.hostnames.join(", "),
Expand Down Expand Up @@ -1410,12 +1465,9 @@ export class Commands {
throw new Error("You are not logged in");
}
const safeHost = toSafeHost(baseUrl);
let binary: string;
try {
binary = await this.cliManager.locateBinary(baseUrl);
} catch {
binary = await this.cliManager.fetchBinary(client);
}
const binary =
(await this.cliManager.locateBinary(baseUrl)) ??
(await this.cliManager.fetchBinary(client));
const version = semver.parse(await cliExec.version(binary));
const featureSet = featureSetForVersion(version);
const configDir = this.pathResolver.getGlobalConfigDir(safeHost);
Expand Down
Loading