@broberg/sso 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.cjs +24 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -540,7 +540,7 @@ const tickets = createTicketVerifier({
|
|
|
540
540
|
|
|
541
541
|
try {
|
|
542
542
|
const who = await tickets.verify(bearer, { scope: "discovery:read-fleet" });
|
|
543
|
-
// { principal: "svc-trail", type: "service", clientId, org, act, scopes, exp, jti }
|
|
543
|
+
// { principal: "svc-trail", type: "service", clientId, org, act, version, cnf, scopes, exp, jti }
|
|
544
544
|
} catch (e) {
|
|
545
545
|
if (e instanceof JwksUnavailableError) return c.text("try again", 503); // could not ask BID
|
|
546
546
|
if (e instanceof SsoError) return c.text("forbidden", 401); // the ticket is not acceptable
|
|
@@ -553,6 +553,12 @@ try {
|
|
|
553
553
|
- **BID briefly down:** a key already in the cache still verifies, with no network call. An unknown key while BID is down throws `JwksUnavailableError` — answer 503, do not reject the caller.
|
|
554
554
|
- Not a login client: a service that only receives tickets needs no `client_id`, no redirect and no session.
|
|
555
555
|
|
|
556
|
+
**Since 0.15.0 (F084.156, broberg-id-F087.15) — read for the future, never shortened:**
|
|
557
|
+
|
|
558
|
+
- `act` is the **whole delegation chain** (RFC 8693): agent-b acting for agent-a acting for user-1 is `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` is the same as before. A chain deeper than 5 links, or a link without a string `sub`, is **rejected** — 0.14.x kept only the first link and read a malformed `act` as "no delegation", both in the direction where the caller looks like it acts for fewer people.
|
|
559
|
+
- `version` is the ticket format (`ver`): 0 for a ticket from before BID set one, 1 today. A `ver` newer than this package understands is **rejected by name** ("upgrade @broberg/sso"), never read with today's rules.
|
|
560
|
+
- `cnf` is `{ jkt }` for a DPoP-bound ticket, else `null`. **It is not checked here**: until your service verifies a DPoP proof against it (F087.16), a bound ticket is accepted exactly like a bearer ticket, and a stolen one works. Do not treat `cnf` as protection yet.
|
|
561
|
+
|
|
556
562
|
## Getting a ticket — `fetchTicket` (since 0.14.1 — 0.14.0 was tagged but never reached npm)
|
|
557
563
|
|
|
558
564
|
The sending half: a workload on **Fly** or in **GitHub Actions** gets a Broberg ID ticket with no key at all. It proves who it is with the identity its platform already gives it, and BID exchanges that (RFC 8693). Nothing secret is stored anywhere.
|
package/dist/index.cjs
CHANGED
|
@@ -142,6 +142,8 @@ function fetchTicket(opts) {
|
|
|
142
142
|
return shared.get(opts);
|
|
143
143
|
}
|
|
144
144
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
145
|
+
var TICKET_FORMAT_VERSION = 1;
|
|
146
|
+
var MAX_ACT_DEPTH = 5;
|
|
145
147
|
function createTicketVerifier(options) {
|
|
146
148
|
const {
|
|
147
149
|
issuer,
|
|
@@ -223,14 +225,24 @@ function createTicketVerifier(options) {
|
|
|
223
225
|
const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
224
226
|
const missing = required.filter((s) => !scopes.includes(s));
|
|
225
227
|
if (missing.length) throw new chunkJGIO7XPR_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
+
const ver = payload.ver === void 0 ? 0 : payload.ver;
|
|
229
|
+
if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
|
|
230
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
|
|
231
|
+
}
|
|
232
|
+
if (ver > TICKET_FORMAT_VERSION) {
|
|
233
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
|
|
234
|
+
}
|
|
235
|
+
const act = payload.act === void 0 ? null : readActor(payload.act, 1);
|
|
236
|
+
const cnfRaw = payload.cnf;
|
|
237
|
+
const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
|
|
228
238
|
return {
|
|
229
239
|
principal: payload.sub,
|
|
230
240
|
type,
|
|
231
241
|
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
232
242
|
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
233
243
|
act,
|
|
244
|
+
version: ver,
|
|
245
|
+
cnf,
|
|
234
246
|
scopes,
|
|
235
247
|
exp: payload.exp,
|
|
236
248
|
jti: String(payload.jti)
|
|
@@ -238,6 +250,14 @@ function createTicketVerifier(options) {
|
|
|
238
250
|
}
|
|
239
251
|
};
|
|
240
252
|
}
|
|
253
|
+
function readActor(raw, depth) {
|
|
254
|
+
if (depth > MAX_ACT_DEPTH) throw new chunkJGIO7XPR_cjs.SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
|
|
255
|
+
const link = raw;
|
|
256
|
+
if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
|
|
257
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket's act (link ${depth}) has no string sub`);
|
|
258
|
+
}
|
|
259
|
+
return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
|
|
260
|
+
}
|
|
241
261
|
|
|
242
262
|
Object.defineProperty(exports, "AVATAR_TYPES", {
|
|
243
263
|
enumerable: true,
|
|
@@ -332,7 +352,9 @@ Object.defineProperty(exports, "verifyValue", {
|
|
|
332
352
|
get: function () { return chunkJGIO7XPR_cjs.verifyValue; }
|
|
333
353
|
});
|
|
334
354
|
exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
|
|
355
|
+
exports.MAX_ACT_DEPTH = MAX_ACT_DEPTH;
|
|
335
356
|
exports.NoWorkloadIdentityError = NoWorkloadIdentityError;
|
|
357
|
+
exports.TICKET_FORMAT_VERSION = TICKET_FORMAT_VERSION;
|
|
336
358
|
exports.TicketExchangeError = TicketExchangeError;
|
|
337
359
|
exports.TicketUnavailableError = TicketUnavailableError;
|
|
338
360
|
exports.createTicketClient = createTicketClient;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":["request","existsSync","SsoError","createJwksCache","JwksUnavailableError","decodeProtectedHeader","jwtVerify","ALLOWED_ALGS"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */\n act: { sub: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n const actRaw = payload.act as { sub?: unknown } | undefined;\n const act = actRaw && typeof actRaw.sub === \"string\" && actRaw.sub ? { sub: actRaw.sub } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":["request","existsSync","SsoError","createJwksCache","JwksUnavailableError","decodeProtectedHeader","jwtVerify","ALLOWED_ALGS"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/**\n * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link\n * outward: agent-b acting for agent-a acting for user-1 reads\n * `{ sub: \"agent-a\", act: { sub: \"user-1\" } }`. `act.sub` alone is the old shape.\n */\nexport interface TicketActor {\n sub: string;\n act?: TicketActor;\n}\n\n/** The newest ticket format version this package understands (BID sets `ver`). */\nexport const TICKET_FORMAT_VERSION = 1;\n/** Delegation deeper than this is refused rather than shortened. */\nexport const MAX_ACT_DEPTH = 5;\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */\n act: TicketActor | null;\n /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */\n version: number;\n /**\n * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.\n * NOT checked here: until the receiver verifies a DPoP proof against it\n * (F087.16), a bound ticket is accepted exactly like a bearer ticket.\n */\n cnf: { jkt: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n // F084.156 — ver: unknown future formats are refused by name, never read\n // with today's rules.\n const ver = payload.ver === undefined ? 0 : payload.ver;\n if (typeof ver !== \"number\" || !Number.isInteger(ver) || ver < 0) {\n throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);\n }\n if (ver > TICKET_FORMAT_VERSION) {\n throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) — upgrade @broberg/sso`);\n }\n // The whole chain, or a refusal. 0.14.2 kept only act.sub, cutting a chain\n // to its first link, and read a malformed act as \"no delegation\" — both in\n // the direction where the caller looks like it acts for fewer people.\n const act = payload.act === undefined ? null : readActor(payload.act, 1);\n const cnfRaw = payload.cnf as { jkt?: unknown } | undefined;\n const cnf = cnfRaw && typeof cnfRaw.jkt === \"string\" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n version: ver,\n cnf,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n\nfunction readActor(raw: unknown, depth: number): TicketActor {\n if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);\n const link = raw as { sub?: unknown; act?: unknown } | null;\n if (!link || typeof link !== \"object\" || typeof link.sub !== \"string\" || link.sub === \"\") {\n throw new SsoError(`ticket's act (link ${depth}) has no string sub`);\n }\n return link.act === undefined ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -48,6 +48,19 @@ declare function fetchTicket(opts: {
|
|
|
48
48
|
}): Promise<string>;
|
|
49
49
|
|
|
50
50
|
type PrincipalType = "human" | "service" | "agent";
|
|
51
|
+
/**
|
|
52
|
+
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
53
|
+
* outward: agent-b acting for agent-a acting for user-1 reads
|
|
54
|
+
* `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
|
|
55
|
+
*/
|
|
56
|
+
interface TicketActor {
|
|
57
|
+
sub: string;
|
|
58
|
+
act?: TicketActor;
|
|
59
|
+
}
|
|
60
|
+
/** The newest ticket format version this package understands (BID sets `ver`). */
|
|
61
|
+
declare const TICKET_FORMAT_VERSION = 1;
|
|
62
|
+
/** Delegation deeper than this is refused rather than shortened. */
|
|
63
|
+
declare const MAX_ACT_DEPTH = 5;
|
|
51
64
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
52
65
|
interface TicketPrincipal {
|
|
53
66
|
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
@@ -57,9 +70,17 @@ interface TicketPrincipal {
|
|
|
57
70
|
clientId: string;
|
|
58
71
|
/** The organisation, when BID stated one. */
|
|
59
72
|
org: string | null;
|
|
60
|
-
/** On delegation: who the caller is acting for (RFC 8693 `act
|
|
61
|
-
act:
|
|
62
|
-
|
|
73
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
|
|
74
|
+
act: TicketActor | null;
|
|
75
|
+
/** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
|
|
76
|
+
version: number;
|
|
77
|
+
/**
|
|
78
|
+
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
79
|
+
* NOT checked here: until the receiver verifies a DPoP proof against it
|
|
80
|
+
* (F087.16), a bound ticket is accepted exactly like a bearer ticket.
|
|
81
|
+
*/
|
|
82
|
+
cnf: {
|
|
83
|
+
jkt: string;
|
|
63
84
|
} | null;
|
|
64
85
|
scopes: string[];
|
|
65
86
|
/** Seconds since the epoch. */
|
|
@@ -94,4 +115,4 @@ interface TicketVerifier {
|
|
|
94
115
|
}
|
|
95
116
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
96
117
|
|
|
97
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
118
|
+
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
package/dist/index.d.ts
CHANGED
|
@@ -48,6 +48,19 @@ declare function fetchTicket(opts: {
|
|
|
48
48
|
}): Promise<string>;
|
|
49
49
|
|
|
50
50
|
type PrincipalType = "human" | "service" | "agent";
|
|
51
|
+
/**
|
|
52
|
+
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
53
|
+
* outward: agent-b acting for agent-a acting for user-1 reads
|
|
54
|
+
* `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
|
|
55
|
+
*/
|
|
56
|
+
interface TicketActor {
|
|
57
|
+
sub: string;
|
|
58
|
+
act?: TicketActor;
|
|
59
|
+
}
|
|
60
|
+
/** The newest ticket format version this package understands (BID sets `ver`). */
|
|
61
|
+
declare const TICKET_FORMAT_VERSION = 1;
|
|
62
|
+
/** Delegation deeper than this is refused rather than shortened. */
|
|
63
|
+
declare const MAX_ACT_DEPTH = 5;
|
|
51
64
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
52
65
|
interface TicketPrincipal {
|
|
53
66
|
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
@@ -57,9 +70,17 @@ interface TicketPrincipal {
|
|
|
57
70
|
clientId: string;
|
|
58
71
|
/** The organisation, when BID stated one. */
|
|
59
72
|
org: string | null;
|
|
60
|
-
/** On delegation: who the caller is acting for (RFC 8693 `act
|
|
61
|
-
act:
|
|
62
|
-
|
|
73
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
|
|
74
|
+
act: TicketActor | null;
|
|
75
|
+
/** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
|
|
76
|
+
version: number;
|
|
77
|
+
/**
|
|
78
|
+
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
79
|
+
* NOT checked here: until the receiver verifies a DPoP proof against it
|
|
80
|
+
* (F087.16), a bound ticket is accepted exactly like a bearer ticket.
|
|
81
|
+
*/
|
|
82
|
+
cnf: {
|
|
83
|
+
jkt: string;
|
|
63
84
|
} | null;
|
|
64
85
|
scopes: string[];
|
|
65
86
|
/** Seconds since the epoch. */
|
|
@@ -94,4 +115,4 @@ interface TicketVerifier {
|
|
|
94
115
|
}
|
|
95
116
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
96
117
|
|
|
97
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
118
|
+
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, TICKET_FORMAT_VERSION, type TicketActor, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
package/dist/index.js
CHANGED
|
@@ -141,6 +141,8 @@ function fetchTicket(opts) {
|
|
|
141
141
|
return shared.get(opts);
|
|
142
142
|
}
|
|
143
143
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
144
|
+
var TICKET_FORMAT_VERSION = 1;
|
|
145
|
+
var MAX_ACT_DEPTH = 5;
|
|
144
146
|
function createTicketVerifier(options) {
|
|
145
147
|
const {
|
|
146
148
|
issuer,
|
|
@@ -222,14 +224,24 @@ function createTicketVerifier(options) {
|
|
|
222
224
|
const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
223
225
|
const missing = required.filter((s) => !scopes.includes(s));
|
|
224
226
|
if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
+
const ver = payload.ver === void 0 ? 0 : payload.ver;
|
|
228
|
+
if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
|
|
229
|
+
throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
|
|
230
|
+
}
|
|
231
|
+
if (ver > TICKET_FORMAT_VERSION) {
|
|
232
|
+
throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
|
|
233
|
+
}
|
|
234
|
+
const act = payload.act === void 0 ? null : readActor(payload.act, 1);
|
|
235
|
+
const cnfRaw = payload.cnf;
|
|
236
|
+
const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
|
|
227
237
|
return {
|
|
228
238
|
principal: payload.sub,
|
|
229
239
|
type,
|
|
230
240
|
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
231
241
|
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
232
242
|
act,
|
|
243
|
+
version: ver,
|
|
244
|
+
cnf,
|
|
233
245
|
scopes,
|
|
234
246
|
exp: payload.exp,
|
|
235
247
|
jti: String(payload.jti)
|
|
@@ -237,7 +249,15 @@ function createTicketVerifier(options) {
|
|
|
237
249
|
}
|
|
238
250
|
};
|
|
239
251
|
}
|
|
252
|
+
function readActor(raw, depth) {
|
|
253
|
+
if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
|
|
254
|
+
const link = raw;
|
|
255
|
+
if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
|
|
256
|
+
throw new SsoError(`ticket's act (link ${depth}) has no string sub`);
|
|
257
|
+
}
|
|
258
|
+
return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
|
|
259
|
+
}
|
|
240
260
|
|
|
241
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
|
|
261
|
+
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, TICKET_FORMAT_VERSION, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
|
|
242
262
|
//# sourceMappingURL=index.js.map
|
|
243
263
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act.sub`). */\n act: { sub: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n const actRaw = payload.act as { sub?: unknown } | undefined;\n const act = actRaw && typeof actRaw.sub === \"string\" && actRaw.sub ? { sub: actRaw.sub } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fetch-ticket.ts","../src/ticket.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * F084.154 — fetch a Broberg ID ticket WITHOUT a key (broberg-id-F087.10/.11).\n *\n * The sending half of F084.153: a workload proves who it is with the identity its\n * platform already gives it, and BID exchanges that for a short-lived ticket\n * (RFC 8693 token exchange). No secret is stored anywhere.\n *\n * Fly.io POST unix:/.fly/api /v1/tokens/oidc {aud: issuer}\n * GitHub Actions GET $ACTIONS_ID_TOKEN_REQUEST_URL&audience=issuer\n * (needs `permissions: id-token: write` in the workflow)\n * then POST {issuer}/oauth2/token grant_type=…token-exchange\n *\n * Reference implementation: broberg-id/.github/workflows/ticket-probe.yml.\n * The ticket itself is never logged or put in an error message.\n */\nimport { existsSync } from \"node:fs\";\nimport { request } from \"node:http\";\n\nexport const DEFAULT_ISSUER = \"https://id.broberg.ai\";\nconst FLY_SOCKET = \"/.fly/api\";\n\n/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */\nexport class NoWorkloadIdentityError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"NoWorkloadIdentityError\";\n }\n}\n\n/**\n * BID refused the exchange. `code` is BID's own error name (the part of\n * error_description before the colon, e.g. `no_rule_for_audience`), `status` the\n * HTTP status. A 4xx is PERMANENT for this request — fix the rule, do not retry.\n */\nexport class TicketExchangeError extends Error {\n constructor(message: string, readonly code: string, readonly status: number) {\n super(message);\n this.name = \"TicketExchangeError\";\n }\n}\n\n/** BID or the platform could not be reached, or answered 5xx. TRANSIENT — retry later. */\nexport class TicketUnavailableError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TicketUnavailableError\";\n }\n}\n\nexport interface TicketClientOptions {\n issuer?: string;\n /** Renew this many seconds before exp. Default 30. */\n renewBeforeSec?: number;\n /** Injectable for tests. */\n env?: Record<string, string | undefined>;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Ask Fly's machine API for an OIDC token. Default: POST over the /.fly/api unix socket. */\n flyOidc?: (aud: string) => Promise<string>;\n /** Whether the Fly socket is present. Default: checks /.fly/api. */\n onFly?: () => boolean;\n}\n\nexport interface TicketClient {\n /** A ticket for `audience` (BID's short name, e.g. \"discovery\"), from cache while it is fresh. */\n get(opts: { audience: string; scope?: string }): Promise<string>;\n}\n\nfunction flyOidcOverSocket(aud: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const body = JSON.stringify({ aud });\n const req = request(\n { socketPath: FLY_SOCKET, path: \"/v1/tokens/oidc\", method: \"POST\", headers: { \"content-type\": \"application/json\", \"content-length\": Buffer.byteLength(body) } },\n (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (c) => (data += c));\n res.on(\"end\", () => {\n if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());\n else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));\n });\n },\n );\n req.on(\"error\", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));\n req.end(body);\n });\n}\n\n/** Seconds since the epoch at which a JWT expires, or null if it cannot be read. */\nfunction jwtExp(token: string): number | null {\n try {\n const payload = JSON.parse(Buffer.from(token.split(\".\")[1] ?? \"\", \"base64url\").toString(\"utf8\"));\n return typeof payload.exp === \"number\" ? payload.exp : null;\n } catch {\n return null;\n }\n}\n\nexport function createTicketClient(options: TicketClientOptions = {}): TicketClient {\n const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\\/+$/, \"\");\n const renewBeforeSec = options.renewBeforeSec ?? 30;\n const env = options.env ?? process.env;\n const fetchImpl = options.fetchImpl ?? fetch;\n const now = options.now ?? (() => Date.now());\n const flyOidc = options.flyOidc ?? flyOidcOverSocket;\n const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));\n\n const cache = new Map<string, { token: string; exp: number }>();\n const inFlight = new Map<string, Promise<string>>();\n\n async function platformToken(): Promise<string> {\n if (onFly()) return flyOidc(issuer);\n const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (url && bearer) {\n let res: Response;\n try {\n res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });\n } catch (e) {\n throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);\n const { value } = (await res.json()) as { value?: string };\n if (!value) throw new TicketUnavailableError(\"GitHub OIDC endpoint returned no token\");\n return value;\n }\n throw new NoWorkloadIdentityError(\n \"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job \" +\n \"(ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN — add `permissions: id-token: write`). A Mac session keeps using its session key.\",\n );\n }\n\n async function exchange(audience: string, scope: string | undefined): Promise<{ token: string; exp: number }> {\n const subject = await platformToken();\n const form = new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:token-exchange\",\n subject_token: subject,\n subject_token_type: \"urn:ietf:params:oauth:token-type:jwt\",\n audience,\n });\n if (scope) form.set(\"scope\", scope);\n let res: Response;\n try {\n res = await fetchImpl(`${issuer}/oauth2/token`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: form,\n });\n } catch (e) {\n throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);\n }\n let body: { access_token?: string; expires_in?: number; error?: string; error_description?: string } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n /* a non-JSON answer is handled by the status checks below */\n }\n if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);\n if (!res.ok || !body.access_token) {\n const desc = body.error_description ?? \"\";\n const code = desc.includes(\":\") ? desc.slice(0, desc.indexOf(\":\")).trim() : body.error ?? \"exchange_refused\";\n throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);\n }\n const nowSec = Math.floor(now() / 1000);\n const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);\n return { token: body.access_token, exp };\n }\n\n return {\n async get({ audience, scope }) {\n const key = `${audience} ${scope ?? \"\"}`;\n const hit = cache.get(key);\n if (hit && hit.exp - renewBeforeSec > now() / 1000) return hit.token;\n const pending = inFlight.get(key);\n if (pending) return pending;\n const p = exchange(audience, scope)\n .then((t) => {\n cache.set(key, t);\n return t.token;\n })\n .finally(() => inFlight.delete(key));\n inFlight.set(key, p);\n return p;\n },\n };\n}\n\nlet shared: TicketClient | null = null;\n/** A ticket from the process-wide client (one cache per process). */\nexport function fetchTicket(opts: { audience: string; scope?: string }): Promise<string> {\n shared ??= createTicketClient();\n return shared.get(opts);\n}\n","/**\n * F084.153 — verify a Broberg ID ticket LOCALLY (broberg-id-F087.4).\n *\n * A ticket is how one service or agent proves who it is to another without a\n * shared secret: Broberg ID signs a short-lived JWT, the receiver checks it\n * against BID's PUBLIC keys. Nothing secret lives at the receiver.\n *\n * Format: broberg-id docs/features/F087.1-identitetsmodel.md §3 (draft, 8/10).\n * iss = the issuer · aud = the receiver's URL · scope = space-separated\n * sub = client_id = the caller · principal_type = human | service | agent\n * org · act {sub} only on delegation (RFC 8693) · exp/iat/jti\n *\n * This is deliberately NOT a method on createSsoClient: a service that only\n * RECEIVES tickets is not a login client — it has no client_id, no redirect,\n * no session. It needs the issuer, its own audience, and the key set.\n *\n * TWO FAILURES, TWO ANSWERS — keep them apart in the caller:\n * JwksUnavailableError → we could not ask BID right now. Answer 503 and let\n * the caller retry; the ticket may be perfectly valid.\n * SsoError → the ticket is not acceptable. Answer 401/403.\n * A known key is served from cache, so a ticket signed with it verifies while\n * BID is briefly unreachable.\n */\nimport { decodeProtectedHeader, jwtVerify, type JWTPayload } from \"jose\";\nimport { ALLOWED_ALGS, SsoError } from \"./client.js\";\nimport { createJwksCache, JwksUnavailableError, type JwksCache } from \"./jwks.js\";\n\nexport type PrincipalType = \"human\" | \"service\" | \"agent\";\nconst PRINCIPAL_TYPES: readonly PrincipalType[] = [\"human\", \"service\", \"agent\"];\n\n/**\n * F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link\n * outward: agent-b acting for agent-a acting for user-1 reads\n * `{ sub: \"agent-a\", act: { sub: \"user-1\" } }`. `act.sub` alone is the old shape.\n */\nexport interface TicketActor {\n sub: string;\n act?: TicketActor;\n}\n\n/** The newest ticket format version this package understands (BID sets `ver`). */\nexport const TICKET_FORMAT_VERSION = 1;\n/** Delegation deeper than this is refused rather than shortened. */\nexport const MAX_ACT_DEPTH = 5;\n\n/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */\nexport interface TicketPrincipal {\n /** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */\n principal: string;\n type: PrincipalType;\n /** The client the ticket was issued to (`client_id`, falling back to `sub`). */\n clientId: string;\n /** The organisation, when BID stated one. */\n org: string | null;\n /** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */\n act: TicketActor | null;\n /** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */\n version: number;\n /**\n * F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.\n * NOT checked here: until the receiver verifies a DPoP proof against it\n * (F087.16), a bound ticket is accepted exactly like a bearer ticket.\n */\n cnf: { jkt: string } | null;\n scopes: string[];\n /** Seconds since the epoch. */\n exp: number;\n jti: string;\n}\n\nexport interface TicketVerifierOptions {\n /** BID's issuer, exactly as it signs `iss` — e.g. `https://id.broberg.ai`. */\n issuer: string;\n /** THIS receiver's audience — its own URL, e.g. `https://discovery.broberg.ai`. */\n audience: string;\n /** The key set URL. Omitted: read once from the issuer's discovery document. */\n jwksUri?: string;\n /**\n * The longest lifetime (exp − iat) a ticket may claim. BID issues 5 min\n * (service) and 15 min (agent); anything longer was not minted by the policy\n * we were told about, so it is refused rather than trusted for longer.\n */\n maxLifetimeSec?: number;\n /** Seconds of clock skew to allow on exp/iat. Default 30. */\n clockToleranceSec?: number;\n /** Fetch the key set at construction, so the first caller does not wait. */\n warmUp?: boolean;\n fetchImpl?: typeof fetch;\n minRefetchIntervalMs?: number;\n}\n\nexport interface TicketVerifier {\n /** Verify a ticket; with `scope`, also require that scope to be granted. */\n verify(token: string, options?: { scope?: string | string[] }): Promise<TicketPrincipal>;\n}\n\nexport function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier {\n const {\n issuer,\n audience,\n maxLifetimeSec = 15 * 60,\n clockToleranceSec = 30,\n fetchImpl = fetch,\n } = options;\n if (!issuer) throw new SsoError(\"createTicketVerifier needs an issuer\");\n if (!audience) throw new SsoError(\"createTicketVerifier needs this receiver's audience\");\n\n let cachePromise: Promise<JwksCache> | null = null;\n const cacheFor = (jwksUri: string) =>\n createJwksCache({\n jwksUri,\n fetchImpl,\n ...(options.warmUp ? { warmUp: true } : {}),\n ...(options.minRefetchIntervalMs !== undefined ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}),\n });\n\n async function keys(): Promise<JwksCache> {\n cachePromise ??= (async () => {\n if (options.jwksUri) return cacheFor(options.jwksUri);\n const url = `${issuer}/.well-known/openid-configuration`;\n let doc: { issuer?: string; jwks_uri?: string };\n try {\n const res = await fetchImpl(url);\n if (!res.ok) throw new Error(`answered ${res.status}`);\n doc = (await res.json()) as typeof doc;\n } catch (cause) {\n // Reached nothing usable: transient, the same class a key-set outage gets.\n throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);\n }\n if (doc.issuer !== issuer) {\n throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} — they must match exactly`);\n }\n if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);\n return cacheFor(doc.jwks_uri);\n })().catch((err) => {\n cachePromise = null; // a later call retries instead of caching the failure\n throw err;\n });\n return cachePromise;\n }\n if (options.warmUp) void keys().catch(() => {});\n\n return {\n async verify(token, opts = {}) {\n let header;\n try {\n header = decodeProtectedHeader(token);\n } catch {\n throw new SsoError(\"ticket is not a JWS\");\n }\n if (!header.kid) throw new SsoError(\"ticket has no kid — cannot pick a signing key\");\n if (header.typ === \"logout+jwt\") throw new SsoError(\"a logout token is not a ticket\");\n\n const cache = await keys();\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, async () => cache.getKey(header.kid!, header.alg ?? \"EdDSA\"), {\n issuer,\n audience,\n algorithms: ALLOWED_ALGS,\n clockTolerance: clockToleranceSec,\n requiredClaims: [\"exp\", \"iat\", \"jti\", \"sub\"],\n }));\n } catch (e) {\n // A key-set outage must surface as itself (→ 503), never as \"rejected\".\n if (e instanceof JwksUnavailableError) throw e;\n throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // An ID token or a logout token must never pass as a ticket. They carry\n // no principal_type; a logout token carries events, an ID token a nonce.\n if (\"events\" in payload) throw new SsoError(\"a logout token is not a ticket\");\n if (\"nonce\" in payload) throw new SsoError(\"an ID token is not a ticket\");\n\n const type = payload.principal_type;\n if (typeof type !== \"string\" || !PRINCIPAL_TYPES.includes(type as PrincipalType)) {\n throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);\n }\n const lifetime = payload.exp! - payload.iat!;\n if (lifetime > maxLifetimeSec) {\n throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);\n }\n if (typeof payload.sub !== \"string\" || payload.sub === \"\") throw new SsoError(\"ticket has no sub\");\n\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \").filter(Boolean) : [];\n const required = opts.scope === undefined ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];\n const missing = required.filter((s) => !scopes.includes(s));\n if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(\", \")}`);\n\n // F084.156 — ver: unknown future formats are refused by name, never read\n // with today's rules.\n const ver = payload.ver === undefined ? 0 : payload.ver;\n if (typeof ver !== \"number\" || !Number.isInteger(ver) || ver < 0) {\n throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);\n }\n if (ver > TICKET_FORMAT_VERSION) {\n throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) — upgrade @broberg/sso`);\n }\n // The whole chain, or a refusal. 0.14.2 kept only act.sub, cutting a chain\n // to its first link, and read a malformed act as \"no delegation\" — both in\n // the direction where the caller looks like it acts for fewer people.\n const act = payload.act === undefined ? null : readActor(payload.act, 1);\n const cnfRaw = payload.cnf as { jkt?: unknown } | undefined;\n const cnf = cnfRaw && typeof cnfRaw.jkt === \"string\" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;\n return {\n principal: payload.sub,\n type: type as PrincipalType,\n clientId: typeof payload.client_id === \"string\" && payload.client_id ? payload.client_id : payload.sub,\n org: typeof payload.org === \"string\" && payload.org ? payload.org : null,\n act,\n version: ver,\n cnf,\n scopes,\n exp: payload.exp!,\n jti: String(payload.jti),\n };\n },\n };\n}\n\nfunction readActor(raw: unknown, depth: number): TicketActor {\n if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);\n const link = raw as { sub?: unknown; act?: unknown } | null;\n if (!link || typeof link !== \"object\" || typeof link.sub !== \"string\" || link.sub === \"\") {\n throw new SsoError(`ticket's act (link ${depth}) has no string sub`);\n }\n return link.act === undefined ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@broberg/sso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "The thin client for Broberg ID — send a user to central login, verify the ID token against JWKS, keep a local session. Public client + PKCE only: no passwords, no passkeys, no client secret can live in an app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|