mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-15 05:36:57 +00:00
Add persistent browser auth to connector canvas
Replace Azure CLI-only authentication with InteractiveBrowserCredential, protected sign-in lifecycle endpoints, and subscription refresh after sign-in. Persist the Azure Identity cache securely across extension reloads and align connector restart guidance with the GitHub Copilot app. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -13,14 +13,15 @@ HTTP endpoint that the extension writes to the Copilot CLI config. The probe
|
||||
uses the configured `X-API-Key`, follows `Mcp-Session-Id`, and accepts standard
|
||||
JSON or SSE JSON-RPC responses.
|
||||
|
||||
The whole point: it runs with **Node and Azure CLI**. No Copilot app, no
|
||||
canvas, no UI. Hand it to anyone (e.g. Arjun) and they can reproduce an MCP
|
||||
server issue locally.
|
||||
The whole point: it runs with **Node and a browser sign-in**. No Copilot app or
|
||||
canvas is required. Hand it to anyone (e.g. Arjun) and they can reproduce an
|
||||
MCP server issue locally.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Azure CLI signed in with `az login`.** The harness asks Azure CLI for the
|
||||
same short-lived ARM token as the extension.
|
||||
1. **A browser for Microsoft Entra sign-in.** The harness opens the same
|
||||
interactive Azure sign-in as the extension when its encrypted Azure Identity
|
||||
session is not already available.
|
||||
2. **A gateway already picked once.** The harness reads gateway coordinates from
|
||||
`~/.copilot/extensions/connector-namespaces/artifacts/gateway-config.json`
|
||||
(`{ subscriptionId, resourceGroup, gatewayName }`). Pick a gateway once in
|
||||
@@ -52,10 +53,12 @@ node extensions/connector-namespaces/test/smoke.mjs --only=WorkIQMail,WorkIQShar
|
||||
node extensions/connector-namespaces/test/smoke.mjs --limit=5 --open-consent
|
||||
```
|
||||
|
||||
## One-time consent, then headless forever
|
||||
## One-time connector consent
|
||||
|
||||
This is the key behavior. OAuth-backed servers (most of them) need a human to
|
||||
consent **once** in a browser. The model:
|
||||
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:
|
||||
|
||||
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
|
||||
@@ -67,8 +70,8 @@ consent **once** in a browser. The model:
|
||||
loopback page is just a redirect target and nothing is listening on it.
|
||||
3. **Re-run the harness.** It sees the pending record, confirms the gateway
|
||||
connection is now `Connected`, finishes the install (mints the API key,
|
||||
writes the CLI entry), and probes it headless. From then on it's reused with
|
||||
zero human interaction.
|
||||
writes the CLI entry), and probes it headless. From then on the connector is
|
||||
reused without repeating its consent.
|
||||
|
||||
So the server taxonomy is:
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// (install.mjs, catalog.mjs, armClient.mjs) and connects through the same native
|
||||
// Streamable HTTP endpoint persisted for the Copilot CLI.
|
||||
//
|
||||
// Runs with `node` and a signed-in Azure CLI — no Copilot app required — so it
|
||||
// can be handed to someone else to reproduce MCP issues. See README.md.
|
||||
// Runs with `node` and an interactive browser sign-in — no Copilot app required
|
||||
// — so it can be handed to someone else to reproduce MCP issues. See README.md.
|
||||
//
|
||||
// Usage:
|
||||
// node extensions/connector-namespaces/test/smoke.mjs [options]
|
||||
@@ -25,6 +25,7 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import { loadSavedConfig } from "../state.mjs";
|
||||
import { getToken } from "../armClient.mjs";
|
||||
import { cancelSignIn, getSignInStatus, isAuthenticationRequiredError, startSignIn } from "../auth.mjs";
|
||||
import { CATEGORY } from "../categories.mjs";
|
||||
import {
|
||||
installConnector,
|
||||
@@ -115,6 +116,31 @@ function logLine(text) {
|
||||
return redact(String(text)).replace(/[\r\n]+/g, " ");
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function signInToAzure() {
|
||||
try {
|
||||
return await getToken();
|
||||
} catch (error) {
|
||||
if (!isAuthenticationRequiredError(error)) throw error;
|
||||
}
|
||||
const started = startSignIn();
|
||||
if (!started.ok || !started.sessionId) {
|
||||
throw new Error(started.error || "Could not start Azure sign-in.");
|
||||
}
|
||||
console.log(`${C.dim}Complete Azure sign-in in the browser window...${C.reset}`);
|
||||
for (let poll = 0; poll < 240; poll++) {
|
||||
const status = getSignInStatus(started.sessionId);
|
||||
if (status.status === "done") return getToken();
|
||||
if (status.status === "error" || status.status === "cancelled" || status.status === "unknown") {
|
||||
throw new Error(status.error || `Azure sign-in ${status.status}.`);
|
||||
}
|
||||
await sleep(2500);
|
||||
}
|
||||
cancelSignIn(started.sessionId);
|
||||
throw new Error("Azure sign-in timed out.");
|
||||
}
|
||||
|
||||
const C = {
|
||||
reset: "\x1b[0m", dim: "\x1b[2m", bold: "\x1b[1m",
|
||||
green: "\x1b[32m", red: "\x1b[31m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
||||
@@ -124,7 +150,7 @@ const tick = (ok) => (ok ? `${C.green}PASS${C.reset}` : `${C.red}FAIL${C.reset}`
|
||||
async function main() {
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
|
||||
// 1. Bootstrap: gateway coords + ARM token (fail fast).
|
||||
// 1. Bootstrap: gateway coords + interactive ARM token (fail fast).
|
||||
const config = loadSavedConfig();
|
||||
if (!config?.subscriptionId || !config?.resourceGroup || !config?.gatewayName) {
|
||||
console.error(`${C.red}No gateway config found.${C.reset} Expected ${join(ARTIFACTS_DIR, "gateway-config.json")}.`);
|
||||
@@ -132,9 +158,9 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
try {
|
||||
await getToken();
|
||||
await signInToAzure();
|
||||
} catch (err) {
|
||||
console.error(`${C.red}Could not get an ARM token.${C.reset} Sign in to Azure when the browser opens.`);
|
||||
console.error(`${C.red}Could not get an ARM token.${C.reset}`);
|
||||
console.error(String(err.message || err).slice(0, 300));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user