@broberg/sso 0.14.2 → 0.16.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 +39 -1
- package/dist/index.cjs +140 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -6
- package/dist/index.d.ts +133 -6
- package/dist/index.js +134 -11
- 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,44 @@ 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`. Since 0.16.0 it is checked — see below.
|
|
561
|
+
|
|
562
|
+
## DPoP — a bound ticket works only for whoever holds the key (0.16.0, F084.157)
|
|
563
|
+
|
|
564
|
+
A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public key. With each request the caller sends a short **proof** signed with the private key (method, URL, time, a hash of the ticket). A ticket copied out of a log does nothing on another machine. RFC 9449; joint design with broberg-id (broberg-id-F087.16).
|
|
565
|
+
|
|
566
|
+
**Receiver** — pass the request's `DPoP` header, method and **public** URL:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
const auth = c.req.header("authorization") ?? "";
|
|
570
|
+
const token = auth.replace(/^(Bearer|DPoP) /, "");
|
|
571
|
+
const who = await tickets.verify(token, {
|
|
572
|
+
scope: "discovery:read",
|
|
573
|
+
dpop: { proof: c.req.header("dpop"), method: c.req.method, url: `https://discovery.broberg.ai${new URL(c.req.url).pathname}` },
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
- **A ticket WITH `cnf.jkt` needs a valid proof — always.** Without one it is refused, also while DPoP is optional for you. (Accepting bound tickets as bearer during a rollout would let a stolen one work the whole time.)
|
|
578
|
+
- **`requireDpop: true`** on `createTicketVerifier` also refuses UNBOUND tickets (and a `cnf` without a string `jkt`). Leave it off until every caller sends DPoP.
|
|
579
|
+
- **Checked:** `typ: dpop+jwt`, an asymmetric `alg`, the signature with the header's `jwk` (never a private key), `jwk` thumbprint = `cnf.jkt`, `htm` = the method, `htu` = the URL (without query/fragment), `iat` within ±60 s (`dpopIatWindowSec`), `ath` = this ticket, and a `jti` not seen before.
|
|
580
|
+
- **The URL must be the PUBLIC one.** Behind Fly's proxy your process sees `http://<internal>`; build the URL from your public origin, or every proof fails on `htu`.
|
|
581
|
+
- **Replay store:** in-memory by default, which holds on **one machine**. A receiver on two machines must pass one shared `replayStore` (`{ seen(key, ttlMs) }`), or a proof replayed against the other machine inside the window passes.
|
|
582
|
+
- Not yet: server nonces (RFC 9449 §8).
|
|
583
|
+
|
|
584
|
+
**Caller** — `createTicketClient({ dpop: true })`, then one `authHeaders` per request:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
const tickets = createTicketClient({ dpop: true });
|
|
588
|
+
const headers = await tickets.authHeaders({ audience: "discovery", scope: "discovery:read", method: "GET", url: "https://discovery.broberg.ai/api/fleet" });
|
|
589
|
+
await fetch("https://discovery.broberg.ai/api/fleet", { headers }); // { authorization: "DPoP <ticket>", dpop: "<proof>" }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
One ES256 key per client, non-extractable, never stored; BID binds the ticket to it from the proof on the token request. A proof is for one request — call `authHeaders` each time. Without `dpop`, `authHeaders` returns `{ authorization: "Bearer <ticket>" }` and nothing else changes.
|
|
593
|
+
|
|
556
594
|
## Getting a ticket — `fetchTicket` (since 0.14.1 — 0.14.0 was tagged but never reached npm)
|
|
557
595
|
|
|
558
596
|
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
|
@@ -5,6 +5,84 @@ var fs = require('fs');
|
|
|
5
5
|
var http = require('http');
|
|
6
6
|
var jose = require('jose');
|
|
7
7
|
|
|
8
|
+
async function createDpopKey() {
|
|
9
|
+
const { privateKey, publicKey } = await jose.generateKeyPair("ES256", { extractable: false });
|
|
10
|
+
const jwk = await jose.exportJWK(publicKey);
|
|
11
|
+
const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
12
|
+
return { privateKey, publicJwk, jkt: await jose.calculateJwkThumbprint(publicJwk) };
|
|
13
|
+
}
|
|
14
|
+
var enc = new TextEncoder();
|
|
15
|
+
var b64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16
|
+
async function accessTokenHash(ticket) {
|
|
17
|
+
return b64url(await crypto.subtle.digest("SHA-256", enc.encode(ticket)));
|
|
18
|
+
}
|
|
19
|
+
function normalizeHtu(url) {
|
|
20
|
+
const u = new URL(url);
|
|
21
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
22
|
+
}
|
|
23
|
+
async function createDpopProof(key, opts) {
|
|
24
|
+
const payload = {
|
|
25
|
+
jti: crypto.randomUUID(),
|
|
26
|
+
htm: opts.method.toUpperCase(),
|
|
27
|
+
htu: normalizeHtu(opts.url),
|
|
28
|
+
iat: Math.floor((opts.now?.() ?? Date.now()) / 1e3)
|
|
29
|
+
};
|
|
30
|
+
if (opts.ticket !== void 0) payload.ath = await accessTokenHash(opts.ticket);
|
|
31
|
+
return new jose.SignJWT(payload).setProtectedHeader({ typ: "dpop+jwt", alg: "ES256", jwk: key.publicJwk }).sign(key.privateKey);
|
|
32
|
+
}
|
|
33
|
+
function createMemoryReplayStore(now = () => Date.now()) {
|
|
34
|
+
const entries = /* @__PURE__ */ new Map();
|
|
35
|
+
return {
|
|
36
|
+
async seen(key, ttlMs) {
|
|
37
|
+
const t = now();
|
|
38
|
+
for (const [k, until] of entries) if (until <= t) entries.delete(k);
|
|
39
|
+
if (entries.has(key)) return true;
|
|
40
|
+
entries.set(key, t + ttlMs);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
var ASYMMETRIC = /* @__PURE__ */ new Set(["ES256", "ES384", "ES512", "EdDSA", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"]);
|
|
46
|
+
async function verifyDpopProof(proof, o) {
|
|
47
|
+
let header;
|
|
48
|
+
try {
|
|
49
|
+
header = jose.decodeProtectedHeader(proof);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof is not a JWS");
|
|
52
|
+
}
|
|
53
|
+
if (header.typ !== "dpop+jwt") throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
|
|
54
|
+
if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
|
|
55
|
+
const jwk = header.jwk;
|
|
56
|
+
if (!jwk || typeof jwk !== "object") throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof carries no jwk");
|
|
57
|
+
if ("d" in jwk) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof jwk contains a private key");
|
|
58
|
+
let payload;
|
|
59
|
+
try {
|
|
60
|
+
const key = await jose.importJWK(jwk, header.alg);
|
|
61
|
+
({ payload } = await jose.jwtVerify(proof, key, { typ: "dpop+jwt", algorithms: [header.alg] }));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
64
|
+
}
|
|
65
|
+
if (await jose.calculateJwkThumbprint(jwk) !== o.jkt) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof key does not match the ticket's cnf.jkt");
|
|
66
|
+
if (payload.htm !== o.method.toUpperCase()) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
|
|
67
|
+
let htu;
|
|
68
|
+
try {
|
|
69
|
+
htu = normalizeHtu(String(payload.htu));
|
|
70
|
+
} catch {
|
|
71
|
+
throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof htu is not a URL");
|
|
72
|
+
}
|
|
73
|
+
if (htu !== normalizeHtu(o.url)) throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
|
|
74
|
+
const nowSec = Math.floor(o.now() / 1e3);
|
|
75
|
+
if (typeof payload.iat !== "number" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {
|
|
76
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
|
|
77
|
+
}
|
|
78
|
+
if (payload.ath !== await accessTokenHash(o.ticket)) throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof ath does not match this ticket");
|
|
79
|
+
if (typeof payload.jti !== "string" || payload.jti === "") throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof has no jti");
|
|
80
|
+
if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1e3)) {
|
|
81
|
+
throw new chunkJGIO7XPR_cjs.SsoError("DPoP proof was already used (jti replay)");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/fetch-ticket.ts
|
|
8
86
|
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
9
87
|
var FLY_SOCKET = "/.fly/api";
|
|
10
88
|
var NoWorkloadIdentityError = class extends Error {
|
|
@@ -66,6 +144,8 @@ function createTicketClient(options = {}) {
|
|
|
66
144
|
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && fs.existsSync(FLY_SOCKET));
|
|
67
145
|
const cache = /* @__PURE__ */ new Map();
|
|
68
146
|
const inFlight = /* @__PURE__ */ new Map();
|
|
147
|
+
let dpopKey = null;
|
|
148
|
+
const ownKey = () => dpopKey ??= createDpopKey();
|
|
69
149
|
async function platformToken() {
|
|
70
150
|
if (onFly()) return flyOidc(issuer);
|
|
71
151
|
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
@@ -97,11 +177,10 @@ function createTicketClient(options = {}) {
|
|
|
97
177
|
if (scope) form.set("scope", scope);
|
|
98
178
|
let res;
|
|
99
179
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
180
|
+
const tokenUrl = `${issuer}/oauth2/token`;
|
|
181
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
182
|
+
if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: "POST", url: tokenUrl, now });
|
|
183
|
+
res = await fetchImpl(tokenUrl, { method: "POST", headers, body: form });
|
|
105
184
|
} catch (e) {
|
|
106
185
|
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
107
186
|
}
|
|
@@ -120,7 +199,12 @@ function createTicketClient(options = {}) {
|
|
|
120
199
|
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
121
200
|
return { token: body.access_token, exp };
|
|
122
201
|
}
|
|
123
|
-
|
|
202
|
+
const client = {
|
|
203
|
+
async authHeaders({ audience, scope, method, url }) {
|
|
204
|
+
const ticket = await client.get({ audience, scope });
|
|
205
|
+
if (!options.dpop) return { authorization: `Bearer ${ticket}` };
|
|
206
|
+
return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };
|
|
207
|
+
},
|
|
124
208
|
async get({ audience, scope }) {
|
|
125
209
|
const key = `${audience} ${scope ?? ""}`;
|
|
126
210
|
const hit = cache.get(key);
|
|
@@ -135,6 +219,7 @@ function createTicketClient(options = {}) {
|
|
|
135
219
|
return p;
|
|
136
220
|
}
|
|
137
221
|
};
|
|
222
|
+
return client;
|
|
138
223
|
}
|
|
139
224
|
var shared = null;
|
|
140
225
|
function fetchTicket(opts) {
|
|
@@ -142,14 +227,20 @@ function fetchTicket(opts) {
|
|
|
142
227
|
return shared.get(opts);
|
|
143
228
|
}
|
|
144
229
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
230
|
+
var TICKET_FORMAT_VERSION = 1;
|
|
231
|
+
var MAX_ACT_DEPTH = 5;
|
|
145
232
|
function createTicketVerifier(options) {
|
|
146
233
|
const {
|
|
147
234
|
issuer,
|
|
148
235
|
audience,
|
|
149
236
|
maxLifetimeSec = 15 * 60,
|
|
150
237
|
clockToleranceSec = 30,
|
|
151
|
-
fetchImpl = fetch
|
|
238
|
+
fetchImpl = fetch,
|
|
239
|
+
requireDpop = false,
|
|
240
|
+
dpopIatWindowSec = 60,
|
|
241
|
+
now = () => Date.now()
|
|
152
242
|
} = options;
|
|
243
|
+
const replayStore = options.replayStore ?? createMemoryReplayStore(now);
|
|
153
244
|
if (!issuer) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs an issuer");
|
|
154
245
|
if (!audience) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs this receiver's audience");
|
|
155
246
|
let cachePromise = null;
|
|
@@ -223,14 +314,38 @@ function createTicketVerifier(options) {
|
|
|
223
314
|
const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
224
315
|
const missing = required.filter((s) => !scopes.includes(s));
|
|
225
316
|
if (missing.length) throw new chunkJGIO7XPR_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
226
|
-
const
|
|
227
|
-
|
|
317
|
+
const ver = payload.ver === void 0 ? 0 : payload.ver;
|
|
318
|
+
if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
|
|
319
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
|
|
320
|
+
}
|
|
321
|
+
if (ver > TICKET_FORMAT_VERSION) {
|
|
322
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
|
|
323
|
+
}
|
|
324
|
+
const act = payload.act === void 0 ? null : readActor(payload.act, 1);
|
|
325
|
+
const cnfRaw = payload.cnf;
|
|
326
|
+
const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
|
|
327
|
+
if (cnf) {
|
|
328
|
+
if (!opts.dpop?.proof) throw new chunkJGIO7XPR_cjs.SsoError("ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented");
|
|
329
|
+
await verifyDpopProof(opts.dpop.proof, {
|
|
330
|
+
method: opts.dpop.method,
|
|
331
|
+
url: opts.dpop.url,
|
|
332
|
+
ticket: token,
|
|
333
|
+
jkt: cnf.jkt,
|
|
334
|
+
replay: replayStore,
|
|
335
|
+
iatWindowSec: dpopIatWindowSec,
|
|
336
|
+
now
|
|
337
|
+
});
|
|
338
|
+
} else if (requireDpop) {
|
|
339
|
+
throw new chunkJGIO7XPR_cjs.SsoError(cnfRaw !== void 0 ? "ticket has a cnf without a string jkt \u2014 DPoP is required here" : "this receiver requires DPoP; the ticket is not bound (no cnf.jkt)");
|
|
340
|
+
}
|
|
228
341
|
return {
|
|
229
342
|
principal: payload.sub,
|
|
230
343
|
type,
|
|
231
344
|
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
232
345
|
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
233
346
|
act,
|
|
347
|
+
version: ver,
|
|
348
|
+
cnf,
|
|
234
349
|
scopes,
|
|
235
350
|
exp: payload.exp,
|
|
236
351
|
jti: String(payload.jti)
|
|
@@ -238,6 +353,14 @@ function createTicketVerifier(options) {
|
|
|
238
353
|
}
|
|
239
354
|
};
|
|
240
355
|
}
|
|
356
|
+
function readActor(raw, depth) {
|
|
357
|
+
if (depth > MAX_ACT_DEPTH) throw new chunkJGIO7XPR_cjs.SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
|
|
358
|
+
const link = raw;
|
|
359
|
+
if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
|
|
360
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket's act (link ${depth}) has no string sub`);
|
|
361
|
+
}
|
|
362
|
+
return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
|
|
363
|
+
}
|
|
241
364
|
|
|
242
365
|
Object.defineProperty(exports, "AVATAR_TYPES", {
|
|
243
366
|
enumerable: true,
|
|
@@ -332,11 +455,19 @@ Object.defineProperty(exports, "verifyValue", {
|
|
|
332
455
|
get: function () { return chunkJGIO7XPR_cjs.verifyValue; }
|
|
333
456
|
});
|
|
334
457
|
exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
|
|
458
|
+
exports.MAX_ACT_DEPTH = MAX_ACT_DEPTH;
|
|
335
459
|
exports.NoWorkloadIdentityError = NoWorkloadIdentityError;
|
|
460
|
+
exports.TICKET_FORMAT_VERSION = TICKET_FORMAT_VERSION;
|
|
336
461
|
exports.TicketExchangeError = TicketExchangeError;
|
|
337
462
|
exports.TicketUnavailableError = TicketUnavailableError;
|
|
463
|
+
exports.accessTokenHash = accessTokenHash;
|
|
464
|
+
exports.createDpopKey = createDpopKey;
|
|
465
|
+
exports.createDpopProof = createDpopProof;
|
|
466
|
+
exports.createMemoryReplayStore = createMemoryReplayStore;
|
|
338
467
|
exports.createTicketClient = createTicketClient;
|
|
339
468
|
exports.createTicketVerifier = createTicketVerifier;
|
|
340
469
|
exports.fetchTicket = fetchTicket;
|
|
470
|
+
exports.normalizeHtu = normalizeHtu;
|
|
471
|
+
exports.verifyDpopProof = verifyDpopProof;
|
|
341
472
|
//# sourceMappingURL=index.cjs.map
|
|
342
473
|
//# sourceMappingURL=index.cjs.map
|
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/dpop.ts","../src/fetch-ticket.ts","../src/ticket.ts"],"names":["generateKeyPair","exportJWK","calculateJwkThumbprint","SignJWT","decodeProtectedHeader","SsoError","importJWK","jwtVerify","request","existsSync","createJwksCache","JwksUnavailableError","ALLOWED_ALGS"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key\n * (broberg-id-F087.16, joint design #2572/#2574).\n *\n * A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public\n * key. With every request the caller signs a short PROOF (method, URL, time, a\n * hash of the ticket) with the matching private key. A ticket copied out of a log\n * is useless on another machine: it has the ticket, not the key.\n *\n * THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a\n * receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —\n * also while DPoP is otherwise optional. `requireDpop` only decides whether\n * UNBOUND tickets are still accepted. Were bound tickets accepted without a proof\n * during the rollout, a stolen bound ticket would work as bearer the whole time.\n *\n * Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay\n * inside the iat window.\n */\nimport {\n SignJWT,\n calculateJwkThumbprint,\n decodeProtectedHeader,\n exportJWK,\n generateKeyPair,\n importJWK,\n jwtVerify,\n type JWK,\n} from \"jose\";\nimport { SsoError } from \"./client.js\";\n\n/** One process's DPoP key. The private half cannot be exported. */\nexport interface DpopKey {\n privateKey: CryptoKey;\n publicJwk: JWK;\n /** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */\n jkt: string;\n}\n\n/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */\nexport async function createDpopKey(): Promise<DpopKey> {\n const { privateKey, publicKey } = await generateKeyPair(\"ES256\", { extractable: false });\n const jwk = await exportJWK(publicKey); // a public key is always exportable\n const publicJwk: JWK = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };\n return { privateKey, publicJwk, jkt: await calculateJwkThumbprint(publicJwk) };\n}\n\nconst enc = new TextEncoder();\nconst b64url = (bytes: ArrayBuffer) =>\n btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n\n/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */\nexport async function accessTokenHash(ticket: string): Promise<string> {\n return b64url(await crypto.subtle.digest(\"SHA-256\", enc.encode(ticket)));\n}\n\n/** RFC 9449 §4.3: htu is compared without query and fragment. */\nexport function normalizeHtu(url: string): string {\n const u = new URL(url);\n return `${u.protocol}//${u.host}${u.pathname}`;\n}\n\n/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */\nexport async function createDpopProof(\n key: DpopKey,\n opts: { method: string; url: string; ticket?: string; now?: () => number },\n): Promise<string> {\n const payload: Record<string, unknown> = {\n jti: crypto.randomUUID(),\n htm: opts.method.toUpperCase(),\n htu: normalizeHtu(opts.url),\n iat: Math.floor((opts.now?.() ?? Date.now()) / 1000),\n };\n if (opts.ticket !== undefined) payload.ath = await accessTokenHash(opts.ticket);\n return new SignJWT(payload).setProtectedHeader({ typ: \"dpop+jwt\", alg: \"ES256\", jwk: key.publicJwk }).sign(key.privateKey);\n}\n\n/**\n * Remembers which proofs were already used. The default is in-memory, which\n * holds on ONE machine: a receiver running on two machines must pass a shared\n * store, or a proof replayed against the other machine inside the window passes.\n */\nexport interface ReplayStore {\n /** Record `key`; resolve true if it was already recorded within `ttlMs`. */\n seen(key: string, ttlMs: number): Promise<boolean>;\n}\n\nexport function createMemoryReplayStore(now: () => number = () => Date.now()): ReplayStore {\n const entries = new Map<string, number>();\n return {\n async seen(key, ttlMs) {\n const t = now();\n for (const [k, until] of entries) if (until <= t) entries.delete(k);\n if (entries.has(key)) return true;\n entries.set(key, t + ttlMs);\n return false;\n },\n };\n}\n\nexport interface VerifyProofOptions {\n method: string;\n /** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */\n url: string;\n ticket: string;\n jkt: string;\n replay: ReplayStore;\n iatWindowSec: number;\n now: () => number;\n}\n\nconst ASYMMETRIC = new Set([\"ES256\", \"ES384\", \"ES512\", \"EdDSA\", \"PS256\", \"PS384\", \"PS512\", \"RS256\", \"RS384\", \"RS512\"]);\n\n/** Throws SsoError naming the first thing that is wrong. */\nexport async function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void> {\n let header;\n try {\n header = decodeProtectedHeader(proof);\n } catch {\n throw new SsoError(\"DPoP proof is not a JWS\");\n }\n if (header.typ !== \"dpop+jwt\") throw new SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not \"dpop+jwt\"`);\n if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);\n const jwk = header.jwk as JWK | undefined;\n if (!jwk || typeof jwk !== \"object\") throw new SsoError(\"DPoP proof carries no jwk\");\n if (\"d\" in jwk) throw new SsoError(\"DPoP proof jwk contains a private key\");\n\n let payload;\n try {\n const key = await importJWK(jwk, header.alg);\n ({ payload } = await jwtVerify(proof, key, { typ: \"dpop+jwt\", algorithms: [header.alg] }));\n } catch (e) {\n throw new SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n if ((await calculateJwkThumbprint(jwk)) !== o.jkt) throw new SsoError(\"DPoP proof key does not match the ticket's cnf.jkt\");\n if (payload.htm !== o.method.toUpperCase()) throw new SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);\n let htu: string;\n try {\n htu = normalizeHtu(String(payload.htu));\n } catch {\n throw new SsoError(\"DPoP proof htu is not a URL\");\n }\n if (htu !== normalizeHtu(o.url)) throw new SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);\n const nowSec = Math.floor(o.now() / 1000);\n if (typeof payload.iat !== \"number\" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {\n throw new SsoError(`DPoP proof iat is outside ±${o.iatWindowSec}s`);\n }\n if (payload.ath !== (await accessTokenHash(o.ticket))) throw new SsoError(\"DPoP proof ath does not match this ticket\");\n if (typeof payload.jti !== \"string\" || payload.jti === \"\") throw new SsoError(\"DPoP proof has no jti\");\n if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1000)) {\n throw new SsoError(\"DPoP proof was already used (jti replay)\");\n }\n}\n","/**\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\";\nimport { createDpopKey, createDpopProof, type DpopKey } from \"./dpop.js\";\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 * F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client\n * (non-extractable, never stored); every call then needs a proof, which\n * `authHeaders` makes. Default false: bearer tickets, exactly as before.\n */\n dpop?: boolean;\n}\n\n/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */\nexport interface TicketAuthHeaders {\n authorization: string;\n dpop?: string;\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 * F084.157 — the headers for ONE request to a receiver: a ticket plus, with\n * `dpop`, a fresh proof bound to this method and URL (use it once). Without\n * `dpop`: `{ authorization: \"Bearer <ticket>\" }`.\n */\n authHeaders(opts: { audience: string; scope?: string; method: string; url: string }): Promise<TicketAuthHeaders>;\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 let dpopKey: Promise<DpopKey> | null = null;\n const ownKey = () => (dpopKey ??= createDpopKey());\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 const tokenUrl = `${issuer}/oauth2/token`;\n const headers: Record<string, string> = { \"content-type\": \"application/x-www-form-urlencoded\" };\n if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: \"POST\", url: tokenUrl, now });\n res = await fetchImpl(tokenUrl, { method: \"POST\", headers, body: form });\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 const client: TicketClient = {\n async authHeaders({ audience, scope, method, url }) {\n const ticket = await client.get({ audience, scope });\n if (!options.dpop) return { authorization: `Bearer ${ticket}` };\n return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };\n },\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 return client;\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\";\nimport { createMemoryReplayStore, verifyDpopProof, type ReplayStore } from \"./dpop.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 * Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with\n * a valid DPoP proof for that key — see `verify(token, { dpop })`.\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 * F084.157 — refuse UNBOUND tickets (bearer). Off by default during the\n * rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.\n */\n requireDpop?: boolean;\n /** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */\n replayStore?: ReplayStore;\n /** How far a proof's iat may be from now. Default 60 s. */\n dpopIatWindowSec?: number;\n /** Injectable clock for the DPoP checks (ms). */\n now?: () => number;\n}\n\n/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */\nexport interface DpopRequest {\n proof: string | null | undefined;\n method: string;\n url: string;\n}\n\nexport interface TicketVerifier {\n /**\n * Verify a ticket; with `scope`, also require that scope to be granted. Pass\n * `dpop` with the request's DPoP header, method and public URL — a bound ticket\n * is refused without it.\n */\n verify(token: string, options?: { scope?: string | string[]; dpop?: DpopRequest }): 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 requireDpop = false,\n dpopIatWindowSec = 60,\n now = () => Date.now(),\n } = options;\n const replayStore = options.replayStore ?? createMemoryReplayStore(now);\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 // F084.157 — a bound ticket needs its proof, always; requireDpop only\n // decides about unbound ones (and then a cnf it cannot read is refused too).\n if (cnf) {\n if (!opts.dpop?.proof) throw new SsoError(\"ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented\");\n await verifyDpopProof(opts.dpop.proof, {\n method: opts.dpop.method,\n url: opts.dpop.url,\n ticket: token,\n jkt: cnf.jkt,\n replay: replayStore,\n iatWindowSec: dpopIatWindowSec,\n now,\n });\n } else if (requireDpop) {\n throw new SsoError(cnfRaw !== undefined ? \"ticket has a cnf without a string jkt — DPoP is required here\" : \"this receiver requires DPoP; the ticket is not bound (no cnf.jkt)\");\n }\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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as InvitationOutcome, f as InvitationResult, g as InviteUsersInput, J as JwksCache, h as JwksCacheOptions, i as JwksError, j as JwksUnavailableError, k as JwksUnknownKeyError, L as LoginResult, l as LoginStart, m as LogoutToken, M as MAX_AVATAR_BYTES, n as MAX_INVITATIONS, o as MigrationState, p as MigrationStatus, b as SessionPayload, q as SsoAppApiError, r as SsoAvatarRejectedError, s as SsoClaims, S as SsoClient, a as SsoConfig, t as SsoConfigError, u as SsoError, v as SsoInsufficientScopeError, w as SsoReauthError, x as TokenSet, T as TokenStore, y as cookieHeader, z as createJwksCache, E as createSsoClient, F as loadSsoConfig, G as memoryTokenStore, H as readCookie, K as signSession, N as signValue, O as verifySession, P as verifyValue } from './session-DsjOyCJb.cjs';
|
|
2
|
-
import 'jose';
|
|
2
|
+
import { JWK } from 'jose';
|
|
3
3
|
|
|
4
4
|
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
5
5
|
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
@@ -32,6 +32,17 @@ interface TicketClientOptions {
|
|
|
32
32
|
flyOidc?: (aud: string) => Promise<string>;
|
|
33
33
|
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
34
34
|
onFly?: () => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client
|
|
37
|
+
* (non-extractable, never stored); every call then needs a proof, which
|
|
38
|
+
* `authHeaders` makes. Default false: bearer tickets, exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
dpop?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */
|
|
43
|
+
interface TicketAuthHeaders {
|
|
44
|
+
authorization: string;
|
|
45
|
+
dpop?: string;
|
|
35
46
|
}
|
|
36
47
|
interface TicketClient {
|
|
37
48
|
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
@@ -39,6 +50,17 @@ interface TicketClient {
|
|
|
39
50
|
audience: string;
|
|
40
51
|
scope?: string;
|
|
41
52
|
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* F084.157 — the headers for ONE request to a receiver: a ticket plus, with
|
|
55
|
+
* `dpop`, a fresh proof bound to this method and URL (use it once). Without
|
|
56
|
+
* `dpop`: `{ authorization: "Bearer <ticket>" }`.
|
|
57
|
+
*/
|
|
58
|
+
authHeaders(opts: {
|
|
59
|
+
audience: string;
|
|
60
|
+
scope?: string;
|
|
61
|
+
method: string;
|
|
62
|
+
url: string;
|
|
63
|
+
}): Promise<TicketAuthHeaders>;
|
|
42
64
|
}
|
|
43
65
|
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
44
66
|
/** A ticket from the process-wide client (one cache per process). */
|
|
@@ -47,7 +69,82 @@ declare function fetchTicket(opts: {
|
|
|
47
69
|
scope?: string;
|
|
48
70
|
}): Promise<string>;
|
|
49
71
|
|
|
72
|
+
/**
|
|
73
|
+
* F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key
|
|
74
|
+
* (broberg-id-F087.16, joint design #2572/#2574).
|
|
75
|
+
*
|
|
76
|
+
* A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public
|
|
77
|
+
* key. With every request the caller signs a short PROOF (method, URL, time, a
|
|
78
|
+
* hash of the ticket) with the matching private key. A ticket copied out of a log
|
|
79
|
+
* is useless on another machine: it has the ticket, not the key.
|
|
80
|
+
*
|
|
81
|
+
* THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a
|
|
82
|
+
* receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —
|
|
83
|
+
* also while DPoP is otherwise optional. `requireDpop` only decides whether
|
|
84
|
+
* UNBOUND tickets are still accepted. Were bound tickets accepted without a proof
|
|
85
|
+
* during the rollout, a stolen bound ticket would work as bearer the whole time.
|
|
86
|
+
*
|
|
87
|
+
* Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay
|
|
88
|
+
* inside the iat window.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** One process's DPoP key. The private half cannot be exported. */
|
|
92
|
+
interface DpopKey {
|
|
93
|
+
privateKey: CryptoKey;
|
|
94
|
+
publicJwk: JWK;
|
|
95
|
+
/** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */
|
|
96
|
+
jkt: string;
|
|
97
|
+
}
|
|
98
|
+
/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */
|
|
99
|
+
declare function createDpopKey(): Promise<DpopKey>;
|
|
100
|
+
/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */
|
|
101
|
+
declare function accessTokenHash(ticket: string): Promise<string>;
|
|
102
|
+
/** RFC 9449 §4.3: htu is compared without query and fragment. */
|
|
103
|
+
declare function normalizeHtu(url: string): string;
|
|
104
|
+
/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */
|
|
105
|
+
declare function createDpopProof(key: DpopKey, opts: {
|
|
106
|
+
method: string;
|
|
107
|
+
url: string;
|
|
108
|
+
ticket?: string;
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Remembers which proofs were already used. The default is in-memory, which
|
|
113
|
+
* holds on ONE machine: a receiver running on two machines must pass a shared
|
|
114
|
+
* store, or a proof replayed against the other machine inside the window passes.
|
|
115
|
+
*/
|
|
116
|
+
interface ReplayStore {
|
|
117
|
+
/** Record `key`; resolve true if it was already recorded within `ttlMs`. */
|
|
118
|
+
seen(key: string, ttlMs: number): Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
declare function createMemoryReplayStore(now?: () => number): ReplayStore;
|
|
121
|
+
interface VerifyProofOptions {
|
|
122
|
+
method: string;
|
|
123
|
+
/** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */
|
|
124
|
+
url: string;
|
|
125
|
+
ticket: string;
|
|
126
|
+
jkt: string;
|
|
127
|
+
replay: ReplayStore;
|
|
128
|
+
iatWindowSec: number;
|
|
129
|
+
now: () => number;
|
|
130
|
+
}
|
|
131
|
+
/** Throws SsoError naming the first thing that is wrong. */
|
|
132
|
+
declare function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void>;
|
|
133
|
+
|
|
50
134
|
type PrincipalType = "human" | "service" | "agent";
|
|
135
|
+
/**
|
|
136
|
+
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
137
|
+
* outward: agent-b acting for agent-a acting for user-1 reads
|
|
138
|
+
* `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
|
|
139
|
+
*/
|
|
140
|
+
interface TicketActor {
|
|
141
|
+
sub: string;
|
|
142
|
+
act?: TicketActor;
|
|
143
|
+
}
|
|
144
|
+
/** The newest ticket format version this package understands (BID sets `ver`). */
|
|
145
|
+
declare const TICKET_FORMAT_VERSION = 1;
|
|
146
|
+
/** Delegation deeper than this is refused rather than shortened. */
|
|
147
|
+
declare const MAX_ACT_DEPTH = 5;
|
|
51
148
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
52
149
|
interface TicketPrincipal {
|
|
53
150
|
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
@@ -57,9 +154,17 @@ interface TicketPrincipal {
|
|
|
57
154
|
clientId: string;
|
|
58
155
|
/** The organisation, when BID stated one. */
|
|
59
156
|
org: string | null;
|
|
60
|
-
/** On delegation: who the caller is acting for (RFC 8693 `act
|
|
61
|
-
act:
|
|
62
|
-
|
|
157
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
|
|
158
|
+
act: TicketActor | null;
|
|
159
|
+
/** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
|
|
160
|
+
version: number;
|
|
161
|
+
/**
|
|
162
|
+
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
163
|
+
* Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with
|
|
164
|
+
* a valid DPoP proof for that key — see `verify(token, { dpop })`.
|
|
165
|
+
*/
|
|
166
|
+
cnf: {
|
|
167
|
+
jkt: string;
|
|
63
168
|
} | null;
|
|
64
169
|
scopes: string[];
|
|
65
170
|
/** Seconds since the epoch. */
|
|
@@ -85,13 +190,35 @@ interface TicketVerifierOptions {
|
|
|
85
190
|
warmUp?: boolean;
|
|
86
191
|
fetchImpl?: typeof fetch;
|
|
87
192
|
minRefetchIntervalMs?: number;
|
|
193
|
+
/**
|
|
194
|
+
* F084.157 — refuse UNBOUND tickets (bearer). Off by default during the
|
|
195
|
+
* rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.
|
|
196
|
+
*/
|
|
197
|
+
requireDpop?: boolean;
|
|
198
|
+
/** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */
|
|
199
|
+
replayStore?: ReplayStore;
|
|
200
|
+
/** How far a proof's iat may be from now. Default 60 s. */
|
|
201
|
+
dpopIatWindowSec?: number;
|
|
202
|
+
/** Injectable clock for the DPoP checks (ms). */
|
|
203
|
+
now?: () => number;
|
|
204
|
+
}
|
|
205
|
+
/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */
|
|
206
|
+
interface DpopRequest {
|
|
207
|
+
proof: string | null | undefined;
|
|
208
|
+
method: string;
|
|
209
|
+
url: string;
|
|
88
210
|
}
|
|
89
211
|
interface TicketVerifier {
|
|
90
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Verify a ticket; with `scope`, also require that scope to be granted. Pass
|
|
214
|
+
* `dpop` with the request's DPoP header, method and public URL — a bound ticket
|
|
215
|
+
* is refused without it.
|
|
216
|
+
*/
|
|
91
217
|
verify(token: string, options?: {
|
|
92
218
|
scope?: string | string[];
|
|
219
|
+
dpop?: DpopRequest;
|
|
93
220
|
}): Promise<TicketPrincipal>;
|
|
94
221
|
}
|
|
95
222
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
96
223
|
|
|
97
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
224
|
+
export { DEFAULT_ISSUER, type DpopKey, type DpopRequest, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, type ReplayStore, TICKET_FORMAT_VERSION, type TicketActor, type TicketAuthHeaders, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { A as AVATAR_TYPES, c as AddressOwnership, B as BeginLoginOptions, d as BidProfile, C as CreateSsoClientOptions, D as DEFAULT_SESSION_MAX_AGE, e as Discovery, I as InvitationOutcome, f as InvitationResult, g as InviteUsersInput, J as JwksCache, h as JwksCacheOptions, i as JwksError, j as JwksUnavailableError, k as JwksUnknownKeyError, L as LoginResult, l as LoginStart, m as LogoutToken, M as MAX_AVATAR_BYTES, n as MAX_INVITATIONS, o as MigrationState, p as MigrationStatus, b as SessionPayload, q as SsoAppApiError, r as SsoAvatarRejectedError, s as SsoClaims, S as SsoClient, a as SsoConfig, t as SsoConfigError, u as SsoError, v as SsoInsufficientScopeError, w as SsoReauthError, x as TokenSet, T as TokenStore, y as cookieHeader, z as createJwksCache, E as createSsoClient, F as loadSsoConfig, G as memoryTokenStore, H as readCookie, K as signSession, N as signValue, O as verifySession, P as verifyValue } from './session-DsjOyCJb.js';
|
|
2
|
-
import 'jose';
|
|
2
|
+
import { JWK } from 'jose';
|
|
3
3
|
|
|
4
4
|
declare const DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
5
5
|
/** No platform identity to exchange: not on Fly, not in a GitHub Actions job with id-token: write. */
|
|
@@ -32,6 +32,17 @@ interface TicketClientOptions {
|
|
|
32
32
|
flyOidc?: (aud: string) => Promise<string>;
|
|
33
33
|
/** Whether the Fly socket is present. Default: checks /.fly/api. */
|
|
34
34
|
onFly?: () => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client
|
|
37
|
+
* (non-extractable, never stored); every call then needs a proof, which
|
|
38
|
+
* `authHeaders` makes. Default false: bearer tickets, exactly as before.
|
|
39
|
+
*/
|
|
40
|
+
dpop?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */
|
|
43
|
+
interface TicketAuthHeaders {
|
|
44
|
+
authorization: string;
|
|
45
|
+
dpop?: string;
|
|
35
46
|
}
|
|
36
47
|
interface TicketClient {
|
|
37
48
|
/** A ticket for `audience` (BID's short name, e.g. "discovery"), from cache while it is fresh. */
|
|
@@ -39,6 +50,17 @@ interface TicketClient {
|
|
|
39
50
|
audience: string;
|
|
40
51
|
scope?: string;
|
|
41
52
|
}): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* F084.157 — the headers for ONE request to a receiver: a ticket plus, with
|
|
55
|
+
* `dpop`, a fresh proof bound to this method and URL (use it once). Without
|
|
56
|
+
* `dpop`: `{ authorization: "Bearer <ticket>" }`.
|
|
57
|
+
*/
|
|
58
|
+
authHeaders(opts: {
|
|
59
|
+
audience: string;
|
|
60
|
+
scope?: string;
|
|
61
|
+
method: string;
|
|
62
|
+
url: string;
|
|
63
|
+
}): Promise<TicketAuthHeaders>;
|
|
42
64
|
}
|
|
43
65
|
declare function createTicketClient(options?: TicketClientOptions): TicketClient;
|
|
44
66
|
/** A ticket from the process-wide client (one cache per process). */
|
|
@@ -47,7 +69,82 @@ declare function fetchTicket(opts: {
|
|
|
47
69
|
scope?: string;
|
|
48
70
|
}): Promise<string>;
|
|
49
71
|
|
|
72
|
+
/**
|
|
73
|
+
* F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key
|
|
74
|
+
* (broberg-id-F087.16, joint design #2572/#2574).
|
|
75
|
+
*
|
|
76
|
+
* A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public
|
|
77
|
+
* key. With every request the caller signs a short PROOF (method, URL, time, a
|
|
78
|
+
* hash of the ticket) with the matching private key. A ticket copied out of a log
|
|
79
|
+
* is useless on another machine: it has the ticket, not the key.
|
|
80
|
+
*
|
|
81
|
+
* THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a
|
|
82
|
+
* receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —
|
|
83
|
+
* also while DPoP is otherwise optional. `requireDpop` only decides whether
|
|
84
|
+
* UNBOUND tickets are still accepted. Were bound tickets accepted without a proof
|
|
85
|
+
* during the rollout, a stolen bound ticket would work as bearer the whole time.
|
|
86
|
+
*
|
|
87
|
+
* Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay
|
|
88
|
+
* inside the iat window.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** One process's DPoP key. The private half cannot be exported. */
|
|
92
|
+
interface DpopKey {
|
|
93
|
+
privateKey: CryptoKey;
|
|
94
|
+
publicJwk: JWK;
|
|
95
|
+
/** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */
|
|
96
|
+
jkt: string;
|
|
97
|
+
}
|
|
98
|
+
/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */
|
|
99
|
+
declare function createDpopKey(): Promise<DpopKey>;
|
|
100
|
+
/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */
|
|
101
|
+
declare function accessTokenHash(ticket: string): Promise<string>;
|
|
102
|
+
/** RFC 9449 §4.3: htu is compared without query and fragment. */
|
|
103
|
+
declare function normalizeHtu(url: string): string;
|
|
104
|
+
/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */
|
|
105
|
+
declare function createDpopProof(key: DpopKey, opts: {
|
|
106
|
+
method: string;
|
|
107
|
+
url: string;
|
|
108
|
+
ticket?: string;
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Remembers which proofs were already used. The default is in-memory, which
|
|
113
|
+
* holds on ONE machine: a receiver running on two machines must pass a shared
|
|
114
|
+
* store, or a proof replayed against the other machine inside the window passes.
|
|
115
|
+
*/
|
|
116
|
+
interface ReplayStore {
|
|
117
|
+
/** Record `key`; resolve true if it was already recorded within `ttlMs`. */
|
|
118
|
+
seen(key: string, ttlMs: number): Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
declare function createMemoryReplayStore(now?: () => number): ReplayStore;
|
|
121
|
+
interface VerifyProofOptions {
|
|
122
|
+
method: string;
|
|
123
|
+
/** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */
|
|
124
|
+
url: string;
|
|
125
|
+
ticket: string;
|
|
126
|
+
jkt: string;
|
|
127
|
+
replay: ReplayStore;
|
|
128
|
+
iatWindowSec: number;
|
|
129
|
+
now: () => number;
|
|
130
|
+
}
|
|
131
|
+
/** Throws SsoError naming the first thing that is wrong. */
|
|
132
|
+
declare function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void>;
|
|
133
|
+
|
|
50
134
|
type PrincipalType = "human" | "service" | "agent";
|
|
135
|
+
/**
|
|
136
|
+
* F084.156 — one link of an RFC 8693 delegation chain. `act` is the next link
|
|
137
|
+
* outward: agent-b acting for agent-a acting for user-1 reads
|
|
138
|
+
* `{ sub: "agent-a", act: { sub: "user-1" } }`. `act.sub` alone is the old shape.
|
|
139
|
+
*/
|
|
140
|
+
interface TicketActor {
|
|
141
|
+
sub: string;
|
|
142
|
+
act?: TicketActor;
|
|
143
|
+
}
|
|
144
|
+
/** The newest ticket format version this package understands (BID sets `ver`). */
|
|
145
|
+
declare const TICKET_FORMAT_VERSION = 1;
|
|
146
|
+
/** Delegation deeper than this is refused rather than shortened. */
|
|
147
|
+
declare const MAX_ACT_DEPTH = 5;
|
|
51
148
|
/** What a verified ticket tells the receiver — a small, typed principal, never the raw payload. */
|
|
52
149
|
interface TicketPrincipal {
|
|
53
150
|
/** Who is calling: the ticket's `sub` (a client id such as `svc-trail`). */
|
|
@@ -57,9 +154,17 @@ interface TicketPrincipal {
|
|
|
57
154
|
clientId: string;
|
|
58
155
|
/** The organisation, when BID stated one. */
|
|
59
156
|
org: string | null;
|
|
60
|
-
/** On delegation: who the caller is acting for (RFC 8693 `act
|
|
61
|
-
act:
|
|
62
|
-
|
|
157
|
+
/** On delegation: who the caller is acting for (RFC 8693 `act`), the WHOLE chain. */
|
|
158
|
+
act: TicketActor | null;
|
|
159
|
+
/** The ticket format version (`ver`); 0 for a ticket issued before BID set one. */
|
|
160
|
+
version: number;
|
|
161
|
+
/**
|
|
162
|
+
* F084.156 — the DPoP key thumbprint when the ticket is sender-constrained.
|
|
163
|
+
* Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with
|
|
164
|
+
* a valid DPoP proof for that key — see `verify(token, { dpop })`.
|
|
165
|
+
*/
|
|
166
|
+
cnf: {
|
|
167
|
+
jkt: string;
|
|
63
168
|
} | null;
|
|
64
169
|
scopes: string[];
|
|
65
170
|
/** Seconds since the epoch. */
|
|
@@ -85,13 +190,35 @@ interface TicketVerifierOptions {
|
|
|
85
190
|
warmUp?: boolean;
|
|
86
191
|
fetchImpl?: typeof fetch;
|
|
87
192
|
minRefetchIntervalMs?: number;
|
|
193
|
+
/**
|
|
194
|
+
* F084.157 — refuse UNBOUND tickets (bearer). Off by default during the
|
|
195
|
+
* rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.
|
|
196
|
+
*/
|
|
197
|
+
requireDpop?: boolean;
|
|
198
|
+
/** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */
|
|
199
|
+
replayStore?: ReplayStore;
|
|
200
|
+
/** How far a proof's iat may be from now. Default 60 s. */
|
|
201
|
+
dpopIatWindowSec?: number;
|
|
202
|
+
/** Injectable clock for the DPoP checks (ms). */
|
|
203
|
+
now?: () => number;
|
|
204
|
+
}
|
|
205
|
+
/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */
|
|
206
|
+
interface DpopRequest {
|
|
207
|
+
proof: string | null | undefined;
|
|
208
|
+
method: string;
|
|
209
|
+
url: string;
|
|
88
210
|
}
|
|
89
211
|
interface TicketVerifier {
|
|
90
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Verify a ticket; with `scope`, also require that scope to be granted. Pass
|
|
214
|
+
* `dpop` with the request's DPoP header, method and public URL — a bound ticket
|
|
215
|
+
* is refused without it.
|
|
216
|
+
*/
|
|
91
217
|
verify(token: string, options?: {
|
|
92
218
|
scope?: string | string[];
|
|
219
|
+
dpop?: DpopRequest;
|
|
93
220
|
}): Promise<TicketPrincipal>;
|
|
94
221
|
}
|
|
95
222
|
declare function createTicketVerifier(options: TicketVerifierOptions): TicketVerifier;
|
|
96
223
|
|
|
97
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, type PrincipalType, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, createTicketClient, createTicketVerifier, fetchTicket };
|
|
224
|
+
export { DEFAULT_ISSUER, type DpopKey, type DpopRequest, MAX_ACT_DEPTH, NoWorkloadIdentityError, type PrincipalType, type ReplayStore, TICKET_FORMAT_VERSION, type TicketActor, type TicketAuthHeaders, type TicketClient, type TicketClientOptions, TicketExchangeError, type TicketPrincipal, TicketUnavailableError, type TicketVerifier, type TicketVerifierOptions, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,86 @@ import { SsoError, ALLOWED_ALGS, JwksUnavailableError, createJwksCache } from '.
|
|
|
2
2
|
export { AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoReauthError, cookieHeader, createJwksCache, createSsoClient, loadSsoConfig, memoryTokenStore, readCookie, signSession, signValue, verifySession, verifyValue } from './chunk-ZPRWYUFG.js';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { request } from 'http';
|
|
5
|
-
import { decodeProtectedHeader, jwtVerify } from 'jose';
|
|
5
|
+
import { generateKeyPair, exportJWK, calculateJwkThumbprint, SignJWT, decodeProtectedHeader, importJWK, jwtVerify } from 'jose';
|
|
6
6
|
|
|
7
|
+
async function createDpopKey() {
|
|
8
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: false });
|
|
9
|
+
const jwk = await exportJWK(publicKey);
|
|
10
|
+
const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
11
|
+
return { privateKey, publicJwk, jkt: await calculateJwkThumbprint(publicJwk) };
|
|
12
|
+
}
|
|
13
|
+
var enc = new TextEncoder();
|
|
14
|
+
var b64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
15
|
+
async function accessTokenHash(ticket) {
|
|
16
|
+
return b64url(await crypto.subtle.digest("SHA-256", enc.encode(ticket)));
|
|
17
|
+
}
|
|
18
|
+
function normalizeHtu(url) {
|
|
19
|
+
const u = new URL(url);
|
|
20
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
21
|
+
}
|
|
22
|
+
async function createDpopProof(key, opts) {
|
|
23
|
+
const payload = {
|
|
24
|
+
jti: crypto.randomUUID(),
|
|
25
|
+
htm: opts.method.toUpperCase(),
|
|
26
|
+
htu: normalizeHtu(opts.url),
|
|
27
|
+
iat: Math.floor((opts.now?.() ?? Date.now()) / 1e3)
|
|
28
|
+
};
|
|
29
|
+
if (opts.ticket !== void 0) payload.ath = await accessTokenHash(opts.ticket);
|
|
30
|
+
return new SignJWT(payload).setProtectedHeader({ typ: "dpop+jwt", alg: "ES256", jwk: key.publicJwk }).sign(key.privateKey);
|
|
31
|
+
}
|
|
32
|
+
function createMemoryReplayStore(now = () => Date.now()) {
|
|
33
|
+
const entries = /* @__PURE__ */ new Map();
|
|
34
|
+
return {
|
|
35
|
+
async seen(key, ttlMs) {
|
|
36
|
+
const t = now();
|
|
37
|
+
for (const [k, until] of entries) if (until <= t) entries.delete(k);
|
|
38
|
+
if (entries.has(key)) return true;
|
|
39
|
+
entries.set(key, t + ttlMs);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
var ASYMMETRIC = /* @__PURE__ */ new Set(["ES256", "ES384", "ES512", "EdDSA", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"]);
|
|
45
|
+
async function verifyDpopProof(proof, o) {
|
|
46
|
+
let header;
|
|
47
|
+
try {
|
|
48
|
+
header = decodeProtectedHeader(proof);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new SsoError("DPoP proof is not a JWS");
|
|
51
|
+
}
|
|
52
|
+
if (header.typ !== "dpop+jwt") throw new SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
|
|
53
|
+
if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
|
|
54
|
+
const jwk = header.jwk;
|
|
55
|
+
if (!jwk || typeof jwk !== "object") throw new SsoError("DPoP proof carries no jwk");
|
|
56
|
+
if ("d" in jwk) throw new SsoError("DPoP proof jwk contains a private key");
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
const key = await importJWK(jwk, header.alg);
|
|
60
|
+
({ payload } = await jwtVerify(proof, key, { typ: "dpop+jwt", algorithms: [header.alg] }));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
throw new SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
63
|
+
}
|
|
64
|
+
if (await calculateJwkThumbprint(jwk) !== o.jkt) throw new SsoError("DPoP proof key does not match the ticket's cnf.jkt");
|
|
65
|
+
if (payload.htm !== o.method.toUpperCase()) throw new SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
|
|
66
|
+
let htu;
|
|
67
|
+
try {
|
|
68
|
+
htu = normalizeHtu(String(payload.htu));
|
|
69
|
+
} catch {
|
|
70
|
+
throw new SsoError("DPoP proof htu is not a URL");
|
|
71
|
+
}
|
|
72
|
+
if (htu !== normalizeHtu(o.url)) throw new SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
|
|
73
|
+
const nowSec = Math.floor(o.now() / 1e3);
|
|
74
|
+
if (typeof payload.iat !== "number" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {
|
|
75
|
+
throw new SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
|
|
76
|
+
}
|
|
77
|
+
if (payload.ath !== await accessTokenHash(o.ticket)) throw new SsoError("DPoP proof ath does not match this ticket");
|
|
78
|
+
if (typeof payload.jti !== "string" || payload.jti === "") throw new SsoError("DPoP proof has no jti");
|
|
79
|
+
if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1e3)) {
|
|
80
|
+
throw new SsoError("DPoP proof was already used (jti replay)");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/fetch-ticket.ts
|
|
7
85
|
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
8
86
|
var FLY_SOCKET = "/.fly/api";
|
|
9
87
|
var NoWorkloadIdentityError = class extends Error {
|
|
@@ -65,6 +143,8 @@ function createTicketClient(options = {}) {
|
|
|
65
143
|
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));
|
|
66
144
|
const cache = /* @__PURE__ */ new Map();
|
|
67
145
|
const inFlight = /* @__PURE__ */ new Map();
|
|
146
|
+
let dpopKey = null;
|
|
147
|
+
const ownKey = () => dpopKey ??= createDpopKey();
|
|
68
148
|
async function platformToken() {
|
|
69
149
|
if (onFly()) return flyOidc(issuer);
|
|
70
150
|
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
@@ -96,11 +176,10 @@ function createTicketClient(options = {}) {
|
|
|
96
176
|
if (scope) form.set("scope", scope);
|
|
97
177
|
let res;
|
|
98
178
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
179
|
+
const tokenUrl = `${issuer}/oauth2/token`;
|
|
180
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
181
|
+
if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: "POST", url: tokenUrl, now });
|
|
182
|
+
res = await fetchImpl(tokenUrl, { method: "POST", headers, body: form });
|
|
104
183
|
} catch (e) {
|
|
105
184
|
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
106
185
|
}
|
|
@@ -119,7 +198,12 @@ function createTicketClient(options = {}) {
|
|
|
119
198
|
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
120
199
|
return { token: body.access_token, exp };
|
|
121
200
|
}
|
|
122
|
-
|
|
201
|
+
const client = {
|
|
202
|
+
async authHeaders({ audience, scope, method, url }) {
|
|
203
|
+
const ticket = await client.get({ audience, scope });
|
|
204
|
+
if (!options.dpop) return { authorization: `Bearer ${ticket}` };
|
|
205
|
+
return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };
|
|
206
|
+
},
|
|
123
207
|
async get({ audience, scope }) {
|
|
124
208
|
const key = `${audience} ${scope ?? ""}`;
|
|
125
209
|
const hit = cache.get(key);
|
|
@@ -134,6 +218,7 @@ function createTicketClient(options = {}) {
|
|
|
134
218
|
return p;
|
|
135
219
|
}
|
|
136
220
|
};
|
|
221
|
+
return client;
|
|
137
222
|
}
|
|
138
223
|
var shared = null;
|
|
139
224
|
function fetchTicket(opts) {
|
|
@@ -141,14 +226,20 @@ function fetchTicket(opts) {
|
|
|
141
226
|
return shared.get(opts);
|
|
142
227
|
}
|
|
143
228
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
229
|
+
var TICKET_FORMAT_VERSION = 1;
|
|
230
|
+
var MAX_ACT_DEPTH = 5;
|
|
144
231
|
function createTicketVerifier(options) {
|
|
145
232
|
const {
|
|
146
233
|
issuer,
|
|
147
234
|
audience,
|
|
148
235
|
maxLifetimeSec = 15 * 60,
|
|
149
236
|
clockToleranceSec = 30,
|
|
150
|
-
fetchImpl = fetch
|
|
237
|
+
fetchImpl = fetch,
|
|
238
|
+
requireDpop = false,
|
|
239
|
+
dpopIatWindowSec = 60,
|
|
240
|
+
now = () => Date.now()
|
|
151
241
|
} = options;
|
|
242
|
+
const replayStore = options.replayStore ?? createMemoryReplayStore(now);
|
|
152
243
|
if (!issuer) throw new SsoError("createTicketVerifier needs an issuer");
|
|
153
244
|
if (!audience) throw new SsoError("createTicketVerifier needs this receiver's audience");
|
|
154
245
|
let cachePromise = null;
|
|
@@ -222,14 +313,38 @@ function createTicketVerifier(options) {
|
|
|
222
313
|
const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
223
314
|
const missing = required.filter((s) => !scopes.includes(s));
|
|
224
315
|
if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
225
|
-
const
|
|
226
|
-
|
|
316
|
+
const ver = payload.ver === void 0 ? 0 : payload.ver;
|
|
317
|
+
if (typeof ver !== "number" || !Number.isInteger(ver) || ver < 0) {
|
|
318
|
+
throw new SsoError(`ticket has an invalid ver (got ${JSON.stringify(payload.ver)})`);
|
|
319
|
+
}
|
|
320
|
+
if (ver > TICKET_FORMAT_VERSION) {
|
|
321
|
+
throw new SsoError(`ticket format version ${ver} is newer than this @broberg/sso understands (${TICKET_FORMAT_VERSION}) \u2014 upgrade @broberg/sso`);
|
|
322
|
+
}
|
|
323
|
+
const act = payload.act === void 0 ? null : readActor(payload.act, 1);
|
|
324
|
+
const cnfRaw = payload.cnf;
|
|
325
|
+
const cnf = cnfRaw && typeof cnfRaw.jkt === "string" && cnfRaw.jkt ? { jkt: cnfRaw.jkt } : null;
|
|
326
|
+
if (cnf) {
|
|
327
|
+
if (!opts.dpop?.proof) throw new SsoError("ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented");
|
|
328
|
+
await verifyDpopProof(opts.dpop.proof, {
|
|
329
|
+
method: opts.dpop.method,
|
|
330
|
+
url: opts.dpop.url,
|
|
331
|
+
ticket: token,
|
|
332
|
+
jkt: cnf.jkt,
|
|
333
|
+
replay: replayStore,
|
|
334
|
+
iatWindowSec: dpopIatWindowSec,
|
|
335
|
+
now
|
|
336
|
+
});
|
|
337
|
+
} else if (requireDpop) {
|
|
338
|
+
throw new SsoError(cnfRaw !== void 0 ? "ticket has a cnf without a string jkt \u2014 DPoP is required here" : "this receiver requires DPoP; the ticket is not bound (no cnf.jkt)");
|
|
339
|
+
}
|
|
227
340
|
return {
|
|
228
341
|
principal: payload.sub,
|
|
229
342
|
type,
|
|
230
343
|
clientId: typeof payload.client_id === "string" && payload.client_id ? payload.client_id : payload.sub,
|
|
231
344
|
org: typeof payload.org === "string" && payload.org ? payload.org : null,
|
|
232
345
|
act,
|
|
346
|
+
version: ver,
|
|
347
|
+
cnf,
|
|
233
348
|
scopes,
|
|
234
349
|
exp: payload.exp,
|
|
235
350
|
jti: String(payload.jti)
|
|
@@ -237,7 +352,15 @@ function createTicketVerifier(options) {
|
|
|
237
352
|
}
|
|
238
353
|
};
|
|
239
354
|
}
|
|
355
|
+
function readActor(raw, depth) {
|
|
356
|
+
if (depth > MAX_ACT_DEPTH) throw new SsoError(`ticket's delegation chain is deeper than ${MAX_ACT_DEPTH}`);
|
|
357
|
+
const link = raw;
|
|
358
|
+
if (!link || typeof link !== "object" || typeof link.sub !== "string" || link.sub === "") {
|
|
359
|
+
throw new SsoError(`ticket's act (link ${depth}) has no string sub`);
|
|
360
|
+
}
|
|
361
|
+
return link.act === void 0 ? { sub: link.sub } : { sub: link.sub, act: readActor(link.act, depth + 1) };
|
|
362
|
+
}
|
|
240
363
|
|
|
241
|
-
export { DEFAULT_ISSUER, NoWorkloadIdentityError, TicketExchangeError, TicketUnavailableError, createTicketClient, createTicketVerifier, fetchTicket };
|
|
364
|
+
export { DEFAULT_ISSUER, MAX_ACT_DEPTH, NoWorkloadIdentityError, TICKET_FORMAT_VERSION, TicketExchangeError, TicketUnavailableError, accessTokenHash, createDpopKey, createDpopProof, createMemoryReplayStore, createTicketClient, createTicketVerifier, fetchTicket, normalizeHtu, verifyDpopProof };
|
|
242
365
|
//# sourceMappingURL=index.js.map
|
|
243
366
|
//# 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":";;;;;;AAkBO,IAAM,cAAA,GAAiB;AAC9B,IAAM,UAAA,GAAa,WAAA;AAGZ,IAAM,uBAAA,GAAN,cAAsC,KAAA,CAAM;AAAA,EACjD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EACd;AACF;AAOO,IAAM,mBAAA,GAAN,cAAkC,KAAA,CAAM;AAAA,EAC7C,WAAA,CAAY,OAAA,EAA0B,IAAA,EAAuB,MAAA,EAAgB;AAC3E,IAAA,KAAA,CAAM,OAAO,CAAA;AADuB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAAuB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAE3D,IAAA,IAAA,CAAK,IAAA,GAAO,qBAAA;AAAA,EACd;AAAA,EAHsC,IAAA;AAAA,EAAuB,MAAA;AAI/D;AAGO,IAAM,sBAAA,GAAN,cAAqC,KAAA,CAAM;AAAA,EAChD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,wBAAA;AAAA,EACd;AACF;AAqBA,SAAS,kBAAkB,GAAA,EAA8B;AACvD,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,EAAE,KAAK,CAAA;AACnC,IAAA,MAAM,GAAA,GAAM,OAAA;AAAA,MACV,EAAE,UAAA,EAAY,UAAA,EAAY,IAAA,EAAM,iBAAA,EAAmB,QAAQ,MAAA,EAAQ,OAAA,EAAS,EAAE,cAAA,EAAgB,oBAAoB,gBAAA,EAAkB,MAAA,CAAO,UAAA,CAAW,IAAI,GAAE,EAAE;AAAA,MAC9J,CAAC,GAAA,KAAQ;AACP,QAAA,IAAI,IAAA,GAAO,EAAA;AACX,QAAA,GAAA,CAAI,YAAY,MAAM,CAAA;AACtB,QAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,CAAA,KAAO,QAAQ,CAAE,CAAA;AACjC,QAAA,GAAA,CAAI,EAAA,CAAG,OAAO,MAAM;AAClB,UAAA,IAAA,CAAK,GAAA,CAAI,UAAA,IAAc,CAAA,KAAM,GAAA,IAAA,CAAQ,GAAA,CAAI,UAAA,IAAc,CAAA,IAAK,GAAA,EAAK,OAAA,CAAQ,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,sBACxE,IAAI,sBAAA,CAAuB,qBAAqB,GAAA,CAAI,UAAU,EAAE,CAAC,CAAA;AAAA,QAC/E,CAAC,CAAA;AAAA,MACH;AAAA,KACF;AACA,IAAA,GAAA,CAAI,EAAA,CAAG,OAAA,EAAS,CAAC,CAAA,KAAM,MAAA,CAAO,IAAI,sBAAA,CAAuB,CAAA,iBAAA,EAAoB,CAAA,CAAE,OAAO,CAAA,CAAE,CAAC,CAAC,CAAA;AAC1F,IAAA,GAAA,CAAI,IAAI,IAAI,CAAA;AAAA,EACd,CAAC,CAAA;AACH;AAGA,SAAS,OAAO,KAAA,EAA8B;AAC5C,EAAA,IAAI;AACF,IAAA,MAAM,UAAU,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,IAAA,CAAK,MAAM,KAAA,CAAM,GAAG,CAAA,CAAE,CAAC,KAAK,EAAA,EAAI,WAAW,CAAA,CAAE,QAAA,CAAS,MAAM,CAAC,CAAA;AAC/F,IAAA,OAAO,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,GAAW,QAAQ,GAAA,GAAM,IAAA;AAAA,EACzD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,kBAAA,CAAmB,OAAA,GAA+B,EAAC,EAAiB;AAClF,EAAA,MAAM,UAAU,OAAA,CAAQ,MAAA,IAAU,cAAA,EAAgB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACpE,EAAA,MAAM,cAAA,GAAiB,QAAQ,cAAA,IAAkB,EAAA;AACjD,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;AACnC,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,KAAA;AACvC,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,KAAQ,MAAM,KAAK,GAAA,EAAI,CAAA;AAC3C,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,iBAAA;AACnC,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,KAAU,MAAM,CAAC,CAAC,GAAA,CAAI,YAAA,IAAgB,UAAA,CAAW,UAAU,CAAA,CAAA;AAEjF,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA4C;AAC9D,EAAA,MAAM,QAAA,uBAAe,GAAA,EAA6B;AAElD,EAAA,eAAe,aAAA,GAAiC;AAC9C,IAAA,IAAI,KAAA,EAAM,EAAG,OAAO,OAAA,CAAQ,MAAM,CAAA;AAClC,IAAA,MAAM,MAAM,GAAA,CAAI,4BAAA;AAChB,IAAA,MAAM,SAAS,GAAA,CAAI,8BAAA;AACnB,IAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,GAAA,GAAM,MAAM,SAAA,CAAU,CAAA,EAAG,GAAG,CAAA,UAAA,EAAa,mBAAmB,MAAM,CAAC,CAAA,CAAA,EAAI,EAAE,SAAS,EAAE,aAAA,EAAe,UAAU,MAAM,CAAA,CAAA,IAAM,CAAA;AAAA,MAC3H,SAAS,CAAA,EAAG;AACV,QAAA,MAAM,IAAI,sBAAA,CAAuB,CAAA,kCAAA,EAAqC,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,MACpH;AACA,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI,MAAM,IAAI,sBAAA,CAAuB,CAAA,8BAAA,EAAiC,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC3F,MAAA,MAAM,EAAE,KAAA,EAAM,GAAK,MAAM,IAAI,IAAA,EAAK;AAClC,MAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,uBAAuB,wCAAwC,CAAA;AACrF,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,MAAM,IAAI,uBAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,eAAe,QAAA,CAAS,UAAkB,KAAA,EAAoE;AAC5G,IAAA,MAAM,OAAA,GAAU,MAAM,aAAA,EAAc;AACpC,IAAA,MAAM,IAAA,GAAO,IAAI,eAAA,CAAgB;AAAA,MAC/B,UAAA,EAAY,iDAAA;AAAA,MACZ,aAAA,EAAe,OAAA;AAAA,MACf,kBAAA,EAAoB,sCAAA;AAAA,MACpB;AAAA,KACD,CAAA;AACD,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,KAAK,CAAA;AAClC,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI;AACF,MAAA,GAAA,GAAM,MAAM,SAAA,CAAU,CAAA,EAAG,MAAM,CAAA,aAAA,CAAA,EAAiB;AAAA,QAC9C,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,mCAAA,EAAoC;AAAA,QAC/D,IAAA,EAAM;AAAA,OACP,CAAA;AAAA,IACH,SAAS,CAAA,EAAG;AACV,MAAA,MAAM,IAAI,sBAAA,CAAuB,CAAA,EAAG,MAAM,CAAA,cAAA,EAAiB,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,IACzG;AACA,IAAA,IAAI,OAAmG,EAAC;AACxG,IAAA,IAAI;AACF,MAAA,IAAA,GAAQ,MAAM,IAAI,IAAA,EAAK;AAAA,IACzB,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI,GAAA,CAAI,MAAA,IAAU,GAAA,EAAK,MAAM,IAAI,sBAAA,CAAuB,CAAA,EAAG,MAAM,CAAA,UAAA,EAAa,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC1F,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,IAAM,CAAC,KAAK,YAAA,EAAc;AACjC,MAAA,MAAM,IAAA,GAAO,KAAK,iBAAA,IAAqB,EAAA;AACvC,MAAA,MAAM,OAAO,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAAI,KAAK,KAAA,CAAM,CAAA,EAAG,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAC,CAAA,CAAE,IAAA,EAAK,GAAI,KAAK,KAAA,IAAS,kBAAA;AAC1F,MAAA,MAAM,IAAI,mBAAA,CAAoB,CAAA,kCAAA,EAAqC,QAAQ,CAAA,EAAA,EAAK,IAAA,IAAQ,IAAA,CAAK,KAAA,IAAS,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,IAAA,EAAM,IAAI,MAAM,CAAA;AAAA,IACtI;AACA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAA,KAAQ,GAAI,CAAA;AACtC,IAAA,MAAM,MAAM,MAAA,CAAO,IAAA,CAAK,YAAY,CAAA,IAAK,MAAA,IAAU,KAAK,UAAA,IAAc,GAAA,CAAA;AACtE,IAAA,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,YAAA,EAAc,GAAA,EAAI;AAAA,EACzC;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,GAAA,CAAI,EAAE,QAAA,EAAU,OAAM,EAAG;AAC7B,MAAA,MAAM,GAAA,GAAM,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,SAAS,EAAE,CAAA,CAAA;AACtC,MAAA,MAAM,GAAA,GAAM,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AACzB,MAAA,IAAI,GAAA,IAAO,IAAI,GAAA,GAAM,cAAA,GAAiB,KAAI,GAAI,GAAA,SAAa,GAAA,CAAI,KAAA;AAC/D,MAAA,MAAM,OAAA,GAAU,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AAChC,MAAA,IAAI,SAAS,OAAO,OAAA;AACpB,MAAA,MAAM,IAAI,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA,CAC/B,IAAA,CAAK,CAAC,CAAA,KAAM;AACX,QAAA,KAAA,CAAM,GAAA,CAAI,KAAK,CAAC,CAAA;AAChB,QAAA,OAAO,CAAA,CAAE,KAAA;AAAA,MACX,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM,QAAA,CAAS,MAAA,CAAO,GAAG,CAAC,CAAA;AACrC,MAAA,QAAA,CAAS,GAAA,CAAI,KAAK,CAAC,CAAA;AACnB,MAAA,OAAO,CAAA;AAAA,IACT;AAAA,GACF;AACF;AAEA,IAAI,MAAA,GAA8B,IAAA;AAE3B,SAAS,YAAY,IAAA,EAA6D;AACvF,EAAA,MAAA,KAAW,kBAAA,EAAmB;AAC9B,EAAA,OAAO,MAAA,CAAO,IAAI,IAAI,CAAA;AACxB;ACpKA,IAAM,eAAA,GAA4C,CAAC,OAAA,EAAS,SAAA,EAAW,OAAO,CAAA;AA6CvE,SAAS,qBAAqB,OAAA,EAAgD;AACnF,EAAA,MAAM;AAAA,IACJ,MAAA;AAAA,IACA,QAAA;AAAA,IACA,iBAAiB,EAAA,GAAK,EAAA;AAAA,IACtB,iBAAA,GAAoB,EAAA;AAAA,IACpB,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AACJ,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,SAAS,sCAAsC,CAAA;AACtE,EAAA,IAAI,CAAC,QAAA,EAAU,MAAM,IAAI,SAAS,qDAAqD,CAAA;AAEvF,EAAA,IAAI,YAAA,GAA0C,IAAA;AAC9C,EAAA,MAAM,QAAA,GAAW,CAAC,OAAA,KAChB,eAAA,CAAgB;AAAA,IACd,OAAA;AAAA,IACA,SAAA;AAAA,IACA,GAAI,OAAA,CAAQ,MAAA,GAAS,EAAE,MAAA,EAAQ,IAAA,KAAS,EAAC;AAAA,IACzC,GAAI,QAAQ,oBAAA,KAAyB,MAAA,GAAY,EAAE,oBAAA,EAAsB,OAAA,CAAQ,oBAAA,EAAqB,GAAI;AAAC,GAC5G,CAAA;AAEH,EAAA,eAAe,IAAA,GAA2B;AACxC,IAAA,YAAA,KAAA,CAAkB,YAAY;AAC5B,MAAA,IAAI,OAAA,CAAQ,OAAA,EAAS,OAAO,QAAA,CAAS,QAAQ,OAAO,CAAA;AACpD,MAAA,MAAM,GAAA,GAAM,GAAG,MAAM,CAAA,iCAAA,CAAA;AACrB,MAAA,IAAI,GAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,SAAA,CAAU,GAAG,CAAA;AAC/B,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AACrD,QAAA,GAAA,GAAO,MAAM,IAAI,IAAA,EAAK;AAAA,MACxB,SAAS,KAAA,EAAO;AAEd,QAAA,MAAM,IAAI,oBAAA,CAAqB,CAAA,EAAG,GAAG,CAAA,EAAA,EAAK,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,MAAA,CAAO,KAAK,CAAC,CAAA,CAAE,CAAA;AAAA,MACpG;AACA,MAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,QAAA,MAAM,IAAI,SAAS,CAAA,UAAA,EAAa,MAAM,QAAQ,GAAG,CAAA,MAAA,EAAS,GAAA,CAAI,MAAM,CAAA,+BAAA,CAA4B,CAAA;AAAA,MAClG;AACA,MAAA,IAAI,CAAC,IAAI,QAAA,EAAU,MAAM,IAAI,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA,kBAAA,CAAoB,CAAA;AAChE,MAAA,OAAO,QAAA,CAAS,IAAI,QAAQ,CAAA;AAAA,IAC9B,CAAA,GAAG,CAAE,KAAA,CAAM,CAAC,GAAA,KAAQ;AAClB,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,MAAM,GAAA;AAAA,IACR,CAAC,CAAA;AACD,IAAA,OAAO,YAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAQ,MAAA,EAAQ,KAAK,IAAA,EAAK,CAAE,MAAM,MAAM;AAAA,EAAC,CAAC,CAAA;AAE9C,EAAA,OAAO;AAAA,IACL,MAAM,MAAA,CAAO,KAAA,EAAO,IAAA,GAAO,EAAC,EAAG;AAC7B,MAAA,IAAI,MAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAA,GAAS,sBAAsB,KAAK,CAAA;AAAA,MACtC,CAAA,CAAA,MAAQ;AACN,QAAA,MAAM,IAAI,SAAS,qBAAqB,CAAA;AAAA,MAC1C;AACA,MAAA,IAAI,CAAC,MAAA,CAAO,GAAA,EAAK,MAAM,IAAI,SAAS,oDAA+C,CAAA;AACnF,MAAA,IAAI,OAAO,GAAA,KAAQ,YAAA,EAAc,MAAM,IAAI,SAAS,gCAAgC,CAAA;AAEpF,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,EAAK;AACzB,MAAA,IAAI,OAAA;AACJ,MAAA,IAAI;AACF,QAAA,CAAC,EAAE,OAAA,EAAQ,GAAI,MAAM,UAAU,KAAA,EAAO,YAAY,KAAA,CAAM,MAAA,CAAO,MAAA,CAAO,GAAA,EAAM,MAAA,CAAO,GAAA,IAAO,OAAO,CAAA,EAAG;AAAA,UAClG,MAAA;AAAA,UACA,QAAA;AAAA,UACA,UAAA,EAAY,YAAA;AAAA,UACZ,cAAA,EAAgB,iBAAA;AAAA,UAChB,cAAA,EAAgB,CAAC,KAAA,EAAO,KAAA,EAAO,OAAO,KAAK;AAAA,SAC5C,CAAA;AAAA,MACH,SAAS,CAAA,EAAG;AAEV,QAAA,IAAI,CAAA,YAAa,sBAAsB,MAAM,CAAA;AAC7C,QAAA,MAAM,IAAI,QAAA,CAAS,CAAA,iBAAA,EAAoB,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,MACrF;AAIA,MAAA,IAAI,QAAA,IAAY,OAAA,EAAS,MAAM,IAAI,SAAS,gCAAgC,CAAA;AAC5E,MAAA,IAAI,OAAA,IAAW,OAAA,EAAS,MAAM,IAAI,SAAS,6BAA6B,CAAA;AAExE,MAAA,MAAM,OAAO,OAAA,CAAQ,cAAA;AACrB,MAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,CAAC,eAAA,CAAgB,QAAA,CAAS,IAAqB,CAAA,EAAG;AAChF,QAAA,MAAM,IAAI,QAAA,CAAS,CAAA,wCAAA,EAA2C,KAAK,SAAA,CAAU,IAAI,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,MACvF;AACA,MAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,GAAO,OAAA,CAAQ,GAAA;AACxC,MAAA,IAAI,WAAW,cAAA,EAAgB;AAC7B,QAAA,MAAM,IAAI,QAAA,CAAS,CAAA,gBAAA,EAAmB,QAAQ,CAAA,oBAAA,EAAuB,cAAc,CAAA,aAAA,CAAe,CAAA;AAAA,MACpG;AACA,MAAA,IAAI,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,IAAY,OAAA,CAAQ,QAAQ,EAAA,EAAI,MAAM,IAAI,QAAA,CAAS,mBAAmB,CAAA;AAEjG,MAAA,MAAM,MAAA,GAAS,OAAO,OAAA,CAAQ,KAAA,KAAU,QAAA,GAAW,OAAA,CAAQ,KAAA,CAAM,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,IAAI,EAAC;AAC/F,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,KAAU,MAAA,GAAY,EAAC,GAAI,KAAA,CAAM,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA,CAAK,KAAA,GAAQ,CAAC,KAAK,KAAK,CAAA;AACrG,MAAA,MAAM,OAAA,GAAU,SAAS,MAAA,CAAO,CAAC,MAAM,CAAC,MAAA,CAAO,QAAA,CAAS,CAAC,CAAC,CAAA;AAC1D,MAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,MAAM,IAAI,QAAA,CAAS,sBAAsB,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAEjF,MAAA,MAAM,SAAS,OAAA,CAAQ,GAAA;AACvB,MAAA,MAAM,GAAA,GAAM,MAAA,IAAU,OAAO,MAAA,CAAO,GAAA,KAAQ,QAAA,IAAY,MAAA,CAAO,GAAA,GAAM,EAAE,GAAA,EAAK,MAAA,CAAO,GAAA,EAAI,GAAI,IAAA;AAC3F,MAAA,OAAO;AAAA,QACL,WAAW,OAAA,CAAQ,GAAA;AAAA,QACnB,IAAA;AAAA,QACA,QAAA,EAAU,OAAO,OAAA,CAAQ,SAAA,KAAc,YAAY,OAAA,CAAQ,SAAA,GAAY,OAAA,CAAQ,SAAA,GAAY,OAAA,CAAQ,GAAA;AAAA,QACnG,GAAA,EAAK,OAAO,OAAA,CAAQ,GAAA,KAAQ,YAAY,OAAA,CAAQ,GAAA,GAAM,QAAQ,GAAA,GAAM,IAAA;AAAA,QACpE,GAAA;AAAA,QACA,MAAA;AAAA,QACA,KAAK,OAAA,CAAQ,GAAA;AAAA,QACb,GAAA,EAAK,MAAA,CAAO,OAAA,CAAQ,GAAG;AAAA,OACzB;AAAA,IACF;AAAA,GACF;AACF","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/dpop.ts","../src/fetch-ticket.ts","../src/ticket.ts"],"names":["decodeProtectedHeader","jwtVerify"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * F084.157 — DPoP (RFC 9449): a bound ticket works only for whoever holds the key\n * (broberg-id-F087.16, joint design #2572/#2574).\n *\n * A DPoP-bound ticket carries `cnf.jkt`, the thumbprint of the caller's public\n * key. With every request the caller signs a short PROOF (method, URL, time, a\n * hash of the ticket) with the matching private key. A ticket copied out of a log\n * is useless on another machine: it has the ticket, not the key.\n *\n * THE RULE THAT MAKES IT PROTECTION (agreed with broberg-id, (A) in #2574): once a\n * receiver verifies with this version, a ticket WITH cnf.jkt needs a valid proof —\n * also while DPoP is otherwise optional. `requireDpop` only decides whether\n * UNBOUND tickets are still accepted. Were bound tickets accepted without a proof\n * during the rollout, a stolen bound ticket would work as bearer the whole time.\n *\n * Not here yet: server nonces (RFC 9449 §8). The jti replay store covers replay\n * inside the iat window.\n */\nimport {\n SignJWT,\n calculateJwkThumbprint,\n decodeProtectedHeader,\n exportJWK,\n generateKeyPair,\n importJWK,\n jwtVerify,\n type JWK,\n} from \"jose\";\nimport { SsoError } from \"./client.js\";\n\n/** One process's DPoP key. The private half cannot be exported. */\nexport interface DpopKey {\n privateKey: CryptoKey;\n publicJwk: JWK;\n /** RFC 7638 thumbprint — what BID puts in the ticket's cnf.jkt. */\n jkt: string;\n}\n\n/** A fresh ES256 key pair, private key non-extractable, never written anywhere. */\nexport async function createDpopKey(): Promise<DpopKey> {\n const { privateKey, publicKey } = await generateKeyPair(\"ES256\", { extractable: false });\n const jwk = await exportJWK(publicKey); // a public key is always exportable\n const publicJwk: JWK = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };\n return { privateKey, publicJwk, jkt: await calculateJwkThumbprint(publicJwk) };\n}\n\nconst enc = new TextEncoder();\nconst b64url = (bytes: ArrayBuffer) =>\n btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n\n/** `ath`: base64url(SHA-256(ticket)) — ties a proof to one ticket. */\nexport async function accessTokenHash(ticket: string): Promise<string> {\n return b64url(await crypto.subtle.digest(\"SHA-256\", enc.encode(ticket)));\n}\n\n/** RFC 9449 §4.3: htu is compared without query and fragment. */\nexport function normalizeHtu(url: string): string {\n const u = new URL(url);\n return `${u.protocol}//${u.host}${u.pathname}`;\n}\n\n/** Sign one proof. Pass `ticket` when calling a resource server (adds ath); omit it for BID's token endpoint. */\nexport async function createDpopProof(\n key: DpopKey,\n opts: { method: string; url: string; ticket?: string; now?: () => number },\n): Promise<string> {\n const payload: Record<string, unknown> = {\n jti: crypto.randomUUID(),\n htm: opts.method.toUpperCase(),\n htu: normalizeHtu(opts.url),\n iat: Math.floor((opts.now?.() ?? Date.now()) / 1000),\n };\n if (opts.ticket !== undefined) payload.ath = await accessTokenHash(opts.ticket);\n return new SignJWT(payload).setProtectedHeader({ typ: \"dpop+jwt\", alg: \"ES256\", jwk: key.publicJwk }).sign(key.privateKey);\n}\n\n/**\n * Remembers which proofs were already used. The default is in-memory, which\n * holds on ONE machine: a receiver running on two machines must pass a shared\n * store, or a proof replayed against the other machine inside the window passes.\n */\nexport interface ReplayStore {\n /** Record `key`; resolve true if it was already recorded within `ttlMs`. */\n seen(key: string, ttlMs: number): Promise<boolean>;\n}\n\nexport function createMemoryReplayStore(now: () => number = () => Date.now()): ReplayStore {\n const entries = new Map<string, number>();\n return {\n async seen(key, ttlMs) {\n const t = now();\n for (const [k, until] of entries) if (until <= t) entries.delete(k);\n if (entries.has(key)) return true;\n entries.set(key, t + ttlMs);\n return false;\n },\n };\n}\n\nexport interface VerifyProofOptions {\n method: string;\n /** The receiver's PUBLIC URL for this request (behind a proxy the process sees an internal one). */\n url: string;\n ticket: string;\n jkt: string;\n replay: ReplayStore;\n iatWindowSec: number;\n now: () => number;\n}\n\nconst ASYMMETRIC = new Set([\"ES256\", \"ES384\", \"ES512\", \"EdDSA\", \"PS256\", \"PS384\", \"PS512\", \"RS256\", \"RS384\", \"RS512\"]);\n\n/** Throws SsoError naming the first thing that is wrong. */\nexport async function verifyDpopProof(proof: string, o: VerifyProofOptions): Promise<void> {\n let header;\n try {\n header = decodeProtectedHeader(proof);\n } catch {\n throw new SsoError(\"DPoP proof is not a JWS\");\n }\n if (header.typ !== \"dpop+jwt\") throw new SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not \"dpop+jwt\"`);\n if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);\n const jwk = header.jwk as JWK | undefined;\n if (!jwk || typeof jwk !== \"object\") throw new SsoError(\"DPoP proof carries no jwk\");\n if (\"d\" in jwk) throw new SsoError(\"DPoP proof jwk contains a private key\");\n\n let payload;\n try {\n const key = await importJWK(jwk, header.alg);\n ({ payload } = await jwtVerify(proof, key, { typ: \"dpop+jwt\", algorithms: [header.alg] }));\n } catch (e) {\n throw new SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n if ((await calculateJwkThumbprint(jwk)) !== o.jkt) throw new SsoError(\"DPoP proof key does not match the ticket's cnf.jkt\");\n if (payload.htm !== o.method.toUpperCase()) throw new SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);\n let htu: string;\n try {\n htu = normalizeHtu(String(payload.htu));\n } catch {\n throw new SsoError(\"DPoP proof htu is not a URL\");\n }\n if (htu !== normalizeHtu(o.url)) throw new SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);\n const nowSec = Math.floor(o.now() / 1000);\n if (typeof payload.iat !== \"number\" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {\n throw new SsoError(`DPoP proof iat is outside ±${o.iatWindowSec}s`);\n }\n if (payload.ath !== (await accessTokenHash(o.ticket))) throw new SsoError(\"DPoP proof ath does not match this ticket\");\n if (typeof payload.jti !== \"string\" || payload.jti === \"\") throw new SsoError(\"DPoP proof has no jti\");\n if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1000)) {\n throw new SsoError(\"DPoP proof was already used (jti replay)\");\n }\n}\n","/**\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\";\nimport { createDpopKey, createDpopProof, type DpopKey } from \"./dpop.js\";\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 * F084.157 — ask BID for DPoP-BOUND tickets. One ES256 key per client\n * (non-extractable, never stored); every call then needs a proof, which\n * `authHeaders` makes. Default false: bearer tickets, exactly as before.\n */\n dpop?: boolean;\n}\n\n/** Headers for one call to a receiver. With `dpop`, `authorization` is `DPoP <ticket>` and `dpop` is the proof. */\nexport interface TicketAuthHeaders {\n authorization: string;\n dpop?: string;\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 * F084.157 — the headers for ONE request to a receiver: a ticket plus, with\n * `dpop`, a fresh proof bound to this method and URL (use it once). Without\n * `dpop`: `{ authorization: \"Bearer <ticket>\" }`.\n */\n authHeaders(opts: { audience: string; scope?: string; method: string; url: string }): Promise<TicketAuthHeaders>;\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 let dpopKey: Promise<DpopKey> | null = null;\n const ownKey = () => (dpopKey ??= createDpopKey());\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 const tokenUrl = `${issuer}/oauth2/token`;\n const headers: Record<string, string> = { \"content-type\": \"application/x-www-form-urlencoded\" };\n if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: \"POST\", url: tokenUrl, now });\n res = await fetchImpl(tokenUrl, { method: \"POST\", headers, body: form });\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 const client: TicketClient = {\n async authHeaders({ audience, scope, method, url }) {\n const ticket = await client.get({ audience, scope });\n if (!options.dpop) return { authorization: `Bearer ${ticket}` };\n return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };\n },\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 return client;\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\";\nimport { createMemoryReplayStore, verifyDpopProof, type ReplayStore } from \"./dpop.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 * Since 0.16.0 (F084.157) a ticket with cnf.jkt is only accepted together with\n * a valid DPoP proof for that key — see `verify(token, { dpop })`.\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 * F084.157 — refuse UNBOUND tickets (bearer). Off by default during the\n * rollout. A BOUND ticket (cnf.jkt) needs a valid proof either way.\n */\n requireDpop?: boolean;\n /** Where used proof ids are remembered. Default in-memory — ONE machine only; share one across machines. */\n replayStore?: ReplayStore;\n /** How far a proof's iat may be from now. Default 60 s. */\n dpopIatWindowSec?: number;\n /** Injectable clock for the DPoP checks (ms). */\n now?: () => number;\n}\n\n/** The DPoP half of a request: the `DPoP` header, and the method + PUBLIC URL the receiver was called on. */\nexport interface DpopRequest {\n proof: string | null | undefined;\n method: string;\n url: string;\n}\n\nexport interface TicketVerifier {\n /**\n * Verify a ticket; with `scope`, also require that scope to be granted. Pass\n * `dpop` with the request's DPoP header, method and public URL — a bound ticket\n * is refused without it.\n */\n verify(token: string, options?: { scope?: string | string[]; dpop?: DpopRequest }): 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 requireDpop = false,\n dpopIatWindowSec = 60,\n now = () => Date.now(),\n } = options;\n const replayStore = options.replayStore ?? createMemoryReplayStore(now);\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 // F084.157 — a bound ticket needs its proof, always; requireDpop only\n // decides about unbound ones (and then a cnf it cannot read is refused too).\n if (cnf) {\n if (!opts.dpop?.proof) throw new SsoError(\"ticket is DPoP-bound (cnf.jkt) but no DPoP proof was presented\");\n await verifyDpopProof(opts.dpop.proof, {\n method: opts.dpop.method,\n url: opts.dpop.url,\n ticket: token,\n jkt: cnf.jkt,\n replay: replayStore,\n iatWindowSec: dpopIatWindowSec,\n now,\n });\n } else if (requireDpop) {\n throw new SsoError(cnfRaw !== undefined ? \"ticket has a cnf without a string jkt — DPoP is required here\" : \"this receiver requires DPoP; the ticket is not bound (no cnf.jkt)\");\n }\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.16.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",
|