@broberg/sso 0.11.0 → 0.12.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/dist/hono.d.cts CHANGED
@@ -439,7 +439,28 @@ interface SsoRoutesOptions {
439
439
  * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.
440
440
  */
441
441
  tokenStore?: TokenStore;
442
+ /**
443
+ * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).
444
+ * "single" (default): an expired login and an unreadable cookie get the SAME
445
+ * answer, `login_failed`. The two differ only in whether the cookie's
446
+ * signature held against the current secret, and a stranger holding a
447
+ * FOUND cookie must not be able to ask that.
448
+ * "granular": the 0.3.0–0.11.0 behaviour — `login_expired` and
449
+ * `bad_login_cookie` told apart. For an app that knows its threat model
450
+ * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.
451
+ * `no_login_in_progress` is its own answer either way: it depends on no secret.
452
+ */
453
+ callbackErrors?: "single" | "granular";
454
+ /**
455
+ * Receives the PRECISE cause of every refused callback, whatever the browser
456
+ * was told — this is the operator's channel. `unreadable` on a real user's
457
+ * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.
458
+ * Without it, the cause is written with console.warn so the log still has it.
459
+ */
460
+ onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;
442
461
  }
462
+ /** Why /callback refused: no login started here · too old · signature did not hold. */
463
+ type CallbackRefusal = "absent" | "expired" | "unreadable";
443
464
  declare function ssoRoutes(options?: SsoRoutesOptions): {
444
465
  app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
445
466
  attach: MiddlewareHandler;
@@ -489,4 +510,4 @@ interface AccountRoutesInput {
489
510
  */
490
511
  declare function accountRoutes(sso: AccountRoutesInput): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
491
512
 
492
- export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
513
+ export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type CallbackRefusal, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
package/dist/hono.d.ts CHANGED
@@ -439,7 +439,28 @@ interface SsoRoutesOptions {
439
439
  * Without it, login behaves exactly as in 0.8.0 and the tokens are dropped.
440
440
  */
441
441
  tokenStore?: TokenStore;
442
+ /**
443
+ * What the BROWSER is told when /callback refuses a login (F084.55, 0.12.0).
444
+ * "single" (default): an expired login and an unreadable cookie get the SAME
445
+ * answer, `login_failed`. The two differ only in whether the cookie's
446
+ * signature held against the current secret, and a stranger holding a
447
+ * FOUND cookie must not be able to ask that.
448
+ * "granular": the 0.3.0–0.11.0 behaviour — `login_expired` and
449
+ * `bad_login_cookie` told apart. For an app that knows its threat model
450
+ * (e.g. internal, behind a VPN) and wants «dit login udløb» in the browser.
451
+ * `no_login_in_progress` is its own answer either way: it depends on no secret.
452
+ */
453
+ callbackErrors?: "single" | "granular";
454
+ /**
455
+ * Receives the PRECISE cause of every refused callback, whatever the browser
456
+ * was told — this is the operator's channel. `unreadable` on a real user's
457
+ * browser is what a rotated SSO_COOKIE_SECRET looks like from outside.
458
+ * Without it, the cause is written with console.warn so the log still has it.
459
+ */
460
+ onCallbackRefused?: (cause: CallbackRefusal, c: Context) => void | Promise<void>;
442
461
  }
462
+ /** Why /callback refused: no login started here · too old · signature did not hold. */
463
+ type CallbackRefusal = "absent" | "expired" | "unreadable";
443
464
  declare function ssoRoutes(options?: SsoRoutesOptions): {
444
465
  app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
445
466
  attach: MiddlewareHandler;
@@ -489,4 +510,4 @@ interface AccountRoutesInput {
489
510
  */
490
511
  declare function accountRoutes(sso: AccountRoutesInput): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
491
512
 
492
- export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
513
+ export { type AccountRoutesInput, type BackchannelOptions, type BackchannelStore, type CallbackRefusal, type SsoRoutesOptions, type TransactionFailure, accountRoutes, getSession, ssoRoutes };
package/dist/hono.js CHANGED
@@ -819,6 +819,8 @@ function ssoRoutes(options = {}) {
819
819
  }
820
820
  const onStoreError = backchannel?.onStoreError ?? "reject";
821
821
  const tokenStore = options.tokenStore;
822
+ const callbackErrors = options.callbackErrors ?? "single";
823
+ const onCallbackRefused = options.onCallbackRefused ?? ((cause) => console.warn(`[@broberg/sso] /callback refused: ${cause}`));
822
824
  async function liveSession(c) {
823
825
  const session = await verifySession(readCookie(c.req.header("cookie"), config.cookieName), config.cookieSecret);
824
826
  if (!session || !backchannel) return session;
@@ -905,7 +907,13 @@ function ssoRoutes(options = {}) {
905
907
  message: "This browser's login cookie could not be read. Begin again from the login page."
906
908
  }
907
909
  };
908
- const { status, ...body } = failures[parsed.reason];
910
+ await onCallbackRefused(parsed.reason, c);
911
+ const shown = callbackErrors === "single" && parsed.reason !== "absent" ? {
912
+ status: 400,
913
+ error: "login_failed",
914
+ message: "This login could not be completed. Begin again from the login page."
915
+ } : failures[parsed.reason];
916
+ const { status, ...body } = shown;
909
917
  c.header("Set-Cookie", cookieHeader(txCookie, "", { maxAge: 0, secure: isSecure(c) }));
910
918
  return c.json(body, status);
911
919
  }