Add device-grant dev sign-in (CRD-driven) for local/agent development
AuthClient dev-console + AuthServer deviceFlow via k8s/auth-dev.yaml; otel server accepts its own URL as an extra audience (OTEL_OIDC_AUDIENCE). Vite dev-only device-flow sign-in, scripts/dev-auth.mjs helper, AGENTS.md notes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fde8a0f92
commit
bd17837663
10 files changed
+305
-3
No files matched your search
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
// Terminal helper for agent/local development against the protected API.
|
||||
// Device grant (RFC 8628) against the go-auth issuer. The refresh token is
|
||||
// kept in ~/.go-auth/dev/<client>.json (0700 dir, 0600 file). Never commit it.
|
||||
//
|
||||
// node scripts/dev-auth.mjs login approve once in a browser
|
||||
// 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
|
||||
//
|
||||
// Env: AUTH_DEV_BASE (issuer, default https://users.k6n.net),
|
||||
// AUTH_DEVICE_CLIENT_ID (default dev-console),
|
||||
// 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";
|
||||
import { join } from "node:path";
|
||||
|
||||
const BASE = (process.env.AUTH_DEV_BASE ?? "https://users.k6n.net").replace(/\/$/, "");
|
||||
const CLIENT = process.env.AUTH_DEVICE_CLIENT_ID ?? "dev-console";
|
||||
const DIR = join(homedir(), ".go-auth", "dev");
|
||||
const FILE = join(DIR, `${CLIENT}.json`);
|
||||
const GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
||||
|
||||
const post = (path, body) =>
|
||||
fetch(`${BASE}/api/auth${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
async function save(refresh) {
|
||||
await mkdir(DIR, { recursive: true, mode: 0o700 });
|
||||
await chmod(DIR, 0o700);
|
||||
const tmp = `${FILE}.tmp`;
|
||||
await writeFile(tmp, JSON.stringify({ refresh_token: refresh }), { mode: 0o600 });
|
||||
await rename(tmp, FILE);
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const start = await post("/device", { client_id: CLIENT });
|
||||
if (!start.ok) throw new Error(`device start failed: HTTP ${start.status}`);
|
||||
const d = await start.json();
|
||||
console.error(`Approve in a browser: ${d.verification_uri_complete ?? d.verification_uri}\nCode: ${d.user_code}`);
|
||||
let interval = (d.interval ?? 5) * 1000;
|
||||
const deadline = Date.now() + d.expires_in * 1000;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, interval));
|
||||
const t = await post("/device/token", { grant_type: GRANT, device_code: d.device_code, client_id: CLIENT });
|
||||
const body = await t.json().catch(() => ({}));
|
||||
if (t.ok) {
|
||||
await save(body.refresh_token);
|
||||
console.error("Signed in; refresh token stored.");
|
||||
return;
|
||||
}
|
||||
if (body.error === "slow_down") interval += 5000;
|
||||
else if (body.error !== "authorization_pending") throw new Error(body.error ?? `HTTP ${t.status}`);
|
||||
}
|
||||
throw new Error("device code expired");
|
||||
}
|
||||
|
||||
async function token() {
|
||||
if (process.env.GO_AUTH_TOKEN) return process.env.GO_AUTH_TOKEN;
|
||||
let stored;
|
||||
try {
|
||||
stored = JSON.parse(await readFile(FILE, "utf8"));
|
||||
} catch {
|
||||
throw new Error("not signed in: run `node scripts/dev-auth.mjs login`");
|
||||
}
|
||||
const res = await post("/device/refresh", { client_id: CLIENT, refresh_token: stored.refresh_token });
|
||||
if (!res.ok) throw new Error(`refresh failed (HTTP ${res.status}); run login again`);
|
||||
const body = await res.json();
|
||||
if (body.refresh_token) await save(body.refresh_token);
|
||||
return body.access_token;
|
||||
}
|
||||
|
||||
const cmds = {
|
||||
login,
|
||||
token: async () => console.log(await token()),
|
||||
whoami: async () => {
|
||||
const t = await token();
|
||||
console.log(JSON.stringify(JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()), null, 2));
|
||||
},
|
||||
forget: () => rm(FILE, { force: true }),
|
||||
};
|
||||
const cmd = cmds[process.argv[2]];
|
||||
if (!cmd) {
|
||||
console.error("usage: dev-auth.mjs login|token|whoami|forget");
|
||||
process.exit(2);
|
||||
}
|
||||
cmd().catch((e) => {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in new issue
Block a user