@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.
- package/README.md +2 -1
- package/dist/cli/index.js +6 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/rotate.d.ts +10 -0
- package/dist/cli/rotate.js +25 -0
- package/dist/cli/rotate.js.map +1 -0
- package/dist/client/connectors/google.d.ts +5 -1
- package/dist/client/connectors/google.js +6 -1
- package/dist/client/connectors/google.js.map +1 -1
- package/dist/client/connectors/types.d.ts +5 -0
- package/dist/client/connectors/upstream.d.ts +9 -1
- package/dist/client/connectors/upstream.js +9 -2
- package/dist/client/connectors/upstream.js.map +1 -1
- package/dist/client/html.js +3 -0
- package/dist/client/html.js.map +1 -1
- package/dist/client/index.d.ts +7 -2
- package/dist/client/index.js +117 -29
- package/dist/client/index.js.map +1 -1
- package/dist/client/jwt.d.ts +8 -0
- package/dist/client/jwt.js +51 -1
- package/dist/client/jwt.js.map +1 -1
- package/dist/client/keys.d.ts +11 -3
- package/dist/client/keys.js +68 -20
- package/dist/client/keys.js.map +1 -1
- package/dist/client/protocol.d.ts +2 -1
- package/dist/client/protocol.js +11 -3
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/responses.d.ts +6 -0
- package/dist/client/responses.js +6 -1
- package/dist/client/responses.js.map +1 -1
- package/dist/component/_generated/component.d.ts +6 -1
- package/dist/component/authRequests.d.ts +8 -1
- package/dist/component/authRequests.js +20 -5
- package/dist/component/authRequests.js.map +1 -1
- package/dist/component/lifecycle.d.ts +14 -6
- package/dist/component/lifecycle.js +109 -67
- package/dist/component/lifecycle.js.map +1 -1
- package/dist/component/schema.d.ts +34 -21
- package/dist/component/schema.js +7 -3
- package/dist/component/schema.js.map +1 -1
- 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
|
|
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
|
|
5
|
-
one
|
|
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
|
|
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
|
}
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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<
|
|
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
|
|
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
|
-
|
|
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":"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"}
|
package/dist/client/html.js
CHANGED
|
@@ -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
|
}
|
package/dist/client/html.js.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/client/html.ts"],"names":[],"mappings":"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"}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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. */
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
180
|
-
//
|
|
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", `${
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
|
362
|
+
let upstream;
|
|
305
363
|
try {
|
|
306
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
897
|
+
...(previewClientIdleDays === undefined
|
|
898
|
+
? {}
|
|
899
|
+
: { previewClientMaxAgeMs: previewClientIdleDays * 24 * 60 * 60 * 1000 }),
|
|
812
900
|
signInRetentionMs: SIGN_IN_RETENTION_MS,
|
|
813
901
|
}),
|
|
814
902
|
}),
|