@samebase/identity-broker 0.2.0 → 0.4.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.
Files changed (41) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/index.js +6 -7
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/rotate.d.ts +10 -0
  5. package/dist/cli/rotate.js +25 -0
  6. package/dist/cli/rotate.js.map +1 -0
  7. package/dist/client/connectors/google.d.ts +5 -1
  8. package/dist/client/connectors/google.js +6 -1
  9. package/dist/client/connectors/google.js.map +1 -1
  10. package/dist/client/connectors/types.d.ts +5 -0
  11. package/dist/client/connectors/upstream.d.ts +9 -1
  12. package/dist/client/connectors/upstream.js +9 -2
  13. package/dist/client/connectors/upstream.js.map +1 -1
  14. package/dist/client/html.js +3 -0
  15. package/dist/client/html.js.map +1 -1
  16. package/dist/client/index.d.ts +7 -2
  17. package/dist/client/index.js +117 -29
  18. package/dist/client/index.js.map +1 -1
  19. package/dist/client/jwt.d.ts +8 -0
  20. package/dist/client/jwt.js +51 -1
  21. package/dist/client/jwt.js.map +1 -1
  22. package/dist/client/keys.d.ts +11 -3
  23. package/dist/client/keys.js +68 -20
  24. package/dist/client/keys.js.map +1 -1
  25. package/dist/client/protocol.d.ts +2 -1
  26. package/dist/client/protocol.js +11 -3
  27. package/dist/client/protocol.js.map +1 -1
  28. package/dist/client/responses.d.ts +6 -0
  29. package/dist/client/responses.js +6 -1
  30. package/dist/client/responses.js.map +1 -1
  31. package/dist/component/_generated/component.d.ts +6 -1
  32. package/dist/component/authRequests.d.ts +8 -1
  33. package/dist/component/authRequests.js +20 -5
  34. package/dist/component/authRequests.js.map +1 -1
  35. package/dist/component/lifecycle.d.ts +14 -6
  36. package/dist/component/lifecycle.js +109 -67
  37. package/dist/component/lifecycle.js.map +1 -1
  38. package/dist/component/schema.d.ts +34 -21
  39. package/dist/component/schema.js +7 -3
  40. package/dist/component/schema.js.map +1 -1
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -81,7 +81,8 @@ export const {
81
81
 
82
82
  Keys: `npx identity-broker keys` prints the value for `IDENTITY_BROKER_KEYS`; `keys --rotate`
83
83
  advances it, and `keys --rotate --no-overlap` replaces a compromised key at once. Back up the key
84
- set: its salt makes every `sub`. `parseKeySet` checks a value before a deploy.
84
+ set: its salt makes every `sub`. `parseKeySet` checks a value's shape before a deploy, and
85
+ `assertKeySetUsable` signs a test token with it and verifies that token against its JWKS.
85
86
 
86
87
  ## Register an app's deployments
87
88
 
package/dist/cli/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { createKeySet, parseKeySet, rotateKeySet, rotateKeySetWithoutOverlap, } from "../client/keys.js";
2
+ import { createKeySet } from "../client/keys.js";
3
+ import { rotateKeys } from "./rotate.js";
3
4
  const USAGE = `identity-broker keys print a new key set for IDENTITY_BROKER_KEYS
4
- identity-broker keys --rotate read the current key set on stdin, print the rotated
5
- one; the retired public key stays for 14 days
5
+ identity-broker keys --rotate read the current key set on stdin, print the
6
+ rotated one once it signs and verifies; the
7
+ retired public key stays for 14 days
6
8
  identity-broker keys --rotate --no-overlap the same after a key compromise: retired keys are
7
9
  dropped at once; the salt is kept`;
8
10
  async function readStdin() {
@@ -25,10 +27,7 @@ async function main(argv) {
25
27
  return 2;
26
28
  }
27
29
  if (rotate) {
28
- const current = parseKeySet(await readStdin());
29
- const rotated = noOverlap
30
- ? await rotateKeySetWithoutOverlap(current)
31
- : await rotateKeySet(current);
30
+ const rotated = await rotateKeys(await readStdin(), { overlap: !noOverlap });
32
31
  process.stdout.write(`${JSON.stringify(rotated)}\n`);
33
32
  return 0;
34
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EACL,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,KAAK,GAAG;;;;+EAIiE,CAAC;AAEhF,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAChD,+FAA+F;IAC/F,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,SAAS;YACvB,CAAC,CAAC,MAAM,0BAA0B,CAAC,OAAO,CAAC;YAC3C,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;IAClE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,KAAK,GAAG;;;;;+EAKiE,CAAC;AAEhF,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;IAChD,+FAA+F;IAC/F,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;QACzB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,MAAM,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;QAC7E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;IAClE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type KeySet } from "../client/keys.js";
2
+ /**
3
+ * `identity-broker keys --rotate`. The input is only parsed, so a key set whose current key cannot
4
+ * sign, such as RSA under 2048 bits, can still be rotated out; the output must be usable. Its new
5
+ * current key is fresh, so with overlap a failure comes from a key carried over as retired, which
6
+ * only `--no-overlap` drops.
7
+ */
8
+ export declare function rotateKeys(raw: string, options: {
9
+ overlap: boolean;
10
+ }): Promise<KeySet>;
@@ -0,0 +1,25 @@
1
+ import { assertKeySetUsable } from "../client/jwt.js";
2
+ import { parseKeySet, rotateKeySet, rotateKeySetWithoutOverlap, } from "../client/keys.js";
3
+ /**
4
+ * `identity-broker keys --rotate`. The input is only parsed, so a key set whose current key cannot
5
+ * sign, such as RSA under 2048 bits, can still be rotated out; the output must be usable. Its new
6
+ * current key is fresh, so with overlap a failure comes from a key carried over as retired, which
7
+ * only `--no-overlap` drops.
8
+ */
9
+ export async function rotateKeys(raw, options) {
10
+ const input = parseKeySet(raw);
11
+ if (!options.overlap) {
12
+ const rotated = await rotateKeySetWithoutOverlap(input);
13
+ await assertKeySetUsable(rotated);
14
+ return rotated;
15
+ }
16
+ const rotated = await rotateKeySet(input);
17
+ try {
18
+ await assertKeySetUsable(rotated);
19
+ }
20
+ catch (error) {
21
+ throw new Error(`${error instanceof Error ? error.message : String(error)}. That key is carried over as retired; rotate with --rotate --no-overlap to drop the retired keys at once.`, { cause: error });
22
+ }
23
+ return rotated;
24
+ }
25
+ //# sourceMappingURL=rotate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotate.js","sourceRoot":"","sources":["../../src/cli/rotate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EACL,WAAW,EACX,YAAY,EACZ,0BAA0B,GAE3B,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAW,EAAE,OAA6B;IACzE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,OAAO,GAAG,MAAM,0BAA0B,CAAC,KAAK,CAAC,CAAC;QACxD,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,4GAA4G,EACrK,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -1,5 +1,9 @@
1
1
  import type { OidcConnector } from "./types.js";
2
- /** Google as an OIDC connector: discovery, id_token claims, `prompt=login` for fresh logins. */
2
+ /**
3
+ * Google as an OIDC connector: discovery and id_token claims. Google documents no `login` value
4
+ * for `prompt`, only `none`, `consent`, and `select_account`, so a fresh login through it answers
5
+ * `login_required`.
6
+ */
3
7
  export declare function google(credentials: {
4
8
  clientId: () => string;
5
9
  clientSecret: () => string;
@@ -1,4 +1,8 @@
1
- /** Google as an OIDC connector: discovery, id_token claims, `prompt=login` for fresh logins. */
1
+ /**
2
+ * Google as an OIDC connector: discovery and id_token claims. Google documents no `login` value
3
+ * for `prompt`, only `none`, `consent`, and `select_account`, so a fresh login through it answers
4
+ * `login_required`.
5
+ */
2
6
  export function google(credentials) {
3
7
  return {
4
8
  kind: "oidc",
@@ -6,6 +10,7 @@ export function google(credentials) {
6
10
  label: "Google",
7
11
  issuer: "https://accounts.google.com",
8
12
  scopes: ["openid", "email", "profile"],
13
+ promptLogin: false,
9
14
  clientId: credentials.clientId,
10
15
  clientSecret: credentials.clientSecret,
11
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"google.js","sourceRoot":"","sources":["../../../src/client/connectors/google.ts"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,MAAM,UAAU,MAAM,CAAC,WAItB;IACC,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,WAAW,CAAC,IAAI,IAAI,QAAQ;QAClC,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,6BAA6B;QACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;QACtC,QAAQ,EAAE,WAAW,CAAC,QAAQ;QAC9B,YAAY,EAAE,WAAW,CAAC,YAAY;KACvC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"google.js","sourceRoot":"","sources":["../../../src/client/connectors/google.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,WAItB;IACC,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,WAAW,CAAC,IAAI,IAAI,QAAQ;QAClC,KAAK,EAAE,QAAQ;QACf,MAAM,EAAE,6BAA6B;QACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;QACtC,WAAW,EAAE,KAAK;QAClB,QAAQ,EAAE,WAAW,CAAC,QAAQ;QAC9B,YAAY,EAAE,WAAW,CAAC,YAAY;KACvC,CAAC;AACJ,CAAC"}
@@ -30,6 +30,11 @@ export type OidcConnector = Credentials & {
30
30
  label: string;
31
31
  issuer: string;
32
32
  scopes: string[];
33
+ /**
34
+ * The provider documents `prompt=login`, so a fresh login (`prompt=login` or `max_age`) is sent
35
+ * upstream as that. Without it, a fresh login answers `login_required`.
36
+ */
37
+ promptLogin: boolean;
33
38
  };
34
39
  /** The host action that delivers a code. The broker never sees how. */
35
40
  export type SendEmailCode = FunctionReference<"action", "internal", {
@@ -11,6 +11,14 @@ export declare function startUpstream(args: {
11
11
  callbackUrl: string;
12
12
  freshLogin: boolean;
13
13
  }): Promise<UpstreamStart>;
14
+ export type UpstreamFinish = {
15
+ identity: UpstreamIdentity;
16
+ /**
17
+ * The id_token's `auth_time`, in seconds: when the provider last authenticated the user. A plain
18
+ * OAuth connector has no id_token and an OIDC provider may omit the claim, so it can be absent.
19
+ */
20
+ authTimeSeconds: number | undefined;
21
+ };
14
22
  /**
15
23
  * The connector callback: validate the response, exchange the code, and read the identity. Errors
16
24
  * propagate; the caller turns them into a redirect back to the client.
@@ -22,4 +30,4 @@ export declare function finishUpstream(args: {
22
30
  state: string;
23
31
  codeVerifier: string;
24
32
  nonce: string | undefined;
25
- }): Promise<UpstreamIdentity>;
33
+ }): Promise<UpstreamFinish>;
@@ -42,7 +42,10 @@ export async function startUpstream(args) {
42
42
  url.searchParams.set("nonce", nonce);
43
43
  }
44
44
  if (args.freshLogin && args.connector.kind === "oidc") {
45
+ // With max_age the provider must return auth_time (OIDC Core 3.1.2.1), which the callback
46
+ // checks; prompt=login asks for the login itself.
45
47
  url.searchParams.set("prompt", "login");
48
+ url.searchParams.set("max_age", "0");
46
49
  }
47
50
  return { url: url.toString(), state, codeVerifier, nonce };
48
51
  }
@@ -58,7 +61,10 @@ export async function finishUpstream(args) {
58
61
  const response = await oauth.authorizationCodeGrantRequest(as, client, clientAuth, params, args.callbackUrl, args.codeVerifier);
59
62
  if (args.connector.kind === "oauth") {
60
63
  const tokens = await oauth.processAuthorizationCodeResponse(as, client, response);
61
- return await args.connector.identity(tokens.access_token);
64
+ return {
65
+ identity: await args.connector.identity(tokens.access_token),
66
+ authTimeSeconds: undefined,
67
+ };
62
68
  }
63
69
  const tokens = await oauth.processAuthorizationCodeResponse(as, client, response, {
64
70
  requireIdToken: true,
@@ -81,6 +87,7 @@ export async function finishUpstream(args) {
81
87
  if (typeof claims["picture"] === "string") {
82
88
  identity.picture = claims["picture"];
83
89
  }
84
- return identity;
90
+ // oauth4webapi rejects an id_token whose `auth_time` is present and not a non-negative number.
91
+ return { identity, authTimeSeconds: claims.auth_time };
85
92
  }
86
93
  //# sourceMappingURL=upstream.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"upstream.js","sourceRoot":"","sources":["../../../src/client/connectors/upstream.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"upstream.js","sourceRoot":"","sources":["../../../src/client/connectors/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,MAAM,UAAU,GAAG,IAAI,GAAG,EAA8C,CAAC;AAEzE,mDAAmD;AACnD,SAAS,mBAAmB,CAC1B,SAAyC;IAEzC,IAAI,SAAS,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,MAAM,EAAE,SAAS,CAAC,qBAAqB;YACvC,sBAAsB,EAAE,SAAS,CAAC,qBAAqB;YACvD,cAAc,EAAE,SAAS,CAAC,aAAa;SACxC,CAAC,CAAC;IACL,CAAC;IACD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK;SAClB,gBAAgB,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;SAC/C,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,wBAAwB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACxE,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,OAAO,OAAO,CAAC;AACjB,CAAC;AASD,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAInC;IACC,MAAM,EAAE,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,IAAI,EAAE,CAAC,sBAAsB,KAAK,SAAS,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,IAAI,iCAAiC,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,EAAE,CAAC;IAC1C,MAAM,YAAY,GAAG,KAAK,CAAC,0BAA0B,EAAE,CAAC;IACxD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACvF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,sBAAsB,CAAC,CAAC;IAC/C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC7D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACvD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,KAAK,CAAC,0BAA0B,CAAC,YAAY,CAAC,CAAC,CAAC;IAC7F,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;IACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtD,0FAA0F;QAC1F,kDAAkD;QAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACxC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;AAC7D,CAAC;AAWD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAOpC;IACC,MAAM,EAAE,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,MAAM,MAAM,GAAiB,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,CAAC;IACtE,MAAM,UAAU,GAAG,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC,CAAC;IACzE,MAAM,MAAM,GAAG,KAAK,CAAC,oBAAoB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACnF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,6BAA6B,CACxD,EAAE,EACF,MAAM,EACN,UAAU,EACV,MAAM,EACN,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,YAAY,CAClB,CAAC;IACF,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,gCAAgC,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QAClF,OAAO;YACL,QAAQ,EAAE,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;YAC5D,eAAe,EAAE,SAAS;SAC3B,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,gCAAgC,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;QAChF,cAAc,EAAE,IAAI;QACpB,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;KACnE,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,KAAK,CAAC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,SAAS,CAAC,IAAI,wBAAwB,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,QAAQ,GAAqB,EAAE,UAAU,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;IAC9D,IAAI,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QACxC,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,gBAAgB,CAAC,KAAK,SAAS,EAAE,CAAC;QAClD,QAAQ,CAAC,aAAa,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;QACvC,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1C,QAAQ,CAAC,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACvC,CAAC;IACD,+FAA+F;IAC/F,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;AACzD,CAAC"}
@@ -38,6 +38,9 @@ export function page(title, body, status = 200) {
38
38
  "Content-Type": "text/html; charset=utf-8",
39
39
  "Cache-Control": "no-store",
40
40
  "Referrer-Policy": "no-referrer",
41
+ // No page of the broker may be framed: the chooser doubles as consent.
42
+ "Content-Security-Policy": "frame-ancestors 'none'",
43
+ "X-Frame-Options": "DENY",
41
44
  },
42
45
  });
43
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/client/html.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,KAAK,GAAG;;;;;;;;;;;;;CAab,CAAC;AAEF,MAAM,UAAU,IAAI,CAAC,KAAa,EAAE,IAAY,EAAE,MAAM,GAAG,GAAG;IAC5D,MAAM,IAAI,GAAG;;;;;;SAMN,MAAM,CAAC,KAAK,CAAC;SACb,KAAK;;cAEA,IAAI;QACV,CAAC;IACP,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,MAAM;QACN,OAAO,EAAE;YACP,cAAc,EAAE,0BAA0B;YAC1C,eAAe,EAAE,UAAU;YAC3B,iBAAiB,EAAE,aAAa;SACjC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAO3B;IACC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU;SAC5B,GAAG,CACF,CAAC,SAAS,EAAE,EAAE,CACZ,2BAA2B,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,mBAAmB,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CACpG;SACA,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,QAAQ,GAAG;QACf,aAAa;QACb,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACrE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAC;IACF,MAAM,IAAI,GACR,QAAQ,CAAC,MAAM,IAAI,CAAC;QAClB,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;QACxB,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IAChF,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU;QAC7B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,oBAAoB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,6DAA6D,IAAI,OAAO,CAAC;IACrH,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU;QAC1B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,eAAe,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,0BAA0B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;IACjG,OAAO,IAAI,CACT,cAAc,IAAI,CAAC,OAAO,EAAE,EAC5B,kBAAkB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,OAAO,qBAAqB,OAAO,SAAS,IAAI,EAAE,CACjG,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAMhC;IACC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3F,OAAO,IAAI,CACT,cAAc,IAAI,CAAC,OAAO,EAAE,EAC5B,kBAAkB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,wEAAwE,KAAK;8BACzF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;6CACJ,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;;;;QAI3D,EACJ,IAAI,CAAC,MAAM,CACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAO7B;IACC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3F,OAAO,IAAI,CACT,cAAc,IAAI,CAAC,OAAO,EAAE,EAC5B,gEAAgE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,KAAK;8BACrE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;6CACJ,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;;;;QAI3D,EACJ,IAAI,CAAC,MAAM,CACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,OAAe,EAAE,MAAM,GAAG,GAAG;IACpE,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,MAAM,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACnF,CAAC"}
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/client/html.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@ export { github } from "./connectors/github.js";
6
6
  export { google } from "./connectors/google.js";
7
7
  export { email } from "./connectors/email.js";
8
8
  export { createKeySet, parseKeySet, rotateKeySet, type KeySet } from "./keys.js";
9
+ export { assertKeySetUsable } from "./jwt.js";
9
10
  export type IdentityBrokerConfig = {
10
11
  /** `components.identityBroker` of the host app. */
11
12
  component: ComponentApi;
@@ -14,9 +15,13 @@ export type IdentityBrokerConfig = {
14
15
  /** The content of `IDENTITY_BROKER_KEYS`, read in the request. */
15
16
  keys: () => string | undefined;
16
17
  connectors: Connector[];
18
+ /**
19
+ * Days without a sign-in after which `cleanup` deletes a preview client. Absent: `cleanup` deletes
20
+ * no client, because a deployment whose client was deleted does not register again by itself.
21
+ */
22
+ previewClientIdleDays?: number;
17
23
  };
18
- /** Lifecycle values the host's cron passes to `cleanup`. */
19
- export declare const PREVIEW_CLIENT_MAX_AGE_MS: number;
24
+ /** How long `cleanup` keeps an audit row of an issued id_token. */
20
25
  export declare const SIGN_IN_RETENTION_MS: number;
21
26
  export declare function defineIdentityBroker(config: IdentityBrokerConfig): {
22
27
  /** Mounts every endpoint on the host's router. */
@@ -12,17 +12,19 @@ export { github } from "./connectors/github.js";
12
12
  export { google } from "./connectors/google.js";
13
13
  export { email } from "./connectors/email.js";
14
14
  export { createKeySet, parseKeySet, rotateKeySet } from "./keys.js";
15
+ export { assertKeySetUsable } from "./jwt.js";
15
16
  /** A request lives this long from /authorize to the code redirect. */
16
17
  const AUTH_REQUEST_TTL_MS = 10 * 60 * 1000;
17
18
  /** A code is redeemed by the client's callback at once. */
18
19
  const CODE_TTL_MS = 60 * 1000;
19
20
  const EMAIL_CODE_TTL_MS = 10 * 60 * 1000;
21
+ /** How far an upstream `auth_time` may sit outside the request's lifetime: the clocks differ. */
22
+ const AUTH_TIME_SKEW_SECONDS = 60;
20
23
  const EMAIL_MAX_ATTEMPTS = 5;
21
24
  /** The scopes the broker grants; any other requested scope is dropped. */
22
25
  const SCOPES = ["openid", "email", "profile"];
23
26
  const REGISTRATION_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
24
- /** Lifecycle values the host's cron passes to `cleanup`. */
25
- export const PREVIEW_CLIENT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
27
+ /** How long `cleanup` keeps an audit row of an issued id_token. */
26
28
  export const SIGN_IN_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
27
29
  export function defineIdentityBroker(config) {
28
30
  const issuerUrl = new URL(config.issuer);
@@ -32,9 +34,13 @@ export function defineIdentityBroker(config) {
32
34
  const issuer = issuerUrl.href.replace(/\/$/, "");
33
35
  const prefix = issuerUrl.pathname.replace(/\/$/, "");
34
36
  const path = (suffix) => `${prefix}${suffix}`;
35
- /** Binds the chooser's links to its browser. `__Host-` needs Path=/, so a path issuer uses a plain name. */
36
- const txnCookieName = prefix === "" ? "__Host-broker_txn" : "broker_txn";
37
- const txnCookieAttributes = `HttpOnly; Secure; SameSite=Lax; Path=${prefix === "" ? "/" : prefix}`;
37
+ /**
38
+ * The chooser binds its request to the browser that saw it with one cookie per request, named
39
+ * after the request id, so two sign-ins in one browser keep their own. `__Host-` needs Path=/, so
40
+ * a path issuer uses a plain name scoped to its path.
41
+ */
42
+ const txnCookiePrefix = prefix === "" ? "__Host-broker_txn_" : "broker_txn_";
43
+ const txnCookieAttributes = `Path=${prefix === "" ? "/" : prefix}; HttpOnly; Secure; SameSite=Lax`;
38
44
  const component = config.component;
39
45
  const connectors = new Map(config.connectors.map((connector) => [connector.name, connector]));
40
46
  if (connectors.size !== config.connectors.length) {
@@ -43,6 +49,41 @@ export function defineIdentityBroker(config) {
43
49
  if (connectors.size === 0) {
44
50
  throw new Error("At least one connector is required");
45
51
  }
52
+ const { previewClientIdleDays } = config;
53
+ // Zero or less would delete every preview client at the next cleanup.
54
+ if (previewClientIdleDays !== undefined && !(previewClientIdleDays > 0)) {
55
+ throw new Error("previewClientIdleDays must be a positive number of days");
56
+ }
57
+ /** The transaction cookies the browser sent, by request id. */
58
+ function readTxnCookies(request) {
59
+ const cookies = new Map();
60
+ for (const pair of request.headers.get("cookie")?.split(";") ?? []) {
61
+ const separator = pair.indexOf("=");
62
+ const name = pair.slice(0, separator).trim();
63
+ if (separator > 0 && name.startsWith(txnCookiePrefix)) {
64
+ cookies.set(name.slice(txnCookiePrefix.length), pair.slice(separator + 1).trim());
65
+ }
66
+ }
67
+ return cookies;
68
+ }
69
+ /**
70
+ * Whether the browser holds the cookie whose hash a chooser request stores. A first-party request
71
+ * that skipped the chooser stores none and needs none.
72
+ */
73
+ async function browserBound(request, stored) {
74
+ if (stored.txnCookieHash === undefined) {
75
+ return true;
76
+ }
77
+ const cookie = readTxnCookies(request).get(stored.requestId);
78
+ return cookie !== undefined && (await sha256Hex(cookie)) === stored.txnCookieHash;
79
+ }
80
+ /** The Set-Cookie value that deletes a request's cookie, when the browser sent one. */
81
+ function clearTxnCookie(request, requestId) {
82
+ return readTxnCookies(request).has(requestId)
83
+ ? `${txnCookiePrefix}${requestId}=; ${txnCookieAttributes}; Max-Age=0`
84
+ : undefined;
85
+ }
86
+ const unboundPage = () => errorPage("Sign-in not continued", "This sign-in did not start in this browser. Return to the app and try again.", 400);
46
87
  const keySet = () => parseKeySet(config.keys());
47
88
  const callbackUrl = (connector) => `${issuer}/connectors/${connector.name}/callback`;
48
89
  const metadata = () => ({
@@ -84,7 +125,12 @@ export function defineIdentityBroker(config) {
84
125
  /** Sends the user to a connector for a stored request, or shows the email form. */
85
126
  async function startConnector(ctx, args) {
86
127
  const { connector, request } = args;
87
- const back = { redirectUri: request.redirectUri, issuer, state: request.state };
128
+ const back = {
129
+ redirectUri: request.redirectUri,
130
+ issuer,
131
+ state: request.state,
132
+ clearCookie: args.clearCookie,
133
+ };
88
134
  if (connector.kind === "email") {
89
135
  return emailAddressPage({
90
136
  appName: args.appName,
@@ -92,7 +138,9 @@ export function defineIdentityBroker(config) {
92
138
  requestId: request.requestId,
93
139
  });
94
140
  }
95
- if (request.freshLogin && connector.kind === "oauth") {
141
+ // Freshness is honored only as prompt=login upstream; a connector without it is refused, never
142
+ // pretended.
143
+ if (request.freshLogin && (connector.kind === "oauth" || !connector.promptLogin)) {
96
144
  return authorizationError({
97
145
  ...back,
98
146
  error: "login_required",
@@ -148,6 +196,7 @@ export function defineIdentityBroker(config) {
148
196
  redirectUri: client.redirectUri,
149
197
  issuer,
150
198
  state: params.get("state") ?? undefined,
199
+ clearCookie: undefined,
151
200
  };
152
201
  const rest = validateAuthorizeRest(params, target.value);
153
202
  if (!rest.ok) {
@@ -176,8 +225,8 @@ export function defineIdentityBroker(config) {
176
225
  const named = rest.value.connector === undefined ? undefined : connectors.get(rest.value.connector);
177
226
  const only = connectors.size === 1 ? [...connectors.values()][0] : undefined;
178
227
  const chosen = app.firstParty ? (named ?? only) : undefined;
179
- // The chooser's links work only with this cookie, so a client that fetches the chooser itself
180
- // cannot send a user's browser on from it.
228
+ // The request moves on only with this cookie, so a client that fetches the chooser itself cannot
229
+ // send a user's browser on from it.
181
230
  const txnCookie = randomToken(32);
182
231
  const now = Date.now();
183
232
  const requestId = randomToken(24);
@@ -206,6 +255,7 @@ export function defineIdentityBroker(config) {
206
255
  freshLogin,
207
256
  },
208
257
  appName: app.name,
258
+ clearCookie: undefined,
209
259
  });
210
260
  }
211
261
  const page = chooserPage({
@@ -219,7 +269,7 @@ export function defineIdentityBroker(config) {
219
269
  })),
220
270
  denyHref: `${path("/continue")}?request=${encodeURIComponent(requestId)}&deny=1`,
221
271
  });
222
- page.headers.append("Set-Cookie", `${txnCookieName}=${txnCookie}; ${txnCookieAttributes}`);
272
+ page.headers.append("Set-Cookie", `${txnCookiePrefix}${requestId}=${txnCookie}; ${txnCookieAttributes}; Max-Age=${AUTH_REQUEST_TTL_MS / 1000}`);
223
273
  return page;
224
274
  });
225
275
  /** A chooser click: start the chosen connector, or abandon the request on deny. */
@@ -230,18 +280,16 @@ export function defineIdentityBroker(config) {
230
280
  if (stored === null) {
231
281
  return errorPage("Sign-in not found", "This sign-in is unknown or finished. Return to the app and try again.", 404);
232
282
  }
233
- const txnCookie = request.headers
234
- .get("cookie")
235
- ?.split(";")
236
- .map((pair) => pair.trim())
237
- .find((pair) => pair.startsWith(`${txnCookieName}=`))
238
- ?.slice(txnCookieName.length + 1);
239
- if (stored.txnCookieHash === undefined ||
240
- txnCookie === undefined ||
241
- (await sha256Hex(txnCookie)) !== stored.txnCookieHash) {
242
- return errorPage("Sign-in not continued", "This sign-in did not start in this browser. Return to the app and try again.", 400);
243
- }
244
- const back = { redirectUri: stored.redirectUri, issuer, state: stored.state };
283
+ // Only a request that showed the chooser has links to continue.
284
+ if (stored.txnCookieHash === undefined || !(await browserBound(request, stored))) {
285
+ return unboundPage();
286
+ }
287
+ const back = {
288
+ redirectUri: stored.redirectUri,
289
+ issuer,
290
+ state: stored.state,
291
+ clearCookie: clearTxnCookie(request, requestId),
292
+ };
245
293
  if (url.searchParams.get("deny") !== null) {
246
294
  await ctx.runMutation(component.authRequests.abandon, { requestId });
247
295
  return authorizationError({
@@ -275,6 +323,7 @@ export function defineIdentityBroker(config) {
275
323
  freshLogin: stored.freshLogin,
276
324
  },
277
325
  appName: app.name,
326
+ clearCookie: back.clearCookie,
278
327
  });
279
328
  });
280
329
  function upstreamCallback(connector) {
@@ -288,12 +337,21 @@ export function defineIdentityBroker(config) {
288
337
  const claim = await ctx.runMutation(component.authRequests.claimUpstream, {
289
338
  upstreamStateHash: await sha256Hex(state),
290
339
  connector: connector.name,
340
+ txnCookieHashes: await Promise.all([...readTxnCookies(request).values()].map((cookie) => sha256Hex(cookie))),
291
341
  now,
292
342
  });
293
343
  if (claim.status === "missing") {
294
344
  return errorPage("Sign-in link used", "This sign-in link is invalid or was already used. Return to the app and try again.", 400);
295
345
  }
296
- const back = { redirectUri: claim.redirectUri, issuer, state: claim.state };
346
+ if (claim.status === "unbound") {
347
+ return unboundPage();
348
+ }
349
+ const back = {
350
+ redirectUri: claim.redirectUri,
351
+ issuer,
352
+ state: claim.state,
353
+ clearCookie: clearTxnCookie(request, claim.requestId),
354
+ };
297
355
  if (claim.status === "expired") {
298
356
  return authorizationError({
299
357
  ...back,
@@ -301,9 +359,9 @@ export function defineIdentityBroker(config) {
301
359
  description: "The sign-in expired.",
302
360
  });
303
361
  }
304
- let identity;
362
+ let upstream;
305
363
  try {
306
- identity = await finishUpstream({
364
+ upstream = await finishUpstream({
307
365
  connector,
308
366
  callbackUrl: callbackUrl(connector),
309
367
  currentUrl: url,
@@ -338,11 +396,30 @@ export function defineIdentityBroker(config) {
338
396
  description: `${connector.label} sign-in failed.`,
339
397
  });
340
398
  }
399
+ let authTime = now;
400
+ if (claim.freshLogin) {
401
+ // prompt=login and max_age=0 went upstream, but a provider can still reuse its session;
402
+ // only the id_token's auth_time attests a login during this request. It must fall between
403
+ // the request's creation and now, each widened by the clock skew; auth_time is in whole
404
+ // seconds. A plain OAuth connector has no auth_time, and startConnector already refused it.
405
+ const { authTimeSeconds } = upstream;
406
+ if (authTimeSeconds === undefined ||
407
+ authTimeSeconds < Math.floor(claim.createdAt / 1000) - AUTH_TIME_SKEW_SECONDS ||
408
+ authTimeSeconds > Math.floor(now / 1000) + AUTH_TIME_SKEW_SECONDS) {
409
+ await ctx.runMutation(component.authRequests.abandon, { requestId: claim.requestId });
410
+ return authorizationError({
411
+ ...back,
412
+ error: "login_required",
413
+ description: `${connector.label} did not confirm a new login, which prompt=login or max_age asked for.`,
414
+ });
415
+ }
416
+ authTime = authTimeSeconds * 1000;
417
+ }
341
418
  const code = randomToken(32);
342
419
  const completed = await ctx.runMutation(component.authRequests.complete, {
343
420
  requestId: claim.requestId,
344
- identity: { connector: connector.name, ...identity },
345
- authTime: now,
421
+ identity: { connector: connector.name, ...upstream.identity },
422
+ authTime,
346
423
  codeHash: await sha256Hex(code),
347
424
  codeExpiresAt: now + CODE_TTL_MS,
348
425
  now,
@@ -371,7 +448,16 @@ export function defineIdentityBroker(config) {
371
448
  if (stored === null) {
372
449
  return errorPage("Sign-in not found", "This sign-in is unknown or finished. Return to the app and try again.", 404);
373
450
  }
374
- const back = { redirectUri: stored.redirectUri, issuer, state: stored.state };
451
+ // Every step moves the request on, so every step needs the browser that saw the chooser.
452
+ if (!(await browserBound(request, stored))) {
453
+ return unboundPage();
454
+ }
455
+ const back = {
456
+ redirectUri: stored.redirectUri,
457
+ issuer,
458
+ state: stored.state,
459
+ clearCookie: clearTxnCookie(request, requestId),
460
+ };
375
461
  const app = await readApp(ctx, stored.appId);
376
462
  if (app === null) {
377
463
  return authorizationError({
@@ -808,7 +894,9 @@ export function defineIdentityBroker(config) {
808
894
  args: {},
809
895
  handler: async (ctx) => await ctx.runMutation(component.lifecycle.cleanup, {
810
896
  now: Date.now(),
811
- previewClientMaxAgeMs: PREVIEW_CLIENT_MAX_AGE_MS,
897
+ ...(previewClientIdleDays === undefined
898
+ ? {}
899
+ : { previewClientMaxAgeMs: previewClientIdleDays * 24 * 60 * 60 * 1000 }),
812
900
  signInRetentionMs: SIGN_IN_RETENTION_MS,
813
901
  }),
814
902
  }),