@broberg/sso 0.19.1 → 0.21.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 +54 -0
- package/dist/{chunk-2F3RPYJV.cjs → chunk-2XMKZWC6.cjs} +314 -7
- package/dist/chunk-2XMKZWC6.cjs.map +1 -0
- package/dist/{chunk-CCDRB6NJ.js → chunk-UDUPP35Z.js} +302 -9
- package/dist/chunk-UDUPP35Z.js.map +1 -0
- package/dist/hono.cjs +38 -37
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +7 -1
- package/dist/hono.d.ts +7 -1
- package/dist/hono.js +3 -2
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +112 -291
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -60
- package/dist/index.d.ts +95 -60
- package/dist/index.js +4 -227
- package/dist/index.js.map +1 -1
- package/dist/{session-qvhMg3ID.d.cts → session-DnotfVNd.d.cts} +26 -1
- package/dist/{session-qvhMg3ID.d.ts → session-DnotfVNd.d.ts} +26 -1
- package/package.json +1 -1
- package/dist/chunk-2F3RPYJV.cjs.map +0 -1
- package/dist/chunk-CCDRB6NJ.js.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { importJWK, decodeProtectedHeader, jwtVerify } from 'jose';
|
|
1
|
+
import { importJWK, generateKeyPair, exportJWK, calculateJwkThumbprint, SignJWT, decodeProtectedHeader, jwtVerify } from 'jose';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { request } from 'http';
|
|
2
4
|
|
|
3
5
|
// src/config.ts
|
|
4
6
|
var SsoConfigError = class extends Error {
|
|
@@ -14,6 +16,12 @@ function required(env, name, hint) {
|
|
|
14
16
|
}
|
|
15
17
|
return raw.trim();
|
|
16
18
|
}
|
|
19
|
+
function parseClientAuth(raw) {
|
|
20
|
+
const v = raw?.trim();
|
|
21
|
+
if (!v) return void 0;
|
|
22
|
+
if (v === "secret" || v === "fly") return v;
|
|
23
|
+
throw new SsoConfigError(`SSO_CLIENT_AUTH must be "secret" or "fly" (got ${JSON.stringify(raw)}).`);
|
|
24
|
+
}
|
|
17
25
|
var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 7;
|
|
18
26
|
function loadSsoConfig(env = process.env) {
|
|
19
27
|
const rawIssuer = required(
|
|
@@ -68,7 +76,8 @@ function loadSsoConfig(env = process.env) {
|
|
|
68
76
|
cookieName: env.SSO_COOKIE_NAME?.trim() || "bid_session",
|
|
69
77
|
sessionMaxAge,
|
|
70
78
|
postLogoutRedirectUri: env.SSO_POST_LOGOUT_REDIRECT_URI?.trim() || void 0,
|
|
71
|
-
clientSecret: env.SSO_CLIENT_SECRET?.trim() || void 0
|
|
79
|
+
clientSecret: env.SSO_CLIENT_SECRET?.trim() || void 0,
|
|
80
|
+
clientAuth: parseClientAuth(env.SSO_CLIENT_AUTH)
|
|
72
81
|
};
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -284,6 +293,184 @@ function createJwksCache(options) {
|
|
|
284
293
|
}
|
|
285
294
|
};
|
|
286
295
|
}
|
|
296
|
+
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
297
|
+
var FLY_SOCKET = "/.fly/api";
|
|
298
|
+
var NoWorkloadIdentityError = class extends Error {
|
|
299
|
+
constructor(message) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.name = "NoWorkloadIdentityError";
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var TicketExchangeError = class extends Error {
|
|
305
|
+
constructor(message, code, status) {
|
|
306
|
+
super(message);
|
|
307
|
+
this.code = code;
|
|
308
|
+
this.status = status;
|
|
309
|
+
this.name = "TicketExchangeError";
|
|
310
|
+
}
|
|
311
|
+
code;
|
|
312
|
+
status;
|
|
313
|
+
};
|
|
314
|
+
var TicketUnavailableError = class extends Error {
|
|
315
|
+
constructor(message) {
|
|
316
|
+
super(message);
|
|
317
|
+
this.name = "TicketUnavailableError";
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
function flyOidcOverSocket(aud) {
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const body = JSON.stringify({ aud });
|
|
323
|
+
const req = request(
|
|
324
|
+
{ socketPath: FLY_SOCKET, path: "/v1/tokens/oidc", method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } },
|
|
325
|
+
(res) => {
|
|
326
|
+
let data = "";
|
|
327
|
+
res.setEncoding("utf8");
|
|
328
|
+
res.on("data", (c) => data += c);
|
|
329
|
+
res.on("end", () => {
|
|
330
|
+
if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());
|
|
331
|
+
else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
req.on("error", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));
|
|
336
|
+
req.end(body);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function jwtExp(token) {
|
|
340
|
+
try {
|
|
341
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
342
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function readTicketResponse(res, issuer, target, now) {
|
|
348
|
+
let body = {};
|
|
349
|
+
try {
|
|
350
|
+
body = await res.json();
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);
|
|
354
|
+
if (!res.ok || !body.access_token) {
|
|
355
|
+
const desc = body.error_description ?? "";
|
|
356
|
+
const code = desc.includes(":") ? desc.slice(0, desc.indexOf(":")).trim() : body.error ?? "exchange_refused";
|
|
357
|
+
throw new TicketExchangeError(`BID refused the ticket for ${target}: ${desc || body.error || res.status}`, code, res.status);
|
|
358
|
+
}
|
|
359
|
+
const nowSec = Math.floor(now() / 1e3);
|
|
360
|
+
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
361
|
+
return { token: body.access_token, exp };
|
|
362
|
+
}
|
|
363
|
+
function createTicketClient(options = {}) {
|
|
364
|
+
const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
365
|
+
const renewBeforeSec = options.renewBeforeSec ?? 30;
|
|
366
|
+
const env = options.env ?? process.env;
|
|
367
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
368
|
+
const now = options.now ?? (() => Date.now());
|
|
369
|
+
const flyOidc = options.flyOidc ?? flyOidcOverSocket;
|
|
370
|
+
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && existsSync(FLY_SOCKET));
|
|
371
|
+
const cache = /* @__PURE__ */ new Map();
|
|
372
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
373
|
+
let dpopKey = null;
|
|
374
|
+
const ownKey = () => dpopKey ??= createDpopKey();
|
|
375
|
+
async function platformToken() {
|
|
376
|
+
if (onFly()) return flyOidc(issuer);
|
|
377
|
+
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
378
|
+
const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
379
|
+
if (url && bearer) {
|
|
380
|
+
let res;
|
|
381
|
+
try {
|
|
382
|
+
res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });
|
|
383
|
+
} catch (e) {
|
|
384
|
+
throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
385
|
+
}
|
|
386
|
+
if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);
|
|
387
|
+
const { value } = await res.json();
|
|
388
|
+
if (!value) throw new TicketUnavailableError("GitHub OIDC endpoint returned no token");
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
throw new NoWorkloadIdentityError(
|
|
392
|
+
"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 (ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN \u2014 add `permissions: id-token: write`). A Mac session keeps using its session key."
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
async function exchange(audience, scope) {
|
|
396
|
+
const subject = await platformToken();
|
|
397
|
+
const form = new URLSearchParams({
|
|
398
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
399
|
+
subject_token: subject,
|
|
400
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
|
|
401
|
+
audience
|
|
402
|
+
});
|
|
403
|
+
if (scope) form.set("scope", scope);
|
|
404
|
+
let res;
|
|
405
|
+
try {
|
|
406
|
+
const tokenUrl = `${issuer}/oauth2/token`;
|
|
407
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
408
|
+
if (options.dpop) headers.dpop = await createDpopProof(await ownKey(), { method: "POST", url: tokenUrl, now });
|
|
409
|
+
res = await fetchImpl(tokenUrl, { method: "POST", headers, body: form });
|
|
410
|
+
} catch (e) {
|
|
411
|
+
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
412
|
+
}
|
|
413
|
+
return readTicketResponse(res, issuer, audience, now);
|
|
414
|
+
}
|
|
415
|
+
const client = {
|
|
416
|
+
async authHeaders({ audience, scope, method, url }) {
|
|
417
|
+
const ticket = await client.get({ audience, scope });
|
|
418
|
+
if (!options.dpop) return { authorization: `Bearer ${ticket}` };
|
|
419
|
+
return { authorization: `DPoP ${ticket}`, dpop: await createDpopProof(await ownKey(), { method, url, ticket, now }) };
|
|
420
|
+
},
|
|
421
|
+
async get({ audience, scope }) {
|
|
422
|
+
const key = `${audience} ${scope ?? ""}`;
|
|
423
|
+
const hit = cache.get(key);
|
|
424
|
+
if (hit && hit.exp - renewBeforeSec > now() / 1e3) return hit.token;
|
|
425
|
+
const pending = inFlight.get(key);
|
|
426
|
+
if (pending) return pending;
|
|
427
|
+
const p = exchange(audience, scope).then((t) => {
|
|
428
|
+
cache.set(key, t);
|
|
429
|
+
return t.token;
|
|
430
|
+
}).finally(() => inFlight.delete(key));
|
|
431
|
+
inFlight.set(key, p);
|
|
432
|
+
return p;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
return client;
|
|
436
|
+
}
|
|
437
|
+
var shared = null;
|
|
438
|
+
function fetchTicket(opts) {
|
|
439
|
+
shared ??= createTicketClient();
|
|
440
|
+
return shared.get(opts);
|
|
441
|
+
}
|
|
442
|
+
async function exchangeUserToken(opts) {
|
|
443
|
+
const issuer = (opts.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
444
|
+
const target = opts.resource ?? opts.audience;
|
|
445
|
+
if (!target) throw new TicketExchangeError("exchangeUserToken needs a receiver: resource (its URL) or audience (BID's name for it)", "malformed_request", 0);
|
|
446
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
447
|
+
const now = opts.now ?? (() => Date.now());
|
|
448
|
+
const form = new URLSearchParams({
|
|
449
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
450
|
+
subject_token: opts.subjectToken,
|
|
451
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:access_token"
|
|
452
|
+
});
|
|
453
|
+
if (opts.resource) form.set("resource", opts.resource);
|
|
454
|
+
if (opts.audience) form.set("audience", opts.audience);
|
|
455
|
+
if (opts.scope) form.set("scope", opts.scope);
|
|
456
|
+
const tokenUrl = `${issuer}/oauth2/token`;
|
|
457
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
458
|
+
if (opts.clientSecret) {
|
|
459
|
+
headers.authorization = `Basic ${btoa(`${encodeURIComponent(opts.clientId)}:${encodeURIComponent(opts.clientSecret)}`)}`;
|
|
460
|
+
} else {
|
|
461
|
+
form.set("client_id", opts.clientId);
|
|
462
|
+
}
|
|
463
|
+
if (opts.dpopKey) headers.dpop = await createDpopProof(opts.dpopKey, { method: "POST", url: tokenUrl, now });
|
|
464
|
+
let res;
|
|
465
|
+
try {
|
|
466
|
+
res = await fetchImpl(tokenUrl, { method: "POST", headers, body: form });
|
|
467
|
+
} catch (e) {
|
|
468
|
+
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
469
|
+
}
|
|
470
|
+
return readTicketResponse(res, issuer, target, now);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/client.ts
|
|
287
474
|
var SsoError = class extends Error {
|
|
288
475
|
constructor(message) {
|
|
289
476
|
super(message);
|
|
@@ -368,6 +555,7 @@ var INVITATION_OUTCOMES = /* @__PURE__ */ new Set([
|
|
|
368
555
|
var MIGRATION_STATES = /* @__PURE__ */ new Set(["ready", "invited", "expired", "not_invited"]);
|
|
369
556
|
var BACKCHANNEL_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
370
557
|
var MAX_INVITATIONS = 500;
|
|
558
|
+
var FLY_CLIENT_ASSERTION_TYPE = "urn:broberg:params:oauth:client-assertion-type:fly-oidc";
|
|
371
559
|
function excerptForLog(raw, clientSecret) {
|
|
372
560
|
let text = raw.replace(/\s+/g, " ").trim();
|
|
373
561
|
if (clientSecret) text = text.split(clientSecret).join("[redacted client_secret]");
|
|
@@ -403,6 +591,18 @@ var ALLOWED_ALGS = [
|
|
|
403
591
|
function createSsoClient(config, options = {}) {
|
|
404
592
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
405
593
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
594
|
+
const flyOidc = options.flyOidc ?? flyOidcOverSocket;
|
|
595
|
+
async function tokenEndpointAuth(tokenEndpoint) {
|
|
596
|
+
if (config.clientAuth !== "fly") return config.clientSecret ? { client_secret: config.clientSecret } : {};
|
|
597
|
+
let assertion;
|
|
598
|
+
try {
|
|
599
|
+
assertion = await flyOidc(tokenEndpoint);
|
|
600
|
+
} catch (e) {
|
|
601
|
+
throw new SsoError(`fly client assertion unavailable: could not get a Fly OIDC token for ${tokenEndpoint} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
602
|
+
}
|
|
603
|
+
if (!assertion) throw new SsoError(`fly client assertion unavailable: Fly returned an empty token for ${tokenEndpoint}`);
|
|
604
|
+
return { client_assertion_type: FLY_CLIENT_ASSERTION_TYPE, client_assertion: assertion };
|
|
605
|
+
}
|
|
406
606
|
async function appPost(path, bearer, payload, what) {
|
|
407
607
|
const url = new URL(path, config.issuer).toString();
|
|
408
608
|
const ctrl = new AbortController();
|
|
@@ -686,7 +886,7 @@ function createSsoClient(config, options = {}) {
|
|
|
686
886
|
// ALWAYS sent, secret or not. See SsoConfig.clientSecret: the two
|
|
687
887
|
// prove different things, so a confidential client keeps PKCE.
|
|
688
888
|
code_verifier: codeVerifier,
|
|
689
|
-
...
|
|
889
|
+
...await tokenEndpointAuth(token_endpoint)
|
|
690
890
|
})
|
|
691
891
|
});
|
|
692
892
|
const raw = await res.text();
|
|
@@ -745,10 +945,25 @@ function createSsoClient(config, options = {}) {
|
|
|
745
945
|
return res.ok ? { ok: true, body: json } : { ok: false, status: res.status, body: json };
|
|
746
946
|
}
|
|
747
947
|
const toError = (r) => r.status === 0 ? { kind: "unavailable", message: `BID did not answer: ${String(r.body.message ?? "")}` } : embeddedError(r.status, r.body);
|
|
948
|
+
function readPasskeySetup(v) {
|
|
949
|
+
if (typeof v !== "object" || v === null) return void 0;
|
|
950
|
+
const o = v;
|
|
951
|
+
if (typeof o.setup_id !== "string" || !o.setup_id || typeof o.frame_url !== "string") return void 0;
|
|
952
|
+
let frame;
|
|
953
|
+
try {
|
|
954
|
+
frame = new URL(o.frame_url, config.issuer);
|
|
955
|
+
} catch {
|
|
956
|
+
return void 0;
|
|
957
|
+
}
|
|
958
|
+
if (frame.origin !== new URL(config.issuer).origin) return void 0;
|
|
959
|
+
return { setupId: o.setup_id, frameUrl: frame.toString(), expiresIn: typeof o.expires_in === "number" ? o.expires_in : 600 };
|
|
960
|
+
}
|
|
748
961
|
async function embeddedNext(r, flow) {
|
|
749
962
|
if (!r.ok) return { kind: "error", error: toError(r) };
|
|
750
963
|
if (r.body.next === "done" && typeof r.body.code === "string") {
|
|
751
|
-
|
|
964
|
+
const result = await exchangeCode(r.body.code, flow.codeVerifier, flow.nonce);
|
|
965
|
+
const passkeySetup = readPasskeySetup(r.body.passkey_setup);
|
|
966
|
+
return passkeySetup ? { kind: "done", result, passkeySetup } : { kind: "done", result };
|
|
752
967
|
}
|
|
753
968
|
if (r.body.next === "second_factor") {
|
|
754
969
|
const methods = (Array.isArray(r.body.methods) ? r.body.methods : []).filter((m) => m === "totp" || m === "sms");
|
|
@@ -787,7 +1002,7 @@ function createSsoClient(config, options = {}) {
|
|
|
787
1002
|
return embeddedNext(await embeddedCall("POST", "/second-factor", { flow_id: flow.flowId, method, code }), flow);
|
|
788
1003
|
},
|
|
789
1004
|
async terms(lang) {
|
|
790
|
-
const r = await embeddedCall("GET", `/terms
|
|
1005
|
+
const r = await embeddedCall("GET", `/terms?${new URLSearchParams({ lang, client_id: config.clientId })}`);
|
|
791
1006
|
const b = r.body;
|
|
792
1007
|
if (!r.ok || typeof b.version !== "string" || typeof b.text !== "string") {
|
|
793
1008
|
throw new SsoError(`BID did not answer the signup terms (${r.ok ? "no version/text" : r.status})`);
|
|
@@ -851,7 +1066,7 @@ function createSsoClient(config, options = {}) {
|
|
|
851
1066
|
grant_type: "refresh_token",
|
|
852
1067
|
refresh_token: refreshToken,
|
|
853
1068
|
client_id: config.clientId,
|
|
854
|
-
...
|
|
1069
|
+
...await tokenEndpointAuth(token_endpoint)
|
|
855
1070
|
})
|
|
856
1071
|
});
|
|
857
1072
|
const raw = await res.text();
|
|
@@ -957,6 +1172,84 @@ function createSsoClient(config, options = {}) {
|
|
|
957
1172
|
};
|
|
958
1173
|
}
|
|
959
1174
|
|
|
1175
|
+
// src/dpop.ts
|
|
1176
|
+
async function createDpopKey() {
|
|
1177
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: false });
|
|
1178
|
+
const jwk = await exportJWK(publicKey);
|
|
1179
|
+
const publicJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
1180
|
+
return { privateKey, publicJwk, jkt: await calculateJwkThumbprint(publicJwk) };
|
|
1181
|
+
}
|
|
1182
|
+
var enc2 = new TextEncoder();
|
|
1183
|
+
var b64url3 = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1184
|
+
async function accessTokenHash(ticket) {
|
|
1185
|
+
return b64url3(await crypto.subtle.digest("SHA-256", enc2.encode(ticket)));
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeHtu(url) {
|
|
1188
|
+
const u = new URL(url);
|
|
1189
|
+
return `${u.protocol}//${u.host}${u.pathname}`;
|
|
1190
|
+
}
|
|
1191
|
+
async function createDpopProof(key, opts) {
|
|
1192
|
+
const payload = {
|
|
1193
|
+
jti: crypto.randomUUID(),
|
|
1194
|
+
htm: opts.method.toUpperCase(),
|
|
1195
|
+
htu: normalizeHtu(opts.url),
|
|
1196
|
+
iat: Math.floor((opts.now?.() ?? Date.now()) / 1e3)
|
|
1197
|
+
};
|
|
1198
|
+
if (opts.ticket !== void 0) payload.ath = await accessTokenHash(opts.ticket);
|
|
1199
|
+
return new SignJWT(payload).setProtectedHeader({ typ: "dpop+jwt", alg: "ES256", jwk: key.publicJwk }).sign(key.privateKey);
|
|
1200
|
+
}
|
|
1201
|
+
function createMemoryReplayStore(now = () => Date.now()) {
|
|
1202
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1203
|
+
return {
|
|
1204
|
+
async seen(key, ttlMs) {
|
|
1205
|
+
const t = now();
|
|
1206
|
+
for (const [k, until] of entries) if (until <= t) entries.delete(k);
|
|
1207
|
+
if (entries.has(key)) return true;
|
|
1208
|
+
entries.set(key, t + ttlMs);
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
var ASYMMETRIC = /* @__PURE__ */ new Set(["ES256", "ES384", "ES512", "EdDSA", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"]);
|
|
1214
|
+
async function verifyDpopProof(proof, o) {
|
|
1215
|
+
let header;
|
|
1216
|
+
try {
|
|
1217
|
+
header = decodeProtectedHeader(proof);
|
|
1218
|
+
} catch {
|
|
1219
|
+
throw new SsoError("DPoP proof is not a JWS");
|
|
1220
|
+
}
|
|
1221
|
+
if (header.typ !== "dpop+jwt") throw new SsoError(`DPoP proof has typ ${JSON.stringify(header.typ)}, not "dpop+jwt"`);
|
|
1222
|
+
if (!header.alg || !ASYMMETRIC.has(header.alg)) throw new SsoError(`DPoP proof alg ${JSON.stringify(header.alg)} is not an accepted asymmetric algorithm`);
|
|
1223
|
+
const jwk = header.jwk;
|
|
1224
|
+
if (!jwk || typeof jwk !== "object") throw new SsoError("DPoP proof carries no jwk");
|
|
1225
|
+
if ("d" in jwk) throw new SsoError("DPoP proof jwk contains a private key");
|
|
1226
|
+
let payload;
|
|
1227
|
+
try {
|
|
1228
|
+
const key = await importJWK(jwk, header.alg);
|
|
1229
|
+
({ payload } = await jwtVerify(proof, key, { typ: "dpop+jwt", algorithms: [header.alg] }));
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
throw new SsoError(`DPoP proof signature rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
1232
|
+
}
|
|
1233
|
+
if (await calculateJwkThumbprint(jwk) !== o.jkt) throw new SsoError("DPoP proof key does not match the ticket's cnf.jkt");
|
|
1234
|
+
if (payload.htm !== o.method.toUpperCase()) throw new SsoError(`DPoP proof is for ${JSON.stringify(payload.htm)}, the request is ${o.method.toUpperCase()}`);
|
|
1235
|
+
let htu;
|
|
1236
|
+
try {
|
|
1237
|
+
htu = normalizeHtu(String(payload.htu));
|
|
1238
|
+
} catch {
|
|
1239
|
+
throw new SsoError("DPoP proof htu is not a URL");
|
|
1240
|
+
}
|
|
1241
|
+
if (htu !== normalizeHtu(o.url)) throw new SsoError(`DPoP proof is for ${htu}, the request is ${normalizeHtu(o.url)}`);
|
|
1242
|
+
const nowSec = Math.floor(o.now() / 1e3);
|
|
1243
|
+
if (typeof payload.iat !== "number" || Math.abs(nowSec - payload.iat) > o.iatWindowSec) {
|
|
1244
|
+
throw new SsoError(`DPoP proof iat is outside \xB1${o.iatWindowSec}s`);
|
|
1245
|
+
}
|
|
1246
|
+
if (payload.ath !== await accessTokenHash(o.ticket)) throw new SsoError("DPoP proof ath does not match this ticket");
|
|
1247
|
+
if (typeof payload.jti !== "string" || payload.jti === "") throw new SsoError("DPoP proof has no jti");
|
|
1248
|
+
if (await o.replay.seen(`${o.jkt}:${payload.jti}`, o.iatWindowSec * 2 * 1e3)) {
|
|
1249
|
+
throw new SsoError("DPoP proof was already used (jti replay)");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
960
1253
|
// src/tokens.ts
|
|
961
1254
|
var warned = false;
|
|
962
1255
|
function memoryTokenStore(options = {}) {
|
|
@@ -1004,6 +1297,6 @@ function newSid() {
|
|
|
1004
1297
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1005
1298
|
}
|
|
1006
1299
|
|
|
1007
|
-
export { ALLOWED_ALGS, AVATAR_TYPES, DEFAULT_SESSION_MAX_AGE, EMBEDDED_FLOW_MAX_AGE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoIntrospectionUnavailableError, SsoReauthError, SsoTokenInactiveError, cookieHeader, createJwksCache, createSsoClient, expiresAtFrom, loadSsoConfig, mediaType, memoryTokenStore, newSid, openEmbeddedFlow, readCookie, sealEmbeddedFlow, signSession, signValue, verifySession, verifyValue };
|
|
1008
|
-
//# sourceMappingURL=chunk-
|
|
1009
|
-
//# sourceMappingURL=chunk-
|
|
1300
|
+
export { ALLOWED_ALGS, AVATAR_TYPES, DEFAULT_ISSUER, DEFAULT_SESSION_MAX_AGE, EMBEDDED_FLOW_MAX_AGE, FLY_CLIENT_ASSERTION_TYPE, JwksError, JwksUnavailableError, JwksUnknownKeyError, MAX_AVATAR_BYTES, MAX_INVITATIONS, NoWorkloadIdentityError, SsoAppApiError, SsoAvatarRejectedError, SsoConfigError, SsoError, SsoInsufficientScopeError, SsoIntrospectionUnavailableError, SsoReauthError, SsoTokenInactiveError, TicketExchangeError, TicketUnavailableError, accessTokenHash, cookieHeader, createDpopKey, createDpopProof, createJwksCache, createMemoryReplayStore, createSsoClient, createTicketClient, exchangeUserToken, expiresAtFrom, fetchTicket, loadSsoConfig, mediaType, memoryTokenStore, newSid, normalizeHtu, openEmbeddedFlow, readCookie, sealEmbeddedFlow, signSession, signValue, verifyDpopProof, verifySession, verifyValue };
|
|
1301
|
+
//# sourceMappingURL=chunk-UDUPP35Z.js.map
|
|
1302
|
+
//# sourceMappingURL=chunk-UDUPP35Z.js.map
|