@broberg/sso 0.6.1 → 0.8.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 +26 -0
- package/dist/hono.cjs +14 -6
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +26 -0
- package/dist/hono.d.ts +26 -0
- package/dist/hono.js +14 -6
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +7 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/hono.d.ts
CHANGED
|
@@ -162,6 +162,18 @@ interface SsoClaims extends JWTPayload {
|
|
|
162
162
|
}
|
|
163
163
|
interface LoginResult {
|
|
164
164
|
claims: SsoClaims;
|
|
165
|
+
/**
|
|
166
|
+
* The claims in `claims` that came from the UNSIGNED userinfo response and
|
|
167
|
+
* not from the signed ID token (components-F084.51, since 0.8.0). Sorted; an
|
|
168
|
+
* empty list means everything in `claims` was signed (or userinfo was not
|
|
169
|
+
* reached). When both sources carry a claim, the signed token's value wins
|
|
170
|
+
* and the claim is not listed.
|
|
171
|
+
*
|
|
172
|
+
* On BID today this always contains "email" and "email_verified": the ID
|
|
173
|
+
* token does not carry them (measured by broberg-id, 20 Sep 2026). A decision
|
|
174
|
+
* that must rest on a signed identity binds on `claims.sub`, never on email.
|
|
175
|
+
*/
|
|
176
|
+
unverifiedClaims: string[];
|
|
165
177
|
idToken: string;
|
|
166
178
|
accessToken?: string;
|
|
167
179
|
refreshToken?: string;
|
|
@@ -291,6 +303,11 @@ interface SessionPayload {
|
|
|
291
303
|
* reference, so a changed picture propagates without every app storing a
|
|
292
304
|
* copy that then goes stale. */
|
|
293
305
|
picture?: string;
|
|
306
|
+
/**
|
|
307
|
+
* Whether BID has verified `email` (F084.152). Absent when BID did not say.
|
|
308
|
+
* Bind access to an address only when this is `true`.
|
|
309
|
+
*/
|
|
310
|
+
email_verified?: boolean;
|
|
294
311
|
}
|
|
295
312
|
|
|
296
313
|
/**
|
|
@@ -318,6 +335,15 @@ interface BackchannelOptions {
|
|
|
318
335
|
onStoreError?: "reject" | "allow";
|
|
319
336
|
}
|
|
320
337
|
interface SsoRoutesOptions {
|
|
338
|
+
/**
|
|
339
|
+
* What «Log ud» in the app does (F084.152).
|
|
340
|
+
* "app" (default): end THIS app's session and send the user to BID's login
|
|
341
|
+
* dialog (prompt=login). They stay signed in to BID — its own «Log ud» is
|
|
342
|
+
* a separate action (decision D-376ffa) — and BID shows the dialog anyway.
|
|
343
|
+
* "central": the pre-0.7.0 behaviour — RP-initiated logout at BID's
|
|
344
|
+
* end-session endpoint, which also ends the BID session.
|
|
345
|
+
*/
|
|
346
|
+
logout?: "app" | "central";
|
|
321
347
|
/**
|
|
322
348
|
* Receive «Log ud overalt» from BID at POST <mount>/backchannel-logout.
|
|
323
349
|
* Nothing arrives until BID has registered that URL for this app (ship dark),
|
package/dist/hono.js
CHANGED
|
@@ -358,18 +358,19 @@ function createSsoClient(config, options = {}) {
|
|
|
358
358
|
}
|
|
359
359
|
async function withUserInfo(claims, accessToken) {
|
|
360
360
|
const doc = await discovery();
|
|
361
|
-
if (!doc.userinfo_endpoint) return claims;
|
|
361
|
+
if (!doc.userinfo_endpoint) return { claims, unverifiedClaims: [] };
|
|
362
362
|
const res = await fetchImpl(doc.userinfo_endpoint, {
|
|
363
363
|
headers: { authorization: `Bearer ${accessToken}` }
|
|
364
364
|
});
|
|
365
|
-
if (!res.ok) return claims;
|
|
365
|
+
if (!res.ok) return { claims, unverifiedClaims: [] };
|
|
366
366
|
const info = await res.json();
|
|
367
367
|
if (info.sub !== claims.sub) {
|
|
368
368
|
throw new SsoError(
|
|
369
369
|
`userinfo describes ${String(info.sub)} but the ID token is for ${claims.sub} \u2014 refusing to merge another user's profile onto this session.`
|
|
370
370
|
);
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
const unverifiedClaims = Object.keys(info).filter((k) => k !== "sub" && info[k] !== void 0 && !(k in claims)).sort();
|
|
373
|
+
return { claims: { ...info, ...claims, sub: claims.sub }, unverifiedClaims };
|
|
373
374
|
}
|
|
374
375
|
return {
|
|
375
376
|
discovery,
|
|
@@ -441,8 +442,10 @@ function createSsoClient(config, options = {}) {
|
|
|
441
442
|
);
|
|
442
443
|
}
|
|
443
444
|
const claims = await verifyIdToken(body.id_token, { nonce });
|
|
445
|
+
const merged = body.access_token ? await withUserInfo(claims, body.access_token) : { claims, unverifiedClaims: [] };
|
|
444
446
|
return {
|
|
445
|
-
claims:
|
|
447
|
+
claims: merged.claims,
|
|
448
|
+
unverifiedClaims: merged.unverifiedClaims,
|
|
446
449
|
idToken: body.id_token,
|
|
447
450
|
...body.access_token ? { accessToken: body.access_token } : {},
|
|
448
451
|
...body.refresh_token ? { refreshToken: body.refresh_token } : {}
|
|
@@ -678,7 +681,7 @@ function ssoRoutes(options = {}) {
|
|
|
678
681
|
app.get("/login", async (c) => {
|
|
679
682
|
const prompt = c.req.query("prompt");
|
|
680
683
|
const start = await client.beginLogin(
|
|
681
|
-
prompt === "none" ? { prompt
|
|
684
|
+
prompt === "none" || prompt === "login" ? { prompt } : {}
|
|
682
685
|
);
|
|
683
686
|
const tx = JSON.stringify({
|
|
684
687
|
state: start.state,
|
|
@@ -738,7 +741,9 @@ function ssoRoutes(options = {}) {
|
|
|
738
741
|
iat: Math.floor(Date.now() / 1e3),
|
|
739
742
|
...result.claims.email ? { email: result.claims.email } : {},
|
|
740
743
|
...result.claims.name ? { name: result.claims.name } : {},
|
|
741
|
-
...result.claims.picture ? { picture: result.claims.picture } : {}
|
|
744
|
+
...result.claims.picture ? { picture: result.claims.picture } : {},
|
|
745
|
+
// Only when BID actually said so — absent means "not stated", never false.
|
|
746
|
+
...typeof result.claims.email_verified === "boolean" ? { email_verified: result.claims.email_verified } : {}
|
|
742
747
|
};
|
|
743
748
|
c.header(
|
|
744
749
|
"Set-Cookie",
|
|
@@ -776,6 +781,9 @@ function ssoRoutes(options = {}) {
|
|
|
776
781
|
cookieHeader(idTokenCookie, "", { maxAge: 0, secure: isSecure(c) }),
|
|
777
782
|
{ append: true }
|
|
778
783
|
);
|
|
784
|
+
if ((options.logout ?? "app") === "app") {
|
|
785
|
+
return c.redirect(`${loginPath}/login?prompt=login`, 302);
|
|
786
|
+
}
|
|
779
787
|
return c.redirect(await client.logoutUrl(idTokenHint ? { idTokenHint } : {}), 302);
|
|
780
788
|
});
|
|
781
789
|
const attach = async (c, next) => {
|