@broberg/sso 0.18.2 → 0.19.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 +25 -0
- package/dist/{chunk-FWFXRNDG.cjs → chunk-MZE4TPFU.cjs} +258 -138
- package/dist/chunk-MZE4TPFU.cjs.map +1 -0
- package/dist/{chunk-RWTUJPV3.js → chunk-PQTT74BA.js} +256 -139
- package/dist/chunk-PQTT74BA.js.map +1 -0
- package/dist/hono.cjs +37 -37
- package/dist/hono.d.cts +1 -1
- package/dist/hono.d.ts +1 -1
- package/dist/hono.js +1 -1
- package/dist/index.cjs +78 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{session-eya6Bon5.d.cts → session-qvhMg3ID.d.cts} +106 -1
- package/dist/{session-eya6Bon5.d.ts → session-qvhMg3ID.d.ts} +106 -1
- package/package.json +1 -1
- package/dist/chunk-FWFXRNDG.cjs.map +0 -1
- package/dist/chunk-RWTUJPV3.js.map +0 -1
|
@@ -19,7 +19,7 @@ function loadSsoConfig(env = process.env) {
|
|
|
19
19
|
const rawIssuer = required(
|
|
20
20
|
env,
|
|
21
21
|
"BID_ISSUER",
|
|
22
|
-
"It is
|
|
22
|
+
"It is BID's bare origin, e.g. https://id.broberg.ai"
|
|
23
23
|
);
|
|
24
24
|
let issuer;
|
|
25
25
|
try {
|
|
@@ -57,7 +57,7 @@ function loadSsoConfig(env = process.env) {
|
|
|
57
57
|
}
|
|
58
58
|
return {
|
|
59
59
|
issuer,
|
|
60
|
-
clientId: required(env, "SSO_CLIENT_ID", "The id this app is registered under in
|
|
60
|
+
clientId: required(env, "SSO_CLIENT_ID", "The id this app is registered under in BID."),
|
|
61
61
|
redirectUri: required(
|
|
62
62
|
env,
|
|
63
63
|
"SSO_REDIRECT_URI",
|
|
@@ -71,6 +71,127 @@ function loadSsoConfig(env = process.env) {
|
|
|
71
71
|
clientSecret: env.SSO_CLIENT_SECRET?.trim() || void 0
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
// src/session.ts
|
|
76
|
+
var enc = new TextEncoder();
|
|
77
|
+
var b64url = (bytes) => {
|
|
78
|
+
let s = "";
|
|
79
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
80
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
81
|
+
};
|
|
82
|
+
var fromB64url = (s) => {
|
|
83
|
+
const pad = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
84
|
+
const bin = atob(pad + "=".repeat((4 - pad.length % 4) % 4));
|
|
85
|
+
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
86
|
+
};
|
|
87
|
+
async function hmacKey(secret) {
|
|
88
|
+
return crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
89
|
+
"sign",
|
|
90
|
+
"verify"
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
async function signValue(value, secret, options = {}) {
|
|
94
|
+
const payload = b64url(enc.encode(value));
|
|
95
|
+
const body = options.maxAgeSeconds === void 0 ? payload : `t${Math.floor((options.now ?? Date.now)() / 1e3)}~${payload}`;
|
|
96
|
+
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey(secret), enc.encode(body)));
|
|
97
|
+
return `${body}.${b64url(sig)}`;
|
|
98
|
+
}
|
|
99
|
+
var STAMPED = /^t(\d+)~(.*)$/s;
|
|
100
|
+
async function verifyValue(token, secret, options = {}) {
|
|
101
|
+
if (!token) return null;
|
|
102
|
+
const dot = token.lastIndexOf(".");
|
|
103
|
+
if (dot <= 0) return null;
|
|
104
|
+
const body = token.slice(0, dot);
|
|
105
|
+
try {
|
|
106
|
+
const ok = await crypto.subtle.verify(
|
|
107
|
+
"HMAC",
|
|
108
|
+
await hmacKey(secret),
|
|
109
|
+
fromB64url(token.slice(dot + 1)),
|
|
110
|
+
enc.encode(body)
|
|
111
|
+
);
|
|
112
|
+
if (!ok) return null;
|
|
113
|
+
const stamped = STAMPED.exec(body);
|
|
114
|
+
if (options.maxAgeSeconds !== void 0) {
|
|
115
|
+
if (!stamped) return null;
|
|
116
|
+
const issuedAt = Number(stamped[1]);
|
|
117
|
+
if ((options.now ?? Date.now)() / 1e3 - issuedAt > options.maxAgeSeconds) return null;
|
|
118
|
+
}
|
|
119
|
+
return new TextDecoder().decode(fromB64url(stamped?.[2] ?? body));
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function signSession(payload, secret) {
|
|
125
|
+
return signValue(JSON.stringify(payload), secret);
|
|
126
|
+
}
|
|
127
|
+
async function verifySession(token, secret, now = Date.now) {
|
|
128
|
+
const body = await verifyValue(token, secret);
|
|
129
|
+
if (body === null) return null;
|
|
130
|
+
try {
|
|
131
|
+
const payload = JSON.parse(body);
|
|
132
|
+
if (typeof payload.sub !== "string" || payload.sub === "") return null;
|
|
133
|
+
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= now()) return null;
|
|
134
|
+
return payload;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function cookieHeader(name, value, opts) {
|
|
140
|
+
const parts = [
|
|
141
|
+
`${name}=${value}`,
|
|
142
|
+
`Path=${opts.path ?? "/"}`,
|
|
143
|
+
`Max-Age=${opts.maxAge}`,
|
|
144
|
+
"HttpOnly",
|
|
145
|
+
// Lax, NOT Strict, and this is load-bearing: the callback from BrobergID
|
|
146
|
+
// is a top-level GET navigation from another site. Strict withholds the
|
|
147
|
+
// cookie on exactly that navigation, so the login transaction cookie would
|
|
148
|
+
// be missing when it is needed and every sign-in would fail with "state
|
|
149
|
+
// does not match" — a message that sends you looking in the wrong place.
|
|
150
|
+
`SameSite=${opts.sameSite ?? "Lax"}`
|
|
151
|
+
];
|
|
152
|
+
if (opts.secure) parts.push("Secure");
|
|
153
|
+
return parts.join("; ");
|
|
154
|
+
}
|
|
155
|
+
function readCookie(header, name) {
|
|
156
|
+
if (!header) return void 0;
|
|
157
|
+
for (const part of header.split(";")) {
|
|
158
|
+
const eq = part.indexOf("=");
|
|
159
|
+
if (eq === -1) continue;
|
|
160
|
+
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/embedded.ts
|
|
166
|
+
var EMBEDDED_FLOW_MAX_AGE = 600;
|
|
167
|
+
function sealEmbeddedFlow(flow, secret, options = {}) {
|
|
168
|
+
return signValue(JSON.stringify(flow), secret, { maxAgeSeconds: EMBEDDED_FLOW_MAX_AGE, ...options });
|
|
169
|
+
}
|
|
170
|
+
async function openEmbeddedFlow(raw, secret, options = {}) {
|
|
171
|
+
if (!raw) return { ok: false, reason: "absent" };
|
|
172
|
+
if (await verifyValue(raw, secret) === null) return { ok: false, reason: "unreadable" };
|
|
173
|
+
const fresh = await verifyValue(raw, secret, { maxAgeSeconds: EMBEDDED_FLOW_MAX_AGE, ...options });
|
|
174
|
+
if (fresh === null) return { ok: false, reason: "expired" };
|
|
175
|
+
try {
|
|
176
|
+
const f = JSON.parse(fresh);
|
|
177
|
+
if (typeof f.flowId !== "string" || typeof f.codeVerifier !== "string" || typeof f.nonce !== "string" || typeof f.email !== "string") return { ok: false, reason: "unreadable" };
|
|
178
|
+
return { ok: true, flow: f };
|
|
179
|
+
} catch {
|
|
180
|
+
return { ok: false, reason: "unreadable" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function embeddedError(status, body) {
|
|
184
|
+
const code = typeof body.error === "string" ? body.error : "";
|
|
185
|
+
const num = (v) => typeof v === "number" ? v : void 0;
|
|
186
|
+
if (status === 400 && code === "invalid_code") return { kind: "invalid_code", ...num(body.attempts_left) !== void 0 ? { attemptsLeft: num(body.attempts_left) } : {} };
|
|
187
|
+
if (status === 400 && code === "expired") return { kind: "expired" };
|
|
188
|
+
if (status === 400 && code === "terms_outdated") return { kind: "terms_outdated", ...typeof body.version === "string" ? { version: body.version } : {} };
|
|
189
|
+
if (status === 400) return { kind: "invalid_request" };
|
|
190
|
+
if (status === 403 && code === "signup_not_enabled") return { kind: "signup_not_enabled" };
|
|
191
|
+
if (status === 403 || status === 404 || status === 405) return { kind: "not_embedded" };
|
|
192
|
+
if (status === 429) return { kind: code === "locked" ? "locked" : "too_many_requests", ...num(body.retry_after) !== void 0 ? { retryAfter: num(body.retry_after) } : {} };
|
|
193
|
+
return { kind: "unavailable", message: `BID answered ${status}${code ? ` (${code})` : ""}` };
|
|
194
|
+
}
|
|
74
195
|
var JwksError = class extends Error {
|
|
75
196
|
constructor(message) {
|
|
76
197
|
super(message);
|
|
@@ -156,7 +277,7 @@ function createJwksCache(options) {
|
|
|
156
277
|
}
|
|
157
278
|
if (!jwk) {
|
|
158
279
|
throw new JwksUnknownKeyError(
|
|
159
|
-
`
|
|
280
|
+
`BID does not publish a signing key with kid ${kid}. The token was not signed by this issuer.`
|
|
160
281
|
);
|
|
161
282
|
}
|
|
162
283
|
return importJWK(jwk, alg);
|
|
@@ -224,15 +345,15 @@ var AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
|
224
345
|
function mediaType(contentType) {
|
|
225
346
|
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
226
347
|
}
|
|
227
|
-
var
|
|
348
|
+
var b64url2 = (bytes) => {
|
|
228
349
|
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
229
350
|
let s = "";
|
|
230
351
|
for (const b of view) s += String.fromCharCode(b);
|
|
231
352
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
232
353
|
};
|
|
233
|
-
var randomToken = (bytes = 32) =>
|
|
354
|
+
var randomToken = (bytes = 32) => b64url2(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
234
355
|
async function challengeFor(verifier) {
|
|
235
|
-
return
|
|
356
|
+
return b64url2(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
|
|
236
357
|
}
|
|
237
358
|
var ADDRESS_OWNERSHIP = /* @__PURE__ */ new Set(["verified", "unverified", "not_on_account"]);
|
|
238
359
|
var INVITATION_OUTCOMES = /* @__PURE__ */ new Set([
|
|
@@ -552,9 +673,133 @@ function createSsoClient(config, options = {}) {
|
|
|
552
673
|
if (options2.cacheMs && options2.cacheMs > 0) introspectCache.set(key, { result, until: Math.min(now() + options2.cacheMs, exp * 1e3) });
|
|
553
674
|
return result;
|
|
554
675
|
}
|
|
676
|
+
async function exchangeCode(code, codeVerifier, nonce) {
|
|
677
|
+
const { token_endpoint } = await discovery();
|
|
678
|
+
const res = await fetchImpl(token_endpoint, {
|
|
679
|
+
method: "POST",
|
|
680
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
681
|
+
body: new URLSearchParams({
|
|
682
|
+
grant_type: "authorization_code",
|
|
683
|
+
code,
|
|
684
|
+
redirect_uri: config.redirectUri,
|
|
685
|
+
client_id: config.clientId,
|
|
686
|
+
// ALWAYS sent, secret or not. See SsoConfig.clientSecret: the two
|
|
687
|
+
// prove different things, so a confidential client keeps PKCE.
|
|
688
|
+
code_verifier: codeVerifier,
|
|
689
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
690
|
+
})
|
|
691
|
+
});
|
|
692
|
+
const raw = await res.text();
|
|
693
|
+
let body;
|
|
694
|
+
try {
|
|
695
|
+
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
696
|
+
} catch {
|
|
697
|
+
throw new SsoError(
|
|
698
|
+
`token exchange failed (${res.status}): the response body is not JSON${res.headers.get("content-type") ? ` (content-type: ${res.headers.get("content-type")})` : ""} \u2014 ${excerptForLog(raw, config.clientSecret)}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
if (!res.ok || !body.id_token) {
|
|
702
|
+
throw new SsoError(
|
|
703
|
+
`token exchange failed (${res.status}): ${body.error ?? "no id_token in response"}` + (body.error_description ? ` \u2014 ${body.error_description}` : "") + invalidClientHint(body.error, config.clientSecret !== void 0)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const claims = await verifyIdToken(body.id_token, { nonce });
|
|
707
|
+
const merged = body.access_token ? await withUserInfo(claims, body.access_token) : { claims, unverifiedClaims: [] };
|
|
708
|
+
return {
|
|
709
|
+
claims: merged.claims,
|
|
710
|
+
unverifiedClaims: merged.unverifiedClaims,
|
|
711
|
+
idToken: body.id_token,
|
|
712
|
+
...body.access_token ? { accessToken: body.access_token } : {},
|
|
713
|
+
...body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
714
|
+
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async function embeddedCall(method, path, body) {
|
|
718
|
+
if (!config.clientSecret) throw new SsoError("the embedded login runs from the server as a confidential client \u2014 set clientSecret (SSO_CLIENT_SECRET)");
|
|
719
|
+
const url = new URL(`/embedded${path}`, config.issuer).toString();
|
|
720
|
+
let res;
|
|
721
|
+
try {
|
|
722
|
+
res = await fetchImpl(url, {
|
|
723
|
+
method,
|
|
724
|
+
headers: {
|
|
725
|
+
authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`,
|
|
726
|
+
accept: "application/json",
|
|
727
|
+
...body ? { "content-type": "application/json" } : {}
|
|
728
|
+
},
|
|
729
|
+
...body ? { body: JSON.stringify({ client_id: config.clientId, ...body }) } : {}
|
|
730
|
+
});
|
|
731
|
+
} catch (e) {
|
|
732
|
+
return { ok: false, status: 0, body: { error: "network", message: e instanceof Error ? e.message : String(e) } };
|
|
733
|
+
}
|
|
734
|
+
const raw = await res.text();
|
|
735
|
+
let json = {};
|
|
736
|
+
try {
|
|
737
|
+
json = raw.trim() ? JSON.parse(raw) : {};
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
if (res.status === 401) {
|
|
741
|
+
throw new SsoError(`BID refused this app's client credentials on /embedded${path} (401 ${typeof json.error === "string" ? json.error : "invalid_client"}) \u2014 check SSO_CLIENT_ID / SSO_CLIENT_SECRET`);
|
|
742
|
+
}
|
|
743
|
+
return res.ok ? { ok: true, body: json } : { ok: false, status: res.status, body: json };
|
|
744
|
+
}
|
|
745
|
+
const toError = (r) => r.status === 0 ? { kind: "unavailable", message: `BID did not answer: ${String(r.body.message ?? "")}` } : embeddedError(r.status, r.body);
|
|
746
|
+
async function embeddedNext(r, flow) {
|
|
747
|
+
if (!r.ok) return { kind: "error", error: toError(r) };
|
|
748
|
+
if (r.body.next === "done" && typeof r.body.code === "string") {
|
|
749
|
+
return { kind: "done", result: await exchangeCode(r.body.code, flow.codeVerifier, flow.nonce) };
|
|
750
|
+
}
|
|
751
|
+
if (r.body.next === "second_factor") {
|
|
752
|
+
const methods = (Array.isArray(r.body.methods) ? r.body.methods : []).filter((m) => m === "totp" || m === "sms");
|
|
753
|
+
return { kind: "second_factor", methods };
|
|
754
|
+
}
|
|
755
|
+
return { kind: "error", error: { kind: "unavailable", message: "BID's answer names no next step" } };
|
|
756
|
+
}
|
|
757
|
+
const embedded = {
|
|
758
|
+
async start(input) {
|
|
759
|
+
const codeVerifier = randomToken();
|
|
760
|
+
const nonce = randomToken(16);
|
|
761
|
+
const r = await embeddedCall("POST", "/start", {
|
|
762
|
+
email: input.email,
|
|
763
|
+
code_challenge: await challengeFor(codeVerifier),
|
|
764
|
+
code_challenge_method: "S256",
|
|
765
|
+
nonce,
|
|
766
|
+
...input.intent === "signup" ? { intent: "signup", name: input.name, terms_version: input.termsVersion } : {},
|
|
767
|
+
...input.endUserIp ? { end_user_ip: input.endUserIp } : {}
|
|
768
|
+
});
|
|
769
|
+
if (!r.ok) return { ok: false, error: toError(r) };
|
|
770
|
+
if (typeof r.body.flow_id !== "string") return { ok: false, error: { kind: "unavailable", message: "BID's answer has no flow_id" } };
|
|
771
|
+
return {
|
|
772
|
+
ok: true,
|
|
773
|
+
flow: { flowId: r.body.flow_id, codeVerifier, nonce, email: input.email },
|
|
774
|
+
expiresIn: typeof r.body.expires_in === "number" ? r.body.expires_in : 600
|
|
775
|
+
};
|
|
776
|
+
},
|
|
777
|
+
async verify(flow, code) {
|
|
778
|
+
return embeddedNext(await embeddedCall("POST", "/verify", { flow_id: flow.flowId, code }), flow);
|
|
779
|
+
},
|
|
780
|
+
async sendSms(flow) {
|
|
781
|
+
const r = await embeddedCall("POST", "/second-factor/sms", { flow_id: flow.flowId });
|
|
782
|
+
return r.ok ? { ok: true } : { ok: false, error: toError(r) };
|
|
783
|
+
},
|
|
784
|
+
async verifySecondFactor(flow, method, code) {
|
|
785
|
+
return embeddedNext(await embeddedCall("POST", "/second-factor", { flow_id: flow.flowId, method, code }), flow);
|
|
786
|
+
},
|
|
787
|
+
async terms(lang) {
|
|
788
|
+
const r = await embeddedCall("GET", `/terms?lang=${lang}`);
|
|
789
|
+
const b = r.body;
|
|
790
|
+
if (!r.ok || typeof b.version !== "string" || typeof b.text !== "string") {
|
|
791
|
+
throw new SsoError(`BID did not answer the signup terms (${r.ok ? "no version/text" : r.status})`);
|
|
792
|
+
}
|
|
793
|
+
const links = (Array.isArray(b.links) ? b.links : []).filter(
|
|
794
|
+
(l) => typeof l === "object" && l !== null && typeof l.label === "string" && typeof l.url === "string"
|
|
795
|
+
);
|
|
796
|
+
return { version: b.version, text: b.text, links: links.map((l) => ({ label: l.label, url: l.url })) };
|
|
797
|
+
}
|
|
798
|
+
};
|
|
555
799
|
return {
|
|
556
800
|
discovery,
|
|
557
801
|
introspectToken,
|
|
802
|
+
embedded,
|
|
558
803
|
get jwks() {
|
|
559
804
|
if (!jwksCache) throw new SsoError("the key cache is created on first verification");
|
|
560
805
|
return jwksCache;
|
|
@@ -582,7 +827,7 @@ function createSsoClient(config, options = {}) {
|
|
|
582
827
|
const error = params.get("error");
|
|
583
828
|
if (error) {
|
|
584
829
|
throw new SsoError(
|
|
585
|
-
`
|
|
830
|
+
`BID refused this login: ${error}` + (params.get("error_description") ? ` \u2014 ${params.get("error_description")}` : "")
|
|
586
831
|
);
|
|
587
832
|
}
|
|
588
833
|
const returned = params.get("state");
|
|
@@ -593,45 +838,7 @@ function createSsoClient(config, options = {}) {
|
|
|
593
838
|
}
|
|
594
839
|
const code = params.get("code");
|
|
595
840
|
if (!code) throw new SsoError("callback carried neither an error nor a code");
|
|
596
|
-
|
|
597
|
-
const res = await fetchImpl(token_endpoint, {
|
|
598
|
-
method: "POST",
|
|
599
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
600
|
-
body: new URLSearchParams({
|
|
601
|
-
grant_type: "authorization_code",
|
|
602
|
-
code,
|
|
603
|
-
redirect_uri: config.redirectUri,
|
|
604
|
-
client_id: config.clientId,
|
|
605
|
-
// ALWAYS sent, secret or not. See SsoConfig.clientSecret: the two
|
|
606
|
-
// prove different things, so a confidential client keeps PKCE.
|
|
607
|
-
code_verifier: codeVerifier,
|
|
608
|
-
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
609
|
-
})
|
|
610
|
-
});
|
|
611
|
-
const raw = await res.text();
|
|
612
|
-
let body;
|
|
613
|
-
try {
|
|
614
|
-
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
615
|
-
} catch {
|
|
616
|
-
throw new SsoError(
|
|
617
|
-
`token exchange failed (${res.status}): the response body is not JSON${res.headers.get("content-type") ? ` (content-type: ${res.headers.get("content-type")})` : ""} \u2014 ${excerptForLog(raw, config.clientSecret)}`
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
if (!res.ok || !body.id_token) {
|
|
621
|
-
throw new SsoError(
|
|
622
|
-
`token exchange failed (${res.status}): ${body.error ?? "no id_token in response"}` + (body.error_description ? ` \u2014 ${body.error_description}` : "") + invalidClientHint(body.error, config.clientSecret !== void 0)
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
const claims = await verifyIdToken(body.id_token, { nonce });
|
|
626
|
-
const merged = body.access_token ? await withUserInfo(claims, body.access_token) : { claims, unverifiedClaims: [] };
|
|
627
|
-
return {
|
|
628
|
-
claims: merged.claims,
|
|
629
|
-
unverifiedClaims: merged.unverifiedClaims,
|
|
630
|
-
idToken: body.id_token,
|
|
631
|
-
...body.access_token ? { accessToken: body.access_token } : {},
|
|
632
|
-
...body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
633
|
-
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
634
|
-
};
|
|
841
|
+
return exchangeCode(code, codeVerifier, nonce);
|
|
635
842
|
},
|
|
636
843
|
async refreshTokens(refreshToken) {
|
|
637
844
|
const { token_endpoint } = await discovery();
|
|
@@ -748,96 +955,6 @@ function createSsoClient(config, options = {}) {
|
|
|
748
955
|
};
|
|
749
956
|
}
|
|
750
957
|
|
|
751
|
-
// src/session.ts
|
|
752
|
-
var enc = new TextEncoder();
|
|
753
|
-
var b64url2 = (bytes) => {
|
|
754
|
-
let s = "";
|
|
755
|
-
for (const b of bytes) s += String.fromCharCode(b);
|
|
756
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
757
|
-
};
|
|
758
|
-
var fromB64url = (s) => {
|
|
759
|
-
const pad = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
760
|
-
const bin = atob(pad + "=".repeat((4 - pad.length % 4) % 4));
|
|
761
|
-
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
|
762
|
-
};
|
|
763
|
-
async function hmacKey(secret) {
|
|
764
|
-
return crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, [
|
|
765
|
-
"sign",
|
|
766
|
-
"verify"
|
|
767
|
-
]);
|
|
768
|
-
}
|
|
769
|
-
async function signValue(value, secret, options = {}) {
|
|
770
|
-
const payload = b64url2(enc.encode(value));
|
|
771
|
-
const body = options.maxAgeSeconds === void 0 ? payload : `t${Math.floor((options.now ?? Date.now)() / 1e3)}~${payload}`;
|
|
772
|
-
const sig = new Uint8Array(await crypto.subtle.sign("HMAC", await hmacKey(secret), enc.encode(body)));
|
|
773
|
-
return `${body}.${b64url2(sig)}`;
|
|
774
|
-
}
|
|
775
|
-
var STAMPED = /^t(\d+)~(.*)$/s;
|
|
776
|
-
async function verifyValue(token, secret, options = {}) {
|
|
777
|
-
if (!token) return null;
|
|
778
|
-
const dot = token.lastIndexOf(".");
|
|
779
|
-
if (dot <= 0) return null;
|
|
780
|
-
const body = token.slice(0, dot);
|
|
781
|
-
try {
|
|
782
|
-
const ok = await crypto.subtle.verify(
|
|
783
|
-
"HMAC",
|
|
784
|
-
await hmacKey(secret),
|
|
785
|
-
fromB64url(token.slice(dot + 1)),
|
|
786
|
-
enc.encode(body)
|
|
787
|
-
);
|
|
788
|
-
if (!ok) return null;
|
|
789
|
-
const stamped = STAMPED.exec(body);
|
|
790
|
-
if (options.maxAgeSeconds !== void 0) {
|
|
791
|
-
if (!stamped) return null;
|
|
792
|
-
const issuedAt = Number(stamped[1]);
|
|
793
|
-
if ((options.now ?? Date.now)() / 1e3 - issuedAt > options.maxAgeSeconds) return null;
|
|
794
|
-
}
|
|
795
|
-
return new TextDecoder().decode(fromB64url(stamped?.[2] ?? body));
|
|
796
|
-
} catch {
|
|
797
|
-
return null;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
async function signSession(payload, secret) {
|
|
801
|
-
return signValue(JSON.stringify(payload), secret);
|
|
802
|
-
}
|
|
803
|
-
async function verifySession(token, secret, now = Date.now) {
|
|
804
|
-
const body = await verifyValue(token, secret);
|
|
805
|
-
if (body === null) return null;
|
|
806
|
-
try {
|
|
807
|
-
const payload = JSON.parse(body);
|
|
808
|
-
if (typeof payload.sub !== "string" || payload.sub === "") return null;
|
|
809
|
-
if (typeof payload.exp !== "number" || payload.exp * 1e3 <= now()) return null;
|
|
810
|
-
return payload;
|
|
811
|
-
} catch {
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
function cookieHeader(name, value, opts) {
|
|
816
|
-
const parts = [
|
|
817
|
-
`${name}=${value}`,
|
|
818
|
-
`Path=${opts.path ?? "/"}`,
|
|
819
|
-
`Max-Age=${opts.maxAge}`,
|
|
820
|
-
"HttpOnly",
|
|
821
|
-
// Lax, NOT Strict, and this is load-bearing: the callback from BrobergID
|
|
822
|
-
// is a top-level GET navigation from another site. Strict withholds the
|
|
823
|
-
// cookie on exactly that navigation, so the login transaction cookie would
|
|
824
|
-
// be missing when it is needed and every sign-in would fail with "state
|
|
825
|
-
// does not match" — a message that sends you looking in the wrong place.
|
|
826
|
-
`SameSite=${opts.sameSite ?? "Lax"}`
|
|
827
|
-
];
|
|
828
|
-
if (opts.secure) parts.push("Secure");
|
|
829
|
-
return parts.join("; ");
|
|
830
|
-
}
|
|
831
|
-
function readCookie(header, name) {
|
|
832
|
-
if (!header) return void 0;
|
|
833
|
-
for (const part of header.split(";")) {
|
|
834
|
-
const eq = part.indexOf("=");
|
|
835
|
-
if (eq === -1) continue;
|
|
836
|
-
if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
|
|
837
|
-
}
|
|
838
|
-
return void 0;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
958
|
// src/tokens.ts
|
|
842
959
|
var warned = false;
|
|
843
960
|
function memoryTokenStore(options = {}) {
|
|
@@ -885,6 +1002,6 @@ function newSid() {
|
|
|
885
1002
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
886
1003
|
}
|
|
887
1004
|
|
|
888
|
-
export { ALLOWED_ALGS, AVATAR_TYPES, DEFAULT_SESSION_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, readCookie, signSession, signValue, verifySession, verifyValue };
|
|
889
|
-
//# sourceMappingURL=chunk-
|
|
890
|
-
//# sourceMappingURL=chunk-
|
|
1005
|
+
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 };
|
|
1006
|
+
//# sourceMappingURL=chunk-PQTT74BA.js.map
|
|
1007
|
+
//# sourceMappingURL=chunk-PQTT74BA.js.map
|