@broberg/sso 0.20.0 → 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 +23 -0
- package/dist/{chunk-KEBSZBHL.cjs → chunk-2XMKZWC6.cjs} +297 -5
- package/dist/chunk-2XMKZWC6.cjs.map +1 -0
- package/dist/{chunk-ZDNBHM2G.js → chunk-UDUPP35Z.js} +285 -7
- 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 -325
- 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 +4 -260
- package/dist/index.js.map +1 -1
- package/dist/{session-AgUJ6U_7.d.cts → session-DnotfVNd.d.cts} +17 -1
- package/dist/{session-AgUJ6U_7.d.ts → session-DnotfVNd.d.ts} +17 -1
- package/package.json +1 -1
- package/dist/chunk-KEBSZBHL.cjs.map +0 -1
- package/dist/chunk-ZDNBHM2G.js.map +0 -1
package/dist/hono.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hono.ts"],"names":["require"],"mappings":"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","file":"hono.js","sourcesContent":["/**\n * The Hono adapter — mounting, and nothing else.\n *\n * Every rule about what a valid session IS lives in the core (client.ts,\n * session.ts). This file only turns those into routes. F084.5 adds a Next.js\n * adapter on the SAME core, because a rule implemented twice is a rule that\n * eventually says two things.\n */\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { loadSsoConfig, type SsoConfig } from \"./config.js\";\nimport {\n AVATAR_TYPES,\n createSsoClient,\n MAX_AVATAR_BYTES,\n mediaType,\n SsoError,\n SsoReauthError,\n type BidProfile,\n type SsoClient,\n} from \"./client.js\";\nimport { expiresAtFrom, newSid, type TokenStore } from \"./tokens.js\";\nimport {\n cookieHeader,\n readCookie,\n signSession,\n signValue,\n verifySession,\n verifyValue,\n type SessionPayload,\n} from \"./session.js\";\n\n/** Where the adapter parks the session on the request, for `getSession`. */\nconst SESSION_KEY = \"bidSession\";\n\n/**\n * How long the SERVER will accept an in-flight login. Five minutes is plenty\n * for a person to type a password; longer just widens the window. This is the\n * real limit: it is enforced in `parseTransaction`, on a timestamp inside the\n * signature, so no client can talk its way past it.\n */\nconst TRANSACTION_MAX_AGE = 300;\n\n/**\n * How long the BROWSER keeps the cookie — deliberately LONGER than the window\n * above, and the two numbers must never be collapsed back into one.\n *\n * A cookie the browser has already dropped NEVER ARRIVES. With one number for\n * both, the moment the window passes there is simply nothing in the request,\n * and the server cannot tell \"she took too long\" from \"she never started a\n * login here\". Those are different things that deserve different answers, and\n * collapsing them cost a diagnosis: this adapter used to answer every failed\n * callback with one message containing the word \"or\".\n *\n * Reported by helpdesk (2026-09-22, components-F084.54), who run 3x in\n * production for exactly this reason. The extra time is INERT — `parseTransaction`\n * refuses anything past TRANSACTION_MAX_AGE and /callback clears the cookie, so\n * a verifier that cannot be exchanged is not a key. It buys diagnosis, not\n * lifetime.\n */\nconst TRANSACTION_COOKIE_MAX_AGE = TRANSACTION_MAX_AGE * 3;\n\n/**\n * Where «Log ud overalt» is remembered (F084.151). The APP provides it, backed by\n * something every instance shares — apps run on two machines, so a per-process\n * list would log the user out on one machine and not the other. There is\n * deliberately no in-memory default.\n */\nexport interface BackchannelStore {\n /** Every session for `sub` minted at or before `iat` is over. Keep the LATEST iat. */\n revokeSubBefore(sub: string, iat: number): Promise<void>;\n /** The iat recorded for `sub`, or null. */\n revokedBefore(sub: string): Promise<number | null>;\n /** Record a logout token id; true the FIRST time, false for a replay. Keep it until `until` (unix s). */\n useJti(jti: string, until: number): Promise<boolean>;\n}\n\nexport interface BackchannelOptions {\n store: BackchannelStore;\n /**\n * When the store cannot answer: \"reject\" (default) treats the user as logged\n * out; \"allow\" lets the session through unchecked. «reject» is the secure\n * choice — a user who pressed «Log ud overalt» must not stay in because a\n * database hiccupped — at the price of everyone being out while it lasts.\n */\n onStoreError?: \"reject\" | \"allow\";\n}\n\nexport interface SsoRoutesOptions {\n /**\n * What «Log ud» in the app does (F084.152).\n * \"app\" (default): end THIS app's session and send the user to BID's login\n * dialog (prompt=login). They stay signed in to BID — its own «Log ud» is\n * a separate action (decision D-376ffa) — and BID shows the dialog anyway.\n * \"central\": the pre-0.7.0 behaviour — RP-initiated logout at BID's\n * end-session endpoint, which also ends the BID session.\n */\n logout?: \"app\" | \"central\";\n /**\n * Receive «Log ud overalt» from BID at POST <mount>/backchannel-logout.\n * Nothing arrives until BID has registered that URL for this app (ship dark),\n * so mounting it early is safe.\n */\n backchannel?: BackchannelOptions;\n config?: SsoConfig;\n client?: SsoClient;\n /** Path prefix these routes are mounted under. Used to build the return URL. */\n loginPath?: string;\n /** Where to send a user after a successful login when no returnTo is given. */\n defaultReturnTo?: string;\n /**\n * F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app\n * can call BID's app API (the account page) as the user via getAccessToken.\n * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.\n */\n tokenStore?: TokenStore;\n /**\n * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).\n * \"single\" (default): an expired login and an unreadable cookie get the SAME\n * answer, `login_failed`. The two differ only in whether the cookie's\n * signature held against the current secret, and a stranger holding a\n * FOUND cookie must not be able to ask that.\n * \"granular\": the 0.3.0–0.11.0 behaviour — `login_expired` and\n * `bad_login_cookie` told apart. For an app that knows its threat model\n * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.\n * `no_login_in_progress` is its own answer either way: it depends on no secret.\n */\n callbackErrors?: \"single\" | \"granular\";\n /**\n * Receives the PRECISE cause of every refused callback, whatever the browser\n * was told — this is the operator's channel. `unreadable` on a real user's\n * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.\n * Without it, the cause is written with console.warn so the log still has it.\n */\n onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;\n}\n\n/** Why /callback refused: no login started here · too old · signature did not hold. */\nexport type CallbackRefusal = \"absent\" | \"expired\" | \"unreadable\";\n\nfunction isSecure(c: Context): boolean {\n // localhost over http is the one case where a Secure cookie would simply\n // never be stored, making local development impossible for a reason no error\n // would explain.\n return new URL(c.req.url).protocol === \"https:\";\n}\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * Without this, `/auth/login?returnTo=https://evil.example` turns this app's\n * own login into an open redirect that arrives wearing our domain — the\n * classic phishing lever, and it is one line to close.\n */\n/**\n * An origin nothing can resolve to. Only used as a yardstick: if a candidate\n * still sits on THIS origin after parsing, it is same-origin wherever the app\n * actually lives.\n */\nconst YARDSTICK = \"https://sso.invalid\";\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * ── WHY THIS ASKS A PARSER INSTEAD OF INSPECTING CHARACTERS ───────────────\n *\n * The first version of this function rejected a leading `//` and required a\n * leading `/`. That is the obvious rule, it reads as correct, and it was\n * EXPLOITABLE — found in the first full security review of this package\n * (2026-09-20), not by any diff review, because this code never appeared in a\n * diff:\n *\n * returnTo=/\\evil.dk → passed the guard\n * → resolves to https://evil.dk/ in a browser\n *\n * A backslash is not a path separator to RFC 3986 and IS one to the WHATWG URL\n * spec, which is what browsers implement. So the string looked like a path to\n * us and was an authority to the thing that acts on it.\n *\n * AND THE OBVIOUS FIX WAS ALSO LEAKY. Parsing once and comparing origins closes\n * the backslash and still lets `/..//evil.dk` through: `..` NORMALISES the path\n * to `//evil.dk`, which is protocol-relative when it is later used as a\n * Location. So the result is re-checked — a fixpoint — rather than trusted\n * because the input parsed cleanly.\n *\n * The rule is therefore: let the SAME parser the browser uses decide, twice.\n * A list of dangerous characters is always one trick behind; the first version\n * knew `//` and not `\\`, and the second knew `\\` and not `..`.\n */\nfunction safeReturnTo(raw: string | undefined, fallback: string): string {\n if (!raw || !raw.startsWith(\"/\")) return fallback;\n\n let once: URL;\n try {\n once = new URL(raw, YARDSTICK);\n } catch {\n return fallback;\n }\n if (once.origin !== YARDSTICK) return fallback;\n\n const normalised = once.pathname + once.search + once.hash;\n let twice: URL;\n try {\n twice = new URL(normalised, YARDSTICK);\n } catch {\n return fallback;\n }\n if (twice.origin !== YARDSTICK) return fallback;\n // Belt and braces: a normalised path must still be a path.\n if (!normalised.startsWith(\"/\") || normalised.startsWith(\"//\")) return fallback;\n\n return normalised;\n}\n\nexport function ssoRoutes(options: SsoRoutesOptions = {}) {\n const config = options.config ?? loadSsoConfig();\n const client = options.client ?? createSsoClient(config);\n const loginPath = options.loginPath ?? \"/auth\";\n const defaultReturnTo = options.defaultReturnTo ?? \"/\";\n const txCookie = `${config.cookieName}_tx`;\n /**\n * The ID token, kept ONLY so logout can prove who is leaving.\n *\n * Its own cookie rather than a field on SessionPayload, for three reasons:\n * that type is public and deliberately small (session.ts says why), a JWT is\n * ~1 kB that would otherwise ride along on every single request to the app,\n * and the two are cleared at different moments.\n */\n const idTokenCookie = `${config.cookieName}_idt`;\n const backchannel = options.backchannel;\n if (backchannel && !backchannel.store) {\n throw new Error(\"ssoRoutes: backchannel needs a store shared by every instance of the app — there is no in-memory default.\");\n }\n const onStoreError = backchannel?.onStoreError ?? \"reject\";\n const tokenStore = options.tokenStore;\n const callbackErrors = options.callbackErrors ?? \"single\";\n const onCallbackRefused =\n options.onCallbackRefused ?? ((cause: CallbackRefusal) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));\n\n /** A verified session, or null — also null when «Log ud overalt» has ended it. */\n async function liveSession(c: Context): Promise<SessionPayload | null> {\n const session = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (!session || !backchannel) return session;\n try {\n const before = await backchannel.store.revokedBefore(session.sub);\n if (before !== null && (session.iat ?? 0) <= before) return null;\n return session;\n } catch {\n return onStoreError === \"allow\" ? session : null;\n }\n }\n\n const app = new Hono();\n\n if (backchannel) {\n app.post(\"/backchannel-logout\", async (c) => {\n c.header(\"Cache-Control\", \"no-store\");\n let token: string | undefined;\n try {\n const form = await c.req.parseBody();\n token = typeof form.logout_token === \"string\" ? form.logout_token : undefined;\n } catch {\n token = undefined;\n }\n if (!token) return c.json({ error: \"invalid_request\", error_description: \"logout_token is missing\" }, 400);\n let verified;\n try {\n verified = await client.verifyLogoutToken(token);\n } catch (e) {\n const msg = e instanceof SsoError ? e.message : \"logout_token could not be verified\";\n return c.json({ error: \"invalid_request\", error_description: msg }, 400);\n }\n try {\n // Revoke FIRST, then record the jti. The other order loses a logout: if the\n // revoke failed after the jti was recorded, the retry would be read as a\n // replay and do nothing. Revoking twice is harmless (the store keeps the\n // latest iat), so a replay only costs one idempotent write.\n await backchannel.store.revokeSubBefore(verified.sub, verified.iat);\n // F095.1 — the user's tokens go with the sessions. Inside the same try:\n // not deleted = not done, so BID reports this app as failed.\n if (tokenStore) await tokenStore.deleteSub(verified.sub);\n await backchannel.store.useJti(verified.jti, verified.exp);\n } catch {\n // Not stored = not done. 5xx so BID reports this app as failed to the user.\n return c.json({ error: \"store_unavailable\" }, 503);\n }\n return c.body(null, 200);\n });\n }\n\n app.get(\"/login\", async (c) => {\n const prompt = c.req.query(\"prompt\");\n // Only the two prompts an app has a reason to ask for; anything else is dropped.\n const start = await client.beginLogin(\n prompt === \"none\" || prompt === \"login\" ? { prompt } : {},\n );\n const tx = JSON.stringify({\n state: start.state,\n codeVerifier: start.codeVerifier,\n nonce: start.nonce,\n returnTo: safeReturnTo(c.req.query(\"returnTo\"), defaultReturnTo),\n });\n c.header(\n \"Set-Cookie\",\n // The signed stamp carries the SERVER's window; the cookie carries the\n // longer browser one, so an expired transaction still reaches us and can\n // be named instead of vanishing.\n cookieHeader(txCookie, await signValue(tx, config.cookieSecret, { maxAgeSeconds: TRANSACTION_MAX_AGE }), {\n maxAge: TRANSACTION_COOKIE_MAX_AGE,\n secure: isSecure(c),\n }),\n );\n // A FULL top-level redirect. Never a hidden iframe — see client.ts.\n return c.redirect(start.url, 302);\n });\n\n app.get(\"/callback\", async (c) => {\n const parsed = await parseTransaction(\n readCookie(c.req.header(\"cookie\"), txCookie),\n config.cookieSecret,\n );\n if (!parsed.ok) {\n /**\n * THREE CAUSES, THREE ANSWERS (components-F084.54).\n *\n * THE ORDER IN `parseTransaction` IS LOAD-BEARING AND NOT OBVIOUS:\n * `expired` is reached ONLY AFTER the signature has verified. So\n * `login_expired` is a POSITIVE CONFIRMATION that the HMAC held against\n * the CURRENT secret, and `bad_login_cookie` is the denial of that.\n *\n * WHICH MAKES THE PAIR A ONE-BIT ORACLE, and the justification that stood\n * here was wrong. It said an attacker can produce all three himself, so\n * naming them tells him nothing. That holds only for an attacker who\n * CONSTRUCTED the input and therefore knows what he is holding. The\n * interesting one has a value he FOUND — in a log, on a shared machine, in\n * a referrer leak, in a backup — and he cannot mint a\n * validly-signed-but-expired value himself. For him the answer separates\n * two things he could not otherwise learn: whether a found value is still\n * live, when the secret rotates (the same value changes code the second it\n * does), and whether two environments share a secret.\n *\n * Reported by helpdesk, 2026-09-22, measured against the published 0.3.0\n * dist rather than argued. It is NOT a hole: it does not help him forge\n * anything — one bit per attempt against an HMAC-SHA256 key he never\n * reaches. It is a property the server gives away for free.\n *\n * SO THE TWO AUDIENCES ARE SPLIT (components-F084.55, Christian\n * 2026-10-07: «ét svar» as the default). The app and the log get all\n * three causes through `onCallbackRefused`, always. The browser gets ONE\n * answer for expired + unreadable unless the app opts into \"granular\".\n * `absent` stays its own code: it depends on no secret, so it leaks\n * nothing. Do NOT reorder parseTransaction to hide the oracle instead —\n * signature-before-age is what keeps the operator's two causes apart.\n *\n * The operator half stands unchanged and is the reason the codes exist at\n * all: `bad_login_cookie` on a REAL user's browser is what a rotated\n * SSO_COOKIE_SECRET looks like from the outside.\n */\n const failures = {\n absent: {\n status: 400 as const,\n error: \"no_login_in_progress\",\n message: \"This browser did not start a login here. Begin again from the login page.\",\n },\n expired: {\n status: 400 as const,\n error: \"login_expired\",\n message: `This login took longer than ${TRANSACTION_MAX_AGE} seconds. Begin again from the login page.`,\n },\n unreadable: {\n status: 400 as const,\n error: \"bad_login_cookie\",\n message: \"This browser's login cookie could not be read. Begin again from the login page.\",\n },\n };\n // The operator's channel must never turn a refusal into a 500: a hook that\n // throws is reported, and the browser still gets its answer.\n try {\n await onCallbackRefused(parsed.reason, c);\n } catch (err) {\n console.error(\"[@broberg/sso] onCallbackRefused threw; the refusal stands\", err);\n }\n const shown =\n callbackErrors === \"single\" && parsed.reason !== \"absent\"\n ? {\n status: 400 as const,\n error: \"login_failed\",\n message: \"This login could not be completed. Begin again from the login page.\",\n }\n : failures[parsed.reason];\n const { status, ...body } = shown;\n // Clear it either way: a transaction we refuse must not sit in the browser\n // waiting to be refused again on every retry.\n c.header(\"Set-Cookie\", cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }));\n return c.json(body, status);\n }\n\n const result = await client.completeLogin({\n params: new URL(c.req.url).searchParams,\n state: parsed.tx.state,\n codeVerifier: parsed.tx.codeVerifier,\n nonce: parsed.tx.nonce,\n });\n\n const nowS = Math.floor(Date.now() / 1000);\n const exp = nowS + config.sessionMaxAge;\n // F095.1 — tokens to the server-side store, keyed by a fresh sid; only the\n // sid rides in the cookie.\n let sid: string | undefined;\n if (tokenStore && result.accessToken) {\n sid = newSid();\n await tokenStore.set(sid, {\n sub: result.claims.sub,\n accessToken: result.accessToken,\n ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),\n expiresAt: expiresAtFrom(result.expiresIn, result.accessToken, nowS),\n });\n }\n const session: SessionPayload = {\n sub: result.claims.sub,\n exp,\n iat: nowS,\n ...(sid ? { sid } : {}),\n ...(result.claims.email ? { email: result.claims.email } : {}),\n ...(result.claims.name ? { name: result.claims.name } : {}),\n ...(result.claims.picture ? { picture: result.claims.picture } : {}),\n // Only when BID actually said so — absent means \"not stated\", never false.\n ...(typeof result.claims.email_verified === \"boolean\" ? { email_verified: result.claims.email_verified } : {}),\n };\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(session, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n );\n // The logout hint. Without it the issuer MUST ask the user to confirm —\n // that is RP-initiated logout per spec, and the plugin says so itself\n // (\"User confirmation is required to complete logout\"). The result was an\n // unstyled English confirmation page with a bare browser button in the\n // middle of our own product.\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, await signValue(result.idToken, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n { append: true },\n );\n // Clear the transaction cookie: it has done its job, and a replayed one is\n // only ever useful to someone who should not have it.\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n return c.redirect(parsed.tx.returnTo, 302);\n });\n\n app.get(\"/logout\", async (c) => {\n // Read the hint BEFORE clearing, obviously — but note what happens when it\n // is absent: a session minted by 0.1.0 has no such cookie, and that session\n // is still live in every app that upgrades. So a missing hint falls back to\n // the old behaviour (the issuer asks) rather than throwing. The upgrade path\n // is the one that fails in production; it gets its own test.\n const idTokenHint = await verifyValue(\n readCookie(c.req.header(\"cookie\"), idTokenCookie),\n config.cookieSecret,\n );\n // F095.1 — the tokens die with the session. Read the sid before the cookie goes.\n if (tokenStore) {\n const leaving = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (leaving?.sid) await tokenStore.delete(leaving.sid);\n }\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, \"\", { maxAge: 0, secure: isSecure(c) }),\n );\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n // F084.152 — the default: this app is logged out, BID is not, and the user\n // lands on BID's login dialog (prompt=login makes BID show it even with a\n // live BID session). Through our own /login, so PKCE and state still apply.\n if ((options.logout ?? \"app\") === \"app\") {\n return c.redirect(`${loginPath}/login?prompt=login`, 302);\n }\n // Local cookies cleared FIRST, then central logout. If the redirect to BID\n // fails or the user closes the tab, the worst case is \"signed out here but\n // not everywhere\" — never the reverse, which would leave this app trusting\n // a session the user believes is gone.\n return c.redirect(await client.logoutUrl(idTokenHint ? { idTokenHint } : {}), 302);\n });\n\n /** Reads the session and puts it on the context. Never blocks. */\n const attach: MiddlewareHandler = async (c, next) => {\n c.set(SESSION_KEY, await liveSession(c));\n await next();\n };\n\n /** Blocks and redirects to login, preserving where the user was going. */\n const require: MiddlewareHandler = async (c, next) => {\n const session = await liveSession(c);\n if (!session) {\n const url = new URL(c.req.url);\n return c.redirect(\n `${loginPath}/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`,\n 302,\n );\n }\n c.set(SESSION_KEY, session);\n await next();\n };\n\n /**\n * F095.1 — the user's access token for calling BID's app API, renewed with the\n * refresh token when it has expired (or is within 30 s of it). Throws SsoError\n * — never returns a stale or empty token — when there is no live session, no\n * stored tokens, or the renewal is refused; a refused renewal also deletes the\n * dead pair so the next call does not try it again.\n */\n const refreshing = new Map<string, Promise<string>>();\n async function getAccessToken(c: Context): Promise<string> {\n if (!tokenStore) {\n throw new SsoError(\"getAccessToken needs ssoRoutes({ tokenStore }) — without one the login's tokens are not kept.\");\n }\n const raw = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n const live = await liveSession(c);\n if (!live) {\n if (raw?.sid) await tokenStore.delete(raw.sid);\n throw new SsoReauthError(\"no live session — log in again.\", \"no_session\");\n }\n const sid = live.sid;\n if (!sid) throw new SsoReauthError(\"this session has no stored tokens (it began before tokenStore was configured) — log in again.\", \"no_tokens\");\n const tokens = await tokenStore.get(sid);\n if (!tokens) throw new SsoReauthError(\"no tokens stored for this session — log in again.\", \"no_tokens\");\n const now = Math.floor(Date.now() / 1000);\n if (tokens.expiresAt - 30 > now) return tokens.accessToken;\n if (!tokens.refreshToken) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(\"the access token has expired and there is no refresh token — log in again.\", \"expired\");\n }\n // One renewal per session at a time: with refresh-token rotation, a second\n // parallel renewal would present an already-spent token and be refused.\n const inFlight = refreshing.get(sid);\n if (inFlight) return inFlight;\n const p = (async () => {\n let next;\n try {\n next = await client.refreshTokens(tokens.refreshToken!);\n } catch (e) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(e instanceof SsoError ? e.message : \"token refresh failed\", \"refresh_failed\");\n }\n await tokenStore.set(sid, {\n sub: tokens.sub,\n accessToken: next.accessToken,\n // Rotation: use the new one if BID issued one, else keep the old.\n ...(next.refreshToken ?? tokens.refreshToken ? { refreshToken: next.refreshToken ?? tokens.refreshToken } : {}),\n expiresAt: expiresAtFrom(next.expiresIn, next.accessToken, Math.floor(Date.now() / 1000)),\n });\n return next.accessToken;\n })();\n refreshing.set(sid, p);\n try {\n return await p;\n } finally {\n refreshing.delete(sid);\n }\n }\n\n /**\n * F095.5 — after the user changed her own name or picture, re-sign the\n * session cookie with the new values, so the user menu shows them on the next\n * page load without a new login. Only the convenience copies change: sub,\n * sid, iat and exp are kept as they are (the cookie's lifetime is NOT\n * extended — Max-Age is what is left of it). No live session → nothing.\n */\n async function refreshSessionProfile(c: Context, profile: { name: string | null; picture: string | null }): Promise<void> {\n const live = await liveSession(c);\n if (!live) return;\n const next: SessionPayload = { ...live };\n if (profile.name) next.name = profile.name;\n else delete next.name;\n if (profile.picture) next.picture = profile.picture;\n else delete next.picture;\n if (next.name === live.name && next.picture === live.picture) return;\n const left = live.exp - Math.floor(Date.now() / 1000);\n if (left <= 0) return;\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),\n { append: true },\n );\n }\n\n return { app, attach, require, client, config, getAccessToken, refreshSessionProfile };\n}\n\n/** Why a login transaction could not be read back. Three states, three answers. */\nexport type TransactionFailure = \"absent\" | \"expired\" | \"unreadable\";\n\ntype TransactionResult =\n | { ok: true; tx: { state: string; codeVerifier: string; nonce: string; returnTo: string } }\n | { ok: false; reason: TransactionFailure };\n\n/**\n * Reads the login transaction back out of its signed cookie.\n *\n * RETURNS A REASON, NOT `null` (components-F084.54). It used to collapse three\n * causes into one empty answer, and /callback then told the user \"you did not\n * start a login here, OR it expired\" — a sentence that admits in its own wording\n * that it does not know. The word \"or\" was the bug.\n *\n * Age-checked against TRANSACTION_MAX_AGE, not only against the cookie's own\n * Max-Age (components-F084.53) — Max-Age is the browser's promise, and a client\n * can decline to make it. It still does NOT go through verifySession: that\n * envelope carries an `exp`, and the first version of this file used `exp: 0`,\n * which the expiry check rejected every time.\n *\n * NO FUTURE-STAMP GUARD, and that is a deliberate divergence from helpdesk's\n * implementation, which rejects a negative age so a runaway clock cannot widen\n * the window. Ours cannot be widened that way: the stamp is inside the HMAC, so\n * only OUR OWN clock could produce a future one — and refusing it would mean a\n * second app instance whose clock is a few seconds behind starts rejecting\n * perfectly good logins. That trades a theoretical gain for a real outage.\n */\nasync function parseTransaction(raw: string | undefined, secret: string): Promise<TransactionResult> {\n if (raw === undefined) return { ok: false, reason: \"absent\" };\n\n // Signature FIRST, without the age limit, so the two questions stay separate.\n // Asking them together is what produced one answer for three causes: a single\n // `verifyValue(..., { maxAgeSeconds })` returns null for a forged cookie and\n // for an honest late one alike.\n const signed = await verifyValue(raw, secret);\n if (signed === null) return { ok: false, reason: \"unreadable\" };\n\n const fresh = await verifyValue(raw, secret, { maxAgeSeconds: TRANSACTION_MAX_AGE });\n if (fresh === null) return { ok: false, reason: \"expired\" };\n\n try {\n const tx = JSON.parse(fresh) as {\n state: string;\n codeVerifier: string;\n nonce: string;\n returnTo: string;\n };\n // A correctly signed cookie whose contents are not a transaction is not an\n // expiry and not an absence — it is ours and it is wrong.\n if (!tx.state || !tx.codeVerifier || !tx.nonce) return { ok: false, reason: \"unreadable\" };\n return { ok: true, tx };\n } catch {\n return { ok: false, reason: \"unreadable\" };\n }\n}\n\n/** Read the session a middleware attached. */\nexport function getSession(c: Context): SessionPayload | null {\n return (c.get(SESSION_KEY) as SessionPayload | null) ?? null;\n}\n\n/* ── F095.2 — the user's own profile, as an API the app's account page calls ── */\n\n/** What accountRoutes needs from ssoRoutes — pass its return value as is. */\nexport interface AccountRoutesInput {\n client: Pick<SsoClient, \"getProfile\" | \"updateProfile\" | \"uploadAvatar\" | \"removeAvatar\">;\n getAccessToken(c: Context): Promise<string>;\n /**\n * F095.5 — re-sign the session cookie with the profile BID answered, so the\n * user menu shows the new name and picture on the next page load. ssoRoutes\n * provides it; optional so an older or hand-built input still works.\n */\n refreshSessionProfile?(c: Context, profile: { name: string | null; picture: string | null }): Promise<void>;\n}\n\n/**\n * Errors are matched by NAME, not instanceof. The ./hono entry is a separate\n * bundle with its own copy of the error classes, and an app may hand ssoRoutes a\n * client built from the main entry — instanceof would then miss every one of\n * them and a 403 insufficient_scope would come out as a 500.\n */\ntype Named = Error & { reason?: string; scope?: string; status?: number | null; code?: string };\nfunction errName(e: unknown): string | undefined {\n return e instanceof Error ? e.name : undefined;\n}\n\n/**\n * Read a request body, but stop at `max` bytes — Content-Length is the client's\n * claim, and a chunked upload has none. Returns null when the body is larger.\n */\nasync function readCapped(c: Context, max: number): Promise<Uint8Array | null> {\n const stream = c.req.raw.body;\n if (!stream) return new Uint8Array(0);\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > max) {\n await reader.cancel().catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const ch of chunks) {\n out.set(ch, at);\n at += ch.byteLength;\n }\n return out;\n}\n\n/**\n * The signed-in user's own name and picture, for the app's account page.\n * Mount it on /api/account:\n *\n * const sso = ssoRoutes({ tokenStore });\n * app.route(\"/api/account\", accountRoutes(sso));\n *\n * GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·\n * POST /profile/avatar/remove. Every answer is JSON, refusals included — it is\n * an API, so never a login redirect:\n * 401 {error:\"unauthenticated\"} no live session\n * 401 {error:\"reauth\"} the session's tokens are spent — log in again\n * 403 {error:\"insufficient_scope\", scope} log in again with that scope requested\n * 413 / 415 picture too large / not PNG, JPEG or WebP\n * 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)\n * 502 {error:\"bid_unavailable\"} BID did not answer usably\n */\nexport function accountRoutes(sso: AccountRoutesInput) {\n const app = new Hono();\n const { client } = sso;\n\n // Personal data: never cached by a browser or a proxy.\n app.use(\"*\", async (c, next) => {\n c.header(\"Cache-Control\", \"no-store\");\n await next();\n });\n\n // A write from ANOTHER site is refused. SameSite=Lax stops a foreign domain,\n // but not a sibling subdomain (same site), and /profile/avatar/remove needs\n // no body at all — a plain <form> would do. Browsers send Sec-Fetch-Site on\n // every request; a non-browser caller sends none and is let through.\n app.use(\"*\", async (c, next) => {\n const site = c.req.header(\"sec-fetch-site\");\n if (c.req.method !== \"GET\" && site !== undefined && site !== \"same-origin\" && site !== \"none\") {\n return c.json({ error: \"cross_site\" }, 403);\n }\n await next();\n });\n\n // Every answer carries the profile AS BID NOW HOLDS IT; the session cookie\n // follows it (F095.5), so the user menu is right after a reload too — also\n // when the name was changed in BID itself.\n const answer = async (c: Context, profile: BidProfile) => {\n if (sso.refreshSessionProfile) await sso.refreshSessionProfile(c, profile);\n return c.json(profile);\n };\n\n // The session is checked FIRST on every route, before any body is looked at.\n app.get(\"/profile\", async (c) => answer(c, await client.getProfile(await sso.getAccessToken(c))));\n\n app.post(\"/profile\", async (c) => {\n const token = await sso.getAccessToken(c);\n const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null;\n if (typeof body?.name !== \"string\") {\n return c.json({ error: \"invalid_request\", error_description: '\"name\" must be a string' }, 400);\n }\n return answer(c, await client.updateProfile(token, body.name));\n });\n\n app.post(\"/profile/avatar\", async (c) => {\n const token = await sso.getAccessToken(c);\n const type = mediaType(c.req.header(\"content-type\"));\n if (!AVATAR_TYPES.includes(type)) {\n return c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n }\n // Early: a declared size over the limit is refused without reading a byte.\n const declared = Number(c.req.header(\"content-length\"));\n if (Number.isFinite(declared) && declared > MAX_AVATAR_BYTES) {\n return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n }\n // And a body without (or lying about) its length is cut off at the limit.\n const bytes = await readCapped(c, MAX_AVATAR_BYTES);\n if (!bytes) return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n return answer(c, await client.uploadAvatar(token, bytes, type));\n });\n\n app.post(\"/profile/avatar/remove\", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));\n\n app.onError((err, c) => {\n c.header(\"Cache-Control\", \"no-store\");\n const e = err as Named;\n switch (errName(err)) {\n case \"SsoReauthError\":\n return c.json({ error: e.reason === \"no_session\" ? \"unauthenticated\" : \"reauth\" }, 401);\n case \"SsoInsufficientScopeError\":\n return c.json({ error: \"insufficient_scope\", scope: e.scope }, 403);\n case \"SsoAvatarRejectedError\":\n return e.reason === \"too_large\"\n ? c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413)\n : c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n case \"SsoAppApiError\": {\n // BID no longer accepts the token (revoked, or the session ended in BID) → log in again.\n if (e.status === 401) return c.json({ error: \"reauth\" }, 401);\n if (typeof e.status === \"number\" && e.status >= 400 && e.status < 500) {\n return c.json({ error: e.code ?? \"bid_refused\" }, e.status as 400);\n }\n return c.json({ error: \"bid_unavailable\" }, 502);\n }\n // A plain SsoError here is a setup mistake (e.g. ssoRoutes without tokenStore).\n case \"SsoError\":\n return c.json({ error: \"sso_misconfigured\" }, 500);\n default:\n return c.json({ error: \"server_error\" }, 500);\n }\n });\n\n return app;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/hono.ts"],"names":["require"],"mappings":"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","file":"hono.js","sourcesContent":["/**\n * The Hono adapter — mounting, and nothing else.\n *\n * Every rule about what a valid session IS lives in the core (client.ts,\n * session.ts). This file only turns those into routes. F084.5 adds a Next.js\n * adapter on the SAME core, because a rule implemented twice is a rule that\n * eventually says two things.\n */\nimport { Hono, type Context, type MiddlewareHandler } from \"hono\";\nimport { loadSsoConfig, type SsoConfig } from \"./config.js\";\nimport {\n AVATAR_TYPES,\n createSsoClient,\n MAX_AVATAR_BYTES,\n mediaType,\n SsoError,\n SsoReauthError,\n type BidProfile,\n type SsoClient,\n} from \"./client.js\";\nimport { expiresAtFrom, newSid, type TokenStore } from \"./tokens.js\";\nimport {\n cookieHeader,\n readCookie,\n signSession,\n signValue,\n verifySession,\n verifyValue,\n type SessionPayload,\n} from \"./session.js\";\n\n/** Where the adapter parks the session on the request, for `getSession`. */\nconst SESSION_KEY = \"bidSession\";\n\n/**\n * How long the SERVER will accept an in-flight login. Five minutes is plenty\n * for a person to type a password; longer just widens the window. This is the\n * real limit: it is enforced in `parseTransaction`, on a timestamp inside the\n * signature, so no client can talk its way past it.\n */\nconst TRANSACTION_MAX_AGE = 300;\n\n/**\n * How long the BROWSER keeps the cookie — deliberately LONGER than the window\n * above, and the two numbers must never be collapsed back into one.\n *\n * A cookie the browser has already dropped NEVER ARRIVES. With one number for\n * both, the moment the window passes there is simply nothing in the request,\n * and the server cannot tell \"she took too long\" from \"she never started a\n * login here\". Those are different things that deserve different answers, and\n * collapsing them cost a diagnosis: this adapter used to answer every failed\n * callback with one message containing the word \"or\".\n *\n * Reported by helpdesk (2026-09-22, components-F084.54), who run 3x in\n * production for exactly this reason. The extra time is INERT — `parseTransaction`\n * refuses anything past TRANSACTION_MAX_AGE and /callback clears the cookie, so\n * a verifier that cannot be exchanged is not a key. It buys diagnosis, not\n * lifetime.\n */\nconst TRANSACTION_COOKIE_MAX_AGE = TRANSACTION_MAX_AGE * 3;\n\n/**\n * Where «Log ud overalt» is remembered (F084.151). The APP provides it, backed by\n * something every instance shares — apps run on two machines, so a per-process\n * list would log the user out on one machine and not the other. There is\n * deliberately no in-memory default.\n */\nexport interface BackchannelStore {\n /** Every session for `sub` minted at or before `iat` is over. Keep the LATEST iat. */\n revokeSubBefore(sub: string, iat: number): Promise<void>;\n /** The iat recorded for `sub`, or null. */\n revokedBefore(sub: string): Promise<number | null>;\n /** Record a logout token id; true the FIRST time, false for a replay. Keep it until `until` (unix s). */\n useJti(jti: string, until: number): Promise<boolean>;\n}\n\nexport interface BackchannelOptions {\n store: BackchannelStore;\n /**\n * When the store cannot answer: \"reject\" (default) treats the user as logged\n * out; \"allow\" lets the session through unchecked. «reject» is the secure\n * choice — a user who pressed «Log ud overalt» must not stay in because a\n * database hiccupped — at the price of everyone being out while it lasts.\n */\n onStoreError?: \"reject\" | \"allow\";\n}\n\nexport interface SsoRoutesOptions {\n /**\n * components-F084.181 — \"fly\": prove this app at BID's token endpoint with its\n * Fly identity instead of a client_secret (see SsoConfig.clientAuth). Overrides\n * SSO_CLIENT_AUTH. Ignored when you pass your own `client`.\n */\n clientAuth?: \"secret\" | \"fly\";\n /**\n * What «Log ud» in the app does (F084.152).\n * \"app\" (default): end THIS app's session and send the user to BID's login\n * dialog (prompt=login). They stay signed in to BID — its own «Log ud» is\n * a separate action (decision D-376ffa) — and BID shows the dialog anyway.\n * \"central\": the pre-0.7.0 behaviour — RP-initiated logout at BID's\n * end-session endpoint, which also ends the BID session.\n */\n logout?: \"app\" | \"central\";\n /**\n * Receive «Log ud overalt» from BID at POST <mount>/backchannel-logout.\n * Nothing arrives until BID has registered that URL for this app (ship dark),\n * so mounting it early is safe.\n */\n backchannel?: BackchannelOptions;\n config?: SsoConfig;\n client?: SsoClient;\n /** Path prefix these routes are mounted under. Used to build the return URL. */\n loginPath?: string;\n /** Where to send a user after a successful login when no returnTo is given. */\n defaultReturnTo?: string;\n /**\n * F095.1 — keep the user's access and refresh token SERVER-SIDE, so the app\n * can call BID's app API (the account page) as the user via getAccessToken.\n * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.\n */\n tokenStore?: TokenStore;\n /**\n * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).\n * \"single\" (default): an expired login and an unreadable cookie get the SAME\n * answer, `login_failed`. The two differ only in whether the cookie's\n * signature held against the current secret, and a stranger holding a\n * FOUND cookie must not be able to ask that.\n * \"granular\": the 0.3.0–0.11.0 behaviour — `login_expired` and\n * `bad_login_cookie` told apart. For an app that knows its threat model\n * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.\n * `no_login_in_progress` is its own answer either way: it depends on no secret.\n */\n callbackErrors?: \"single\" | \"granular\";\n /**\n * Receives the PRECISE cause of every refused callback, whatever the browser\n * was told — this is the operator's channel. `unreadable` on a real user's\n * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.\n * Without it, the cause is written with console.warn so the log still has it.\n */\n onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;\n}\n\n/** Why /callback refused: no login started here · too old · signature did not hold. */\nexport type CallbackRefusal = \"absent\" | \"expired\" | \"unreadable\";\n\nfunction isSecure(c: Context): boolean {\n // localhost over http is the one case where a Secure cookie would simply\n // never be stored, making local development impossible for a reason no error\n // would explain.\n return new URL(c.req.url).protocol === \"https:\";\n}\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * Without this, `/auth/login?returnTo=https://evil.example` turns this app's\n * own login into an open redirect that arrives wearing our domain — the\n * classic phishing lever, and it is one line to close.\n */\n/**\n * An origin nothing can resolve to. Only used as a yardstick: if a candidate\n * still sits on THIS origin after parsing, it is same-origin wherever the app\n * actually lives.\n */\nconst YARDSTICK = \"https://sso.invalid\";\n\n/**\n * Only same-origin, path-only return targets are honoured.\n *\n * ── WHY THIS ASKS A PARSER INSTEAD OF INSPECTING CHARACTERS ───────────────\n *\n * The first version of this function rejected a leading `//` and required a\n * leading `/`. That is the obvious rule, it reads as correct, and it was\n * EXPLOITABLE — found in the first full security review of this package\n * (2026-09-20), not by any diff review, because this code never appeared in a\n * diff:\n *\n * returnTo=/\\evil.dk → passed the guard\n * → resolves to https://evil.dk/ in a browser\n *\n * A backslash is not a path separator to RFC 3986 and IS one to the WHATWG URL\n * spec, which is what browsers implement. So the string looked like a path to\n * us and was an authority to the thing that acts on it.\n *\n * AND THE OBVIOUS FIX WAS ALSO LEAKY. Parsing once and comparing origins closes\n * the backslash and still lets `/..//evil.dk` through: `..` NORMALISES the path\n * to `//evil.dk`, which is protocol-relative when it is later used as a\n * Location. So the result is re-checked — a fixpoint — rather than trusted\n * because the input parsed cleanly.\n *\n * The rule is therefore: let the SAME parser the browser uses decide, twice.\n * A list of dangerous characters is always one trick behind; the first version\n * knew `//` and not `\\`, and the second knew `\\` and not `..`.\n */\nfunction safeReturnTo(raw: string | undefined, fallback: string): string {\n if (!raw || !raw.startsWith(\"/\")) return fallback;\n\n let once: URL;\n try {\n once = new URL(raw, YARDSTICK);\n } catch {\n return fallback;\n }\n if (once.origin !== YARDSTICK) return fallback;\n\n const normalised = once.pathname + once.search + once.hash;\n let twice: URL;\n try {\n twice = new URL(normalised, YARDSTICK);\n } catch {\n return fallback;\n }\n if (twice.origin !== YARDSTICK) return fallback;\n // Belt and braces: a normalised path must still be a path.\n if (!normalised.startsWith(\"/\") || normalised.startsWith(\"//\")) return fallback;\n\n return normalised;\n}\n\nexport function ssoRoutes(options: SsoRoutesOptions = {}) {\n const baseConfig = options.config ?? loadSsoConfig();\n const config = options.clientAuth ? { ...baseConfig, clientAuth: options.clientAuth } : baseConfig;\n const client = options.client ?? createSsoClient(config);\n const loginPath = options.loginPath ?? \"/auth\";\n const defaultReturnTo = options.defaultReturnTo ?? \"/\";\n const txCookie = `${config.cookieName}_tx`;\n /**\n * The ID token, kept ONLY so logout can prove who is leaving.\n *\n * Its own cookie rather than a field on SessionPayload, for three reasons:\n * that type is public and deliberately small (session.ts says why), a JWT is\n * ~1 kB that would otherwise ride along on every single request to the app,\n * and the two are cleared at different moments.\n */\n const idTokenCookie = `${config.cookieName}_idt`;\n const backchannel = options.backchannel;\n if (backchannel && !backchannel.store) {\n throw new Error(\"ssoRoutes: backchannel needs a store shared by every instance of the app — there is no in-memory default.\");\n }\n const onStoreError = backchannel?.onStoreError ?? \"reject\";\n const tokenStore = options.tokenStore;\n const callbackErrors = options.callbackErrors ?? \"single\";\n const onCallbackRefused =\n options.onCallbackRefused ?? ((cause: CallbackRefusal) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));\n\n /** A verified session, or null — also null when «Log ud overalt» has ended it. */\n async function liveSession(c: Context): Promise<SessionPayload | null> {\n const session = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (!session || !backchannel) return session;\n try {\n const before = await backchannel.store.revokedBefore(session.sub);\n if (before !== null && (session.iat ?? 0) <= before) return null;\n return session;\n } catch {\n return onStoreError === \"allow\" ? session : null;\n }\n }\n\n const app = new Hono();\n\n if (backchannel) {\n app.post(\"/backchannel-logout\", async (c) => {\n c.header(\"Cache-Control\", \"no-store\");\n let token: string | undefined;\n try {\n const form = await c.req.parseBody();\n token = typeof form.logout_token === \"string\" ? form.logout_token : undefined;\n } catch {\n token = undefined;\n }\n if (!token) return c.json({ error: \"invalid_request\", error_description: \"logout_token is missing\" }, 400);\n let verified;\n try {\n verified = await client.verifyLogoutToken(token);\n } catch (e) {\n const msg = e instanceof SsoError ? e.message : \"logout_token could not be verified\";\n return c.json({ error: \"invalid_request\", error_description: msg }, 400);\n }\n try {\n // Revoke FIRST, then record the jti. The other order loses a logout: if the\n // revoke failed after the jti was recorded, the retry would be read as a\n // replay and do nothing. Revoking twice is harmless (the store keeps the\n // latest iat), so a replay only costs one idempotent write.\n await backchannel.store.revokeSubBefore(verified.sub, verified.iat);\n // F095.1 — the user's tokens go with the sessions. Inside the same try:\n // not deleted = not done, so BID reports this app as failed.\n if (tokenStore) await tokenStore.deleteSub(verified.sub);\n await backchannel.store.useJti(verified.jti, verified.exp);\n } catch {\n // Not stored = not done. 5xx so BID reports this app as failed to the user.\n return c.json({ error: \"store_unavailable\" }, 503);\n }\n return c.body(null, 200);\n });\n }\n\n app.get(\"/login\", async (c) => {\n const prompt = c.req.query(\"prompt\");\n // Only the two prompts an app has a reason to ask for; anything else is dropped.\n const start = await client.beginLogin(\n prompt === \"none\" || prompt === \"login\" ? { prompt } : {},\n );\n const tx = JSON.stringify({\n state: start.state,\n codeVerifier: start.codeVerifier,\n nonce: start.nonce,\n returnTo: safeReturnTo(c.req.query(\"returnTo\"), defaultReturnTo),\n });\n c.header(\n \"Set-Cookie\",\n // The signed stamp carries the SERVER's window; the cookie carries the\n // longer browser one, so an expired transaction still reaches us and can\n // be named instead of vanishing.\n cookieHeader(txCookie, await signValue(tx, config.cookieSecret, { maxAgeSeconds: TRANSACTION_MAX_AGE }), {\n maxAge: TRANSACTION_COOKIE_MAX_AGE,\n secure: isSecure(c),\n }),\n );\n // A FULL top-level redirect. Never a hidden iframe — see client.ts.\n return c.redirect(start.url, 302);\n });\n\n app.get(\"/callback\", async (c) => {\n const parsed = await parseTransaction(\n readCookie(c.req.header(\"cookie\"), txCookie),\n config.cookieSecret,\n );\n if (!parsed.ok) {\n /**\n * THREE CAUSES, THREE ANSWERS (components-F084.54).\n *\n * THE ORDER IN `parseTransaction` IS LOAD-BEARING AND NOT OBVIOUS:\n * `expired` is reached ONLY AFTER the signature has verified. So\n * `login_expired` is a POSITIVE CONFIRMATION that the HMAC held against\n * the CURRENT secret, and `bad_login_cookie` is the denial of that.\n *\n * WHICH MAKES THE PAIR A ONE-BIT ORACLE, and the justification that stood\n * here was wrong. It said an attacker can produce all three himself, so\n * naming them tells him nothing. That holds only for an attacker who\n * CONSTRUCTED the input and therefore knows what he is holding. The\n * interesting one has a value he FOUND — in a log, on a shared machine, in\n * a referrer leak, in a backup — and he cannot mint a\n * validly-signed-but-expired value himself. For him the answer separates\n * two things he could not otherwise learn: whether a found value is still\n * live, when the secret rotates (the same value changes code the second it\n * does), and whether two environments share a secret.\n *\n * Reported by helpdesk, 2026-09-22, measured against the published 0.3.0\n * dist rather than argued. It is NOT a hole: it does not help him forge\n * anything — one bit per attempt against an HMAC-SHA256 key he never\n * reaches. It is a property the server gives away for free.\n *\n * SO THE TWO AUDIENCES ARE SPLIT (components-F084.55, Christian\n * 2026-10-07: «ét svar» as the default). The app and the log get all\n * three causes through `onCallbackRefused`, always. The browser gets ONE\n * answer for expired + unreadable unless the app opts into \"granular\".\n * `absent` stays its own code: it depends on no secret, so it leaks\n * nothing. Do NOT reorder parseTransaction to hide the oracle instead —\n * signature-before-age is what keeps the operator's two causes apart.\n *\n * The operator half stands unchanged and is the reason the codes exist at\n * all: `bad_login_cookie` on a REAL user's browser is what a rotated\n * SSO_COOKIE_SECRET looks like from the outside.\n */\n const failures = {\n absent: {\n status: 400 as const,\n error: \"no_login_in_progress\",\n message: \"This browser did not start a login here. Begin again from the login page.\",\n },\n expired: {\n status: 400 as const,\n error: \"login_expired\",\n message: `This login took longer than ${TRANSACTION_MAX_AGE} seconds. Begin again from the login page.`,\n },\n unreadable: {\n status: 400 as const,\n error: \"bad_login_cookie\",\n message: \"This browser's login cookie could not be read. Begin again from the login page.\",\n },\n };\n // The operator's channel must never turn a refusal into a 500: a hook that\n // throws is reported, and the browser still gets its answer.\n try {\n await onCallbackRefused(parsed.reason, c);\n } catch (err) {\n console.error(\"[@broberg/sso] onCallbackRefused threw; the refusal stands\", err);\n }\n const shown =\n callbackErrors === \"single\" && parsed.reason !== \"absent\"\n ? {\n status: 400 as const,\n error: \"login_failed\",\n message: \"This login could not be completed. Begin again from the login page.\",\n }\n : failures[parsed.reason];\n const { status, ...body } = shown;\n // Clear it either way: a transaction we refuse must not sit in the browser\n // waiting to be refused again on every retry.\n c.header(\"Set-Cookie\", cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }));\n return c.json(body, status);\n }\n\n const result = await client.completeLogin({\n params: new URL(c.req.url).searchParams,\n state: parsed.tx.state,\n codeVerifier: parsed.tx.codeVerifier,\n nonce: parsed.tx.nonce,\n });\n\n const nowS = Math.floor(Date.now() / 1000);\n const exp = nowS + config.sessionMaxAge;\n // F095.1 — tokens to the server-side store, keyed by a fresh sid; only the\n // sid rides in the cookie.\n let sid: string | undefined;\n if (tokenStore && result.accessToken) {\n sid = newSid();\n await tokenStore.set(sid, {\n sub: result.claims.sub,\n accessToken: result.accessToken,\n ...(result.refreshToken ? { refreshToken: result.refreshToken } : {}),\n expiresAt: expiresAtFrom(result.expiresIn, result.accessToken, nowS),\n });\n }\n const session: SessionPayload = {\n sub: result.claims.sub,\n exp,\n iat: nowS,\n ...(sid ? { sid } : {}),\n ...(result.claims.email ? { email: result.claims.email } : {}),\n ...(result.claims.name ? { name: result.claims.name } : {}),\n ...(result.claims.picture ? { picture: result.claims.picture } : {}),\n // Only when BID actually said so — absent means \"not stated\", never false.\n ...(typeof result.claims.email_verified === \"boolean\" ? { email_verified: result.claims.email_verified } : {}),\n };\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(session, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n );\n // The logout hint. Without it the issuer MUST ask the user to confirm —\n // that is RP-initiated logout per spec, and the plugin says so itself\n // (\"User confirmation is required to complete logout\"). The result was an\n // unstyled English confirmation page with a bare browser button in the\n // middle of our own product.\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, await signValue(result.idToken, config.cookieSecret), {\n maxAge: config.sessionMaxAge,\n secure: isSecure(c),\n }),\n { append: true },\n );\n // Clear the transaction cookie: it has done its job, and a replayed one is\n // only ever useful to someone who should not have it.\n c.header(\n \"Set-Cookie\",\n cookieHeader(txCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n return c.redirect(parsed.tx.returnTo, 302);\n });\n\n app.get(\"/logout\", async (c) => {\n // Read the hint BEFORE clearing, obviously — but note what happens when it\n // is absent: a session minted by 0.1.0 has no such cookie, and that session\n // is still live in every app that upgrades. So a missing hint falls back to\n // the old behaviour (the issuer asks) rather than throwing. The upgrade path\n // is the one that fails in production; it gets its own test.\n const idTokenHint = await verifyValue(\n readCookie(c.req.header(\"cookie\"), idTokenCookie),\n config.cookieSecret,\n );\n // F095.1 — the tokens die with the session. Read the sid before the cookie goes.\n if (tokenStore) {\n const leaving = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n if (leaving?.sid) await tokenStore.delete(leaving.sid);\n }\n\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, \"\", { maxAge: 0, secure: isSecure(c) }),\n );\n c.header(\n \"Set-Cookie\",\n cookieHeader(idTokenCookie, \"\", { maxAge: 0, secure: isSecure(c) }),\n { append: true },\n );\n // F084.152 — the default: this app is logged out, BID is not, and the user\n // lands on BID's login dialog (prompt=login makes BID show it even with a\n // live BID session). Through our own /login, so PKCE and state still apply.\n if ((options.logout ?? \"app\") === \"app\") {\n return c.redirect(`${loginPath}/login?prompt=login`, 302);\n }\n // Local cookies cleared FIRST, then central logout. If the redirect to BID\n // fails or the user closes the tab, the worst case is \"signed out here but\n // not everywhere\" — never the reverse, which would leave this app trusting\n // a session the user believes is gone.\n return c.redirect(await client.logoutUrl(idTokenHint ? { idTokenHint } : {}), 302);\n });\n\n /** Reads the session and puts it on the context. Never blocks. */\n const attach: MiddlewareHandler = async (c, next) => {\n c.set(SESSION_KEY, await liveSession(c));\n await next();\n };\n\n /** Blocks and redirects to login, preserving where the user was going. */\n const require: MiddlewareHandler = async (c, next) => {\n const session = await liveSession(c);\n if (!session) {\n const url = new URL(c.req.url);\n return c.redirect(\n `${loginPath}/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`,\n 302,\n );\n }\n c.set(SESSION_KEY, session);\n await next();\n };\n\n /**\n * F095.1 — the user's access token for calling BID's app API, renewed with the\n * refresh token when it has expired (or is within 30 s of it). Throws SsoError\n * — never returns a stale or empty token — when there is no live session, no\n * stored tokens, or the renewal is refused; a refused renewal also deletes the\n * dead pair so the next call does not try it again.\n */\n const refreshing = new Map<string, Promise<string>>();\n async function getAccessToken(c: Context): Promise<string> {\n if (!tokenStore) {\n throw new SsoError(\"getAccessToken needs ssoRoutes({ tokenStore }) — without one the login's tokens are not kept.\");\n }\n const raw = await verifySession(readCookie(c.req.header(\"cookie\"), config.cookieName), config.cookieSecret);\n const live = await liveSession(c);\n if (!live) {\n if (raw?.sid) await tokenStore.delete(raw.sid);\n throw new SsoReauthError(\"no live session — log in again.\", \"no_session\");\n }\n const sid = live.sid;\n if (!sid) throw new SsoReauthError(\"this session has no stored tokens (it began before tokenStore was configured) — log in again.\", \"no_tokens\");\n const tokens = await tokenStore.get(sid);\n if (!tokens) throw new SsoReauthError(\"no tokens stored for this session — log in again.\", \"no_tokens\");\n const now = Math.floor(Date.now() / 1000);\n if (tokens.expiresAt - 30 > now) return tokens.accessToken;\n if (!tokens.refreshToken) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(\"the access token has expired and there is no refresh token — log in again.\", \"expired\");\n }\n // One renewal per session at a time: with refresh-token rotation, a second\n // parallel renewal would present an already-spent token and be refused.\n const inFlight = refreshing.get(sid);\n if (inFlight) return inFlight;\n const p = (async () => {\n let next;\n try {\n next = await client.refreshTokens(tokens.refreshToken!);\n } catch (e) {\n await tokenStore.delete(sid);\n throw new SsoReauthError(e instanceof SsoError ? e.message : \"token refresh failed\", \"refresh_failed\");\n }\n await tokenStore.set(sid, {\n sub: tokens.sub,\n accessToken: next.accessToken,\n // Rotation: use the new one if BID issued one, else keep the old.\n ...(next.refreshToken ?? tokens.refreshToken ? { refreshToken: next.refreshToken ?? tokens.refreshToken } : {}),\n expiresAt: expiresAtFrom(next.expiresIn, next.accessToken, Math.floor(Date.now() / 1000)),\n });\n return next.accessToken;\n })();\n refreshing.set(sid, p);\n try {\n return await p;\n } finally {\n refreshing.delete(sid);\n }\n }\n\n /**\n * F095.5 — after the user changed her own name or picture, re-sign the\n * session cookie with the new values, so the user menu shows them on the next\n * page load without a new login. Only the convenience copies change: sub,\n * sid, iat and exp are kept as they are (the cookie's lifetime is NOT\n * extended — Max-Age is what is left of it). No live session → nothing.\n */\n async function refreshSessionProfile(c: Context, profile: { name: string | null; picture: string | null }): Promise<void> {\n const live = await liveSession(c);\n if (!live) return;\n const next: SessionPayload = { ...live };\n if (profile.name) next.name = profile.name;\n else delete next.name;\n if (profile.picture) next.picture = profile.picture;\n else delete next.picture;\n if (next.name === live.name && next.picture === live.picture) return;\n const left = live.exp - Math.floor(Date.now() / 1000);\n if (left <= 0) return;\n c.header(\n \"Set-Cookie\",\n cookieHeader(config.cookieName, await signSession(next, config.cookieSecret), { maxAge: left, secure: isSecure(c) }),\n { append: true },\n );\n }\n\n return { app, attach, require, client, config, getAccessToken, refreshSessionProfile };\n}\n\n/** Why a login transaction could not be read back. Three states, three answers. */\nexport type TransactionFailure = \"absent\" | \"expired\" | \"unreadable\";\n\ntype TransactionResult =\n | { ok: true; tx: { state: string; codeVerifier: string; nonce: string; returnTo: string } }\n | { ok: false; reason: TransactionFailure };\n\n/**\n * Reads the login transaction back out of its signed cookie.\n *\n * RETURNS A REASON, NOT `null` (components-F084.54). It used to collapse three\n * causes into one empty answer, and /callback then told the user \"you did not\n * start a login here, OR it expired\" — a sentence that admits in its own wording\n * that it does not know. The word \"or\" was the bug.\n *\n * Age-checked against TRANSACTION_MAX_AGE, not only against the cookie's own\n * Max-Age (components-F084.53) — Max-Age is the browser's promise, and a client\n * can decline to make it. It still does NOT go through verifySession: that\n * envelope carries an `exp`, and the first version of this file used `exp: 0`,\n * which the expiry check rejected every time.\n *\n * NO FUTURE-STAMP GUARD, and that is a deliberate divergence from helpdesk's\n * implementation, which rejects a negative age so a runaway clock cannot widen\n * the window. Ours cannot be widened that way: the stamp is inside the HMAC, so\n * only OUR OWN clock could produce a future one — and refusing it would mean a\n * second app instance whose clock is a few seconds behind starts rejecting\n * perfectly good logins. That trades a theoretical gain for a real outage.\n */\nasync function parseTransaction(raw: string | undefined, secret: string): Promise<TransactionResult> {\n if (raw === undefined) return { ok: false, reason: \"absent\" };\n\n // Signature FIRST, without the age limit, so the two questions stay separate.\n // Asking them together is what produced one answer for three causes: a single\n // `verifyValue(..., { maxAgeSeconds })` returns null for a forged cookie and\n // for an honest late one alike.\n const signed = await verifyValue(raw, secret);\n if (signed === null) return { ok: false, reason: \"unreadable\" };\n\n const fresh = await verifyValue(raw, secret, { maxAgeSeconds: TRANSACTION_MAX_AGE });\n if (fresh === null) return { ok: false, reason: \"expired\" };\n\n try {\n const tx = JSON.parse(fresh) as {\n state: string;\n codeVerifier: string;\n nonce: string;\n returnTo: string;\n };\n // A correctly signed cookie whose contents are not a transaction is not an\n // expiry and not an absence — it is ours and it is wrong.\n if (!tx.state || !tx.codeVerifier || !tx.nonce) return { ok: false, reason: \"unreadable\" };\n return { ok: true, tx };\n } catch {\n return { ok: false, reason: \"unreadable\" };\n }\n}\n\n/** Read the session a middleware attached. */\nexport function getSession(c: Context): SessionPayload | null {\n return (c.get(SESSION_KEY) as SessionPayload | null) ?? null;\n}\n\n/* ── F095.2 — the user's own profile, as an API the app's account page calls ── */\n\n/** What accountRoutes needs from ssoRoutes — pass its return value as is. */\nexport interface AccountRoutesInput {\n client: Pick<SsoClient, \"getProfile\" | \"updateProfile\" | \"uploadAvatar\" | \"removeAvatar\">;\n getAccessToken(c: Context): Promise<string>;\n /**\n * F095.5 — re-sign the session cookie with the profile BID answered, so the\n * user menu shows the new name and picture on the next page load. ssoRoutes\n * provides it; optional so an older or hand-built input still works.\n */\n refreshSessionProfile?(c: Context, profile: { name: string | null; picture: string | null }): Promise<void>;\n}\n\n/**\n * Errors are matched by NAME, not instanceof. The ./hono entry is a separate\n * bundle with its own copy of the error classes, and an app may hand ssoRoutes a\n * client built from the main entry — instanceof would then miss every one of\n * them and a 403 insufficient_scope would come out as a 500.\n */\ntype Named = Error & { reason?: string; scope?: string; status?: number | null; code?: string };\nfunction errName(e: unknown): string | undefined {\n return e instanceof Error ? e.name : undefined;\n}\n\n/**\n * Read a request body, but stop at `max` bytes — Content-Length is the client's\n * claim, and a chunked upload has none. Returns null when the body is larger.\n */\nasync function readCapped(c: Context, max: number): Promise<Uint8Array | null> {\n const stream = c.req.raw.body;\n if (!stream) return new Uint8Array(0);\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > max) {\n await reader.cancel().catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const ch of chunks) {\n out.set(ch, at);\n at += ch.byteLength;\n }\n return out;\n}\n\n/**\n * The signed-in user's own name and picture, for the app's account page.\n * Mount it on /api/account:\n *\n * const sso = ssoRoutes({ tokenStore });\n * app.route(\"/api/account\", accountRoutes(sso));\n *\n * GET /profile · POST /profile {name} · POST /profile/avatar (raw image body) ·\n * POST /profile/avatar/remove. Every answer is JSON, refusals included — it is\n * an API, so never a login redirect:\n * 401 {error:\"unauthenticated\"} no live session\n * 401 {error:\"reauth\"} the session's tokens are spent — log in again\n * 403 {error:\"insufficient_scope\", scope} log in again with that scope requested\n * 413 / 415 picture too large / not PNG, JPEG or WebP\n * 4xx {error:<BID's code>} BID refused the change (e.g. an invalid name)\n * 502 {error:\"bid_unavailable\"} BID did not answer usably\n */\nexport function accountRoutes(sso: AccountRoutesInput) {\n const app = new Hono();\n const { client } = sso;\n\n // Personal data: never cached by a browser or a proxy.\n app.use(\"*\", async (c, next) => {\n c.header(\"Cache-Control\", \"no-store\");\n await next();\n });\n\n // A write from ANOTHER site is refused. SameSite=Lax stops a foreign domain,\n // but not a sibling subdomain (same site), and /profile/avatar/remove needs\n // no body at all — a plain <form> would do. Browsers send Sec-Fetch-Site on\n // every request; a non-browser caller sends none and is let through.\n app.use(\"*\", async (c, next) => {\n const site = c.req.header(\"sec-fetch-site\");\n if (c.req.method !== \"GET\" && site !== undefined && site !== \"same-origin\" && site !== \"none\") {\n return c.json({ error: \"cross_site\" }, 403);\n }\n await next();\n });\n\n // Every answer carries the profile AS BID NOW HOLDS IT; the session cookie\n // follows it (F095.5), so the user menu is right after a reload too — also\n // when the name was changed in BID itself.\n const answer = async (c: Context, profile: BidProfile) => {\n if (sso.refreshSessionProfile) await sso.refreshSessionProfile(c, profile);\n return c.json(profile);\n };\n\n // The session is checked FIRST on every route, before any body is looked at.\n app.get(\"/profile\", async (c) => answer(c, await client.getProfile(await sso.getAccessToken(c))));\n\n app.post(\"/profile\", async (c) => {\n const token = await sso.getAccessToken(c);\n const body = (await c.req.json().catch(() => null)) as { name?: unknown } | null;\n if (typeof body?.name !== \"string\") {\n return c.json({ error: \"invalid_request\", error_description: '\"name\" must be a string' }, 400);\n }\n return answer(c, await client.updateProfile(token, body.name));\n });\n\n app.post(\"/profile/avatar\", async (c) => {\n const token = await sso.getAccessToken(c);\n const type = mediaType(c.req.header(\"content-type\"));\n if (!AVATAR_TYPES.includes(type)) {\n return c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n }\n // Early: a declared size over the limit is refused without reading a byte.\n const declared = Number(c.req.header(\"content-length\"));\n if (Number.isFinite(declared) && declared > MAX_AVATAR_BYTES) {\n return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n }\n // And a body without (or lying about) its length is cut off at the limit.\n const bytes = await readCapped(c, MAX_AVATAR_BYTES);\n if (!bytes) return c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413);\n return answer(c, await client.uploadAvatar(token, bytes, type));\n });\n\n app.post(\"/profile/avatar/remove\", async (c) => answer(c, await client.removeAvatar(await sso.getAccessToken(c))));\n\n app.onError((err, c) => {\n c.header(\"Cache-Control\", \"no-store\");\n const e = err as Named;\n switch (errName(err)) {\n case \"SsoReauthError\":\n return c.json({ error: e.reason === \"no_session\" ? \"unauthenticated\" : \"reauth\" }, 401);\n case \"SsoInsufficientScopeError\":\n return c.json({ error: \"insufficient_scope\", scope: e.scope }, 403);\n case \"SsoAvatarRejectedError\":\n return e.reason === \"too_large\"\n ? c.json({ error: \"too_large\", max_bytes: MAX_AVATAR_BYTES }, 413)\n : c.json({ error: \"unsupported_type\", accepted: AVATAR_TYPES }, 415);\n case \"SsoAppApiError\": {\n // BID no longer accepts the token (revoked, or the session ended in BID) → log in again.\n if (e.status === 401) return c.json({ error: \"reauth\" }, 401);\n if (typeof e.status === \"number\" && e.status >= 400 && e.status < 500) {\n return c.json({ error: e.code ?? \"bid_refused\" }, e.status as 400);\n }\n return c.json({ error: \"bid_unavailable\" }, 502);\n }\n // A plain SsoError here is a setup mistake (e.g. ssoRoutes without tokenStore).\n case \"SsoError\":\n return c.json({ error: \"sso_misconfigured\" }, 500);\n default:\n return c.json({ error: \"server_error\" }, 500);\n }\n });\n\n return app;\n}\n"]}
|