@broberg/sso 0.11.0 → 0.12.1
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 +42 -2
- package/dist/hono.cjs +13 -1
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +22 -1
- package/dist/hono.d.ts +22 -1
- package/dist/hono.js +13 -1
- package/dist/hono.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -204,8 +204,9 @@ run 3× in production; this package's own Hono adapter now does the same, after
|
|
|
204
204
|
answering every failed callback with one message containing the word *"or"*.
|
|
205
205
|
|
|
206
206
|
**Mount the Hono adapter on 0.3.0 or later and you get all of this for free** —
|
|
207
|
-
the signed window, the longer cookie, and three distinct
|
|
208
|
-
(`
|
|
207
|
+
the signed window, the longer cookie, and three distinct causes
|
|
208
|
+
(`expired` · `absent` · `unreadable`). **Since 0.12.0 the browser no longer sees
|
|
209
|
+
all three by default**: see *What a refused login tells the browser* below. **On 0.2.3–0.2.5
|
|
209
210
|
the adapter had the signed window but still used ONE number for both**, so it
|
|
210
211
|
answered every failed callback with a single message containing the word *"or"*.
|
|
211
212
|
Take the core and nothing is passed on your behalf, on any version. Reported by broberg-id, who found their own framework-free
|
|
@@ -484,3 +485,42 @@ picks up a change the user made in BID itself.
|
|
|
484
485
|
|
|
485
486
|
Mail, password, passkeys, two-factor and sessions stay in BID: link to the
|
|
486
487
|
profile's `account_url` in a new tab.
|
|
488
|
+
|
|
489
|
+
## What a refused login tells the browser (changed in 0.12.0)
|
|
490
|
+
|
|
491
|
+
**Changed default: the browser now gets ONE answer, `login_failed`, for an expired
|
|
492
|
+
login AND an unreadable cookie.** Through 0.11.0 it got `login_expired` and
|
|
493
|
+
`bad_login_cookie` separately. `no_login_in_progress` is unchanged.
|
|
494
|
+
|
|
495
|
+
| cause | your app / log (`onCallbackRefused`) | browser, default `"single"` | browser, `"granular"` |
|
|
496
|
+
|---|---|---|---|
|
|
497
|
+
| no login started here | `absent` | `no_login_in_progress` | `no_login_in_progress` |
|
|
498
|
+
| took too long | `expired` | `login_failed` | `login_expired` |
|
|
499
|
+
| signature did not hold | `unreadable` | `login_failed` | `bad_login_cookie` |
|
|
500
|
+
|
|
501
|
+
**Why.** The order in `parseTransaction` is what makes this matter, and it is not
|
|
502
|
+
obvious until you read the function: the signature is checked FIRST, and the age
|
|
503
|
+
only after it held. So `login_expired` is a positive confirmation that the
|
|
504
|
+
cookie's HMAC held against the CURRENT secret, and `bad_login_cookie` denies it.
|
|
505
|
+
Someone holding a cookie they FOUND (a log, a shared machine, a backup) could ask
|
|
506
|
+
whether it is still live, see the moment the secret rotates, and see whether two
|
|
507
|
+
environments share a secret. It does not help anyone forge anything. It is a
|
|
508
|
+
fact the server gave away for free. Reported by helpdesk, 22 September 2026.
|
|
509
|
+
|
|
510
|
+
**The operator still gets all three.** `onCallbackRefused(cause, c)` receives the
|
|
511
|
+
precise cause on every refusal; without it the adapter writes
|
|
512
|
+
`[@broberg/sso] /callback refused: <cause>` with `console.warn`. Watch for
|
|
513
|
+
`unreadable` on real users: that is what a rotated `SSO_COOKIE_SECRET` looks like. A hook that throws is reported with `console.error` and the refusal still stands (0.12.1); it never becomes a 500.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
ssoRoutes({
|
|
517
|
+
onCallbackRefused: (cause) => log.warn({ cause }, "sso callback refused"),
|
|
518
|
+
// callbackErrors: "granular", // opt in: «dit login udløb» in the browser
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**What the opt-in costs.** `callbackErrors: "granular"` gives the browser the
|
|
523
|
+
three codes back, so it can say *"your login expired, try again"*. The price is
|
|
524
|
+
the one bit above, for anyone with a found cookie. Fine for an internal app behind
|
|
525
|
+
a VPN; think twice on the open internet.
|
|
526
|
+
|
package/dist/hono.cjs
CHANGED
|
@@ -821,6 +821,8 @@ function ssoRoutes(options = {}) {
|
|
|
821
821
|
}
|
|
822
822
|
const onStoreError = backchannel?.onStoreError ?? "reject";
|
|
823
823
|
const tokenStore = options.tokenStore;
|
|
824
|
+
const callbackErrors = options.callbackErrors ?? "single";
|
|
825
|
+
const onCallbackRefused = options.onCallbackRefused ?? ((cause) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));
|
|
824
826
|
async function liveSession(c) {
|
|
825
827
|
const session = await verifySession(readCookie(c.req.header("cookie"), config.cookieName), config.cookieSecret);
|
|
826
828
|
if (!session || !backchannel) return session;
|
|
@@ -907,7 +909,17 @@ function ssoRoutes(options = {}) {
|
|
|
907
909
|
message: "This browser's login cookie could not be read. Begin again from the login page."
|
|
908
910
|
}
|
|
909
911
|
};
|
|
910
|
-
|
|
912
|
+
try {
|
|
913
|
+
await onCallbackRefused(parsed.reason, c);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
console.error("[@broberg/sso] onCallbackRefused threw; the refusal stands", err);
|
|
916
|
+
}
|
|
917
|
+
const shown = callbackErrors === "single" && parsed.reason !== "absent" ? {
|
|
918
|
+
status: 400,
|
|
919
|
+
error: "login_failed",
|
|
920
|
+
message: "This login could not be completed. Begin again from the login page."
|
|
921
|
+
} : failures[parsed.reason];
|
|
922
|
+
const { status, ...body } = shown;
|
|
911
923
|
c.header("Set-Cookie", cookieHeader(txCookie, "", { maxAge: 0, secure: isSecure(c) }));
|
|
912
924
|
return c.json(body, status);
|
|
913
925
|
}
|