From ee5ab9a91fd92f8d56f58e34f7e6f783677fb36d Mon Sep 17 00:00:00 2001 From: "Alex Yang (DevDiv)" Date: Mon, 20 Jul 2026 15:22:39 -0700 Subject: [PATCH] Harden sign-in cancellation behavior Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- extensions/connector-namespaces/auth.mjs | 2 +- extensions/connector-namespaces/auth.test.mjs | 9 +++++++++ extensions/connector-namespaces/test/README.md | 12 ++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/extensions/connector-namespaces/auth.mjs b/extensions/connector-namespaces/auth.mjs index dc5b01fd..0b72597b 100644 --- a/extensions/connector-namespaces/auth.mjs +++ b/extensions/connector-namespaces/auth.mjs @@ -220,7 +220,7 @@ export class InteractiveAuthBroker { cancelSignIn(sessionId) { const session = this.sessions.get(sessionId); - if (!session) return { ok: true }; + if (!session || session.status !== "pending") return { ok: true }; session.status = "cancelled"; session.abortController.abort(); return { ok: true }; diff --git a/extensions/connector-namespaces/auth.test.mjs b/extensions/connector-namespaces/auth.test.mjs index 608769b9..43356644 100644 --- a/extensions/connector-namespaces/auth.test.mjs +++ b/extensions/connector-namespaces/auth.test.mjs @@ -118,6 +118,9 @@ test("interactive sign-in reports pending then done and caches the ARM token", a assert.equal(authenticateOptions.abortSignal.aborted, false); assert.deepEqual(broker.getSignInStatus(started.sessionId), { ok: true, status: "done" }); assert.deepEqual(broker.getSignInStatus(started.sessionId), { ok: true, status: "done" }); + assert.deepEqual(broker.cancelSignIn(started.sessionId), { ok: true }); + assert.equal(authenticateOptions.abortSignal.aborted, false); + assert.deepEqual(broker.getSignInStatus(started.sessionId), { ok: true, status: "done" }); assert.equal(await broker.getToken(), "token"); }); @@ -266,6 +269,12 @@ test("sign-in failures are surfaced through the status endpoint contract", async status: "error", error: "browser launch failed", }); + assert.deepEqual(broker.cancelSignIn(started.sessionId), { ok: true }); + assert.deepEqual(broker.getSignInStatus(started.sessionId), { + ok: false, + status: "error", + error: "browser launch failed", + }); }); test("legacy credential cleanup retries after a transient failure", async () => { diff --git a/extensions/connector-namespaces/test/README.md b/extensions/connector-namespaces/test/README.md index ce68026f..d98d8bd6 100644 --- a/extensions/connector-namespaces/test/README.md +++ b/extensions/connector-namespaces/test/README.md @@ -26,8 +26,12 @@ MCP server issue locally. `~/.copilot/extensions/connector-namespaces/artifacts/gateway-config.json` (`{ subscriptionId, resourceGroup, gatewayName }`). Pick a gateway once in the connector-namespaces canvas, or write that file by hand. -3. **Node 20+** (developed on Node 24). -4. **Extension dependencies installed.** From the repository root, run: +3. **An operating-system secure credential store.** Windows and macOS provide one + by default. Linux and WSL require a Secret Service-compatible keyring, such as + GNOME Keyring, with `libsecret` available. Unencrypted token storage is + intentionally disabled. +4. **Node 20+** (developed on Node 24). +5. **Extension dependencies installed.** From the repository root, run: ```bash npm install --prefix extensions/connector-namespaces @@ -62,8 +66,8 @@ node extensions/connector-namespaces/test/smoke.mjs --limit=5 --open-consent OAuth-backed servers (most of them) need a human to consent **once** in a browser. Azure ARM sign-in is restored from the operating system's encrypted -credential store when available; the one-time behavior below applies to the -connector's own consent. The model: +credential store described in the prerequisites; the one-time behavior below +applies to the connector's own consent. The model: 1. **First run** hits a server that needs consent → the harness prints a consent URL and marks it `NEEDS_CONSENT`. It saves a pending record to