From 26b677154b43684dde256ea51a3a423b0a029c9c Mon Sep 17 00:00:00 2001 From: matst80 Date: Sat, 19 Sep 2026 09:18:06 +0000 Subject: [PATCH] dev-auth: add 'get' command; rewrite AGENTS.md dev-auth as quick start + gotchas Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 65 ++++++++++++++++++++++++++++++++------------ scripts/dev-auth.mjs | 28 ++++++++++++++++++- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d38371a..04d243e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,21 +174,52 @@ move to the next. Suggested order: Traces, Logs, Metrics, Search, Trends, Setup. ## Local / agent development without login blocking -Uses the go-auth device grant (RFC 8628); reference: `https://users.k6n.net/dev-auth.md`. -Config is CRD-driven (`https://users.k6n.net/crds.md`), see `k8s/auth-dev.yaml`: -an `AuthClient` `dev-console` with `spec.audience: [https://otel.k6n.net]` -(audience entries must be absolute URLs - `otel-debug` was rejected and briefly -made the AuthServer `READY False`), plus `AuthServer.spec.deviceFlow`. The otel -server accepts that audience via `OTEL_OIDC_AUDIENCE` (`k8s/deployment.yaml`). +### Quick start (agents: do this first) -- **Browser (`npm run dev`)**: `src/dev/device-auth.ts` is imported only under - `import.meta.env.DEV`. With no session it shows a "Dev sign-in" link + code, - polls, then reloads. `/dev-auth/*` is proxied by `vite.config.ts` to the - issuer's `/api/auth/*` (override the issuer with `AUTH_DEV_BASE`). -- **Terminal / agents**: `node scripts/dev-auth.mjs login|token|whoami|forget`. - Refresh token lives in `~/.go-auth/dev/` (0700/0600). Use - `curl -H "Authorization: Bearer $(node scripts/dev-auth.mjs token)" ...`. - `GO_AUTH_TOKEN` is an explicit-token escape hatch; never store it. -- Never commit tokens, never persist them in a production build, never log them. -- Apply with `kubectl apply --dry-run=server --server-side --field-manager=otel-dev-auth -f k8s/auth-dev.yaml` - first; check `kubectl get authserver -n ecommerce` stays `READY True`. +```sh +node scripts/dev-auth.mjs get /api/issues # signs in if needed, then GETs +``` + +If not signed in it prints `Approve in a browser: https://users.k6n.net/device?user_code=XXXX-XXXX`. +**Stop and give the human that link**, then wait - the script polls (~10 min +window) and continues on its own. Refresh token is stored in `~/.go-auth/dev/` +(0700/0600), so this happens once, not per command. Other commands: +`login | token | whoami | forget`. For raw curl: +`curl -H "Authorization: Bearer $(node scripts/dev-auth.mjs token)" https://otel.k6n.net/api/...`. +Env: `OTEL_URL`, `AUTH_DEV_BASE`, `AUTH_DEVICE_CLIENT_ID` (default `dev-console`), +`GO_AUTH_TOKEN` (explicit-token escape hatch, never store). + +Browser (`npm run dev`): with no session a "Dev sign-in" banner shows the same +link + code and reloads once approved. Code is `src/dev/device-auth.ts`, loaded +only under `import.meta.env.DEV`; `/dev-auth/*` is proxied by `vite.config.ts` +to the issuer's `/api/auth/*`. + +### Gotchas (each cost a failed attempt) + +- **Request encoding:** `/api/auth/device` and `/api/auth/device/token` take + **form-encoded** bodies (RFC 8628); only `/api/auth/device/refresh` takes JSON + (`{"refresh_token": ...}`). JSON to the first two gives a fast `invalid_grant` + that looks like a denied approval but is a malformed request. +- **401 `token not issued for this client`:** the otel server rejects the + token's `aud`. It must contain `otel-debug` or `OTEL_OIDC_AUDIENCE` + (`https://otel.k6n.net`, set in `k8s/deployment.yaml`). If it 401s, the + deployed server predates that setting - redeploy with `build.sh`. +- **Never probe the issuer or auth CRDs with ad-hoc curl/kubectl** unless the + user has said to; the harness blocks it. The script above is the sanctioned path. + +### CRD setup (already applied; reference) + +Reference docs: `https://users.k6n.net/dev-auth.md`, `https://users.k6n.net/crds.md`. +`k8s/auth-dev.yaml` defines an `AuthClient` `dev-console` (`redirectURIs: []` +is required even for device-only clients; `spec.audience: [https://otel.k6n.net]`) +plus `AuthServer.spec.deviceFlow`. + +- `audience` entries **must be absolute URLs**. `otel-debug` was rejected by the + operator and flipped the AuthServer to `READY False` until fixed. + `--dry-run=server` does **not** catch this; after applying, always check + `kubectl get authserver -n ecommerce` (READY True) and + `kubectl describe authserver auth -n ecommerce` (events). +- Apply: `kubectl apply --server-side --field-manager=otel-dev-auth -f k8s/auth-dev.yaml` + (a non-fatal "spec.issuer: Required value" warning for the partial AuthServer + apply is expected). +- Never commit tokens, persist them in a production build, or log them. diff --git a/scripts/dev-auth.mjs b/scripts/dev-auth.mjs index 54978b9..0889ce1 100644 --- a/scripts/dev-auth.mjs +++ b/scripts/dev-auth.mjs @@ -7,9 +7,12 @@ // node scripts/dev-auth.mjs token print a fresh access token // node scripts/dev-auth.mjs whoami print the token claims // node scripts/dev-auth.mjs forget delete the stored refresh token +// node scripts/dev-auth.mjs get PATH authenticated GET against the otel API +// (signs in first if needed), e.g. get /api/issues // // Env: AUTH_DEV_BASE (issuer, default https://users.k6n.net), // AUTH_DEVICE_CLIENT_ID (default dev-console), +// OTEL_URL (API for `get`, default https://otel.k6n.net), // GO_AUTH_TOKEN (explicit token escape hatch; `token` prints it as-is). import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; @@ -74,7 +77,30 @@ async function token() { return body.access_token; } +const API = (process.env.OTEL_URL ?? "https://otel.k6n.net").replace(/\/$/, ""); + +async function haveSession() { + try { + await token(); + return true; + } catch { + return false; + } +} + +// Authenticated GET. Signs in first when there is no working session, so an +// agent can just run `get /api/issues` and ask the human to approve the link. +async function get(path) { + if (!path?.startsWith("/")) throw new Error("usage: dev-auth.mjs get /api/..."); + if (!(await haveSession())) await login(); + const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${await token()}` } }); + const text = await res.text(); + if (!res.ok) throw new Error(`HTTP ${res.status} ${text.slice(0, 300)}`); + console.log(text); +} + const cmds = { + get: () => get(process.argv[3]), login, token: async () => console.log(await token()), whoami: async () => { @@ -85,7 +111,7 @@ const cmds = { }; const cmd = cmds[process.argv[2]]; if (!cmd) { - console.error("usage: dev-auth.mjs login|token|whoami|forget"); + console.error("usage: dev-auth.mjs login|token|whoami|forget|get PATH"); process.exit(2); } cmd().catch((e) => {