@pidgeon-ai/issuer 0.1.1 → 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.
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/directory.ts
4
+ function onePerAddress(agents) {
5
+ const count = /* @__PURE__ */ new Map();
6
+ for (const agent of agents) count.set(agent.address, (count.get(agent.address) ?? 0) + 1);
7
+ return agents.filter((agent) => count.get(agent.address) === 1);
8
+ }
4
9
  function staticDirectory(agents) {
5
10
  const list = agents.map((agent) => ({ ...agent, address: agent.address.toLowerCase() }));
6
11
  return {
@@ -25,6 +30,86 @@ function cachedDirectory(inner, seconds = 60, now = () => Date.now()) {
25
30
  };
26
31
  }
27
32
 
33
+ // src/auth0.ts
34
+ var PER_PAGE = 100;
35
+ function auth0Directory(options) {
36
+ const base = `https://${options.tenant.replace(/^https:\/\//, "").replace(/\/$/, "")}`;
37
+ const domain = options.domain.toLowerCase();
38
+ const fetcher = options.fetch ?? fetch;
39
+ const now = options.now ?? (() => Date.now());
40
+ let token = null;
41
+ async function accessToken() {
42
+ if (token && token.until > now()) return token.value;
43
+ const response = await fetcher(`${base}/oauth/token`, {
44
+ method: "POST",
45
+ headers: { "content-type": "application/x-www-form-urlencoded" },
46
+ body: new URLSearchParams({
47
+ grant_type: "client_credentials",
48
+ client_id: options.clientId,
49
+ client_secret: options.clientSecret,
50
+ audience: `${base}/api/v2/`
51
+ })
52
+ });
53
+ const body = await response.json().catch(() => ({}));
54
+ if (!response.ok || !body.access_token) {
55
+ throw new Error(
56
+ `Auth0 refused the issuer's application ${options.clientId} (${response.status}).`
57
+ );
58
+ }
59
+ token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
60
+ return token.value;
61
+ }
62
+ async function management(path) {
63
+ const response = await fetcher(`${base}/api/v2${path}`, {
64
+ headers: { authorization: `Bearer ${await accessToken()}`, accept: "application/json" }
65
+ });
66
+ if (response.status === 404) return null;
67
+ if (!response.ok) throw new Error(`Auth0 answered ${response.status} for ${path}.`);
68
+ return await response.json();
69
+ }
70
+ function agentOf(client) {
71
+ if (client.app_type !== "non_interactive") return null;
72
+ const metadata = client.client_metadata ?? {};
73
+ if (metadata.pidgeon_disabled?.trim().toLowerCase() === "true") return null;
74
+ const key = metadata.pidgeon_key?.trim();
75
+ if (!key || Buffer.from(key, "base64url").length !== 32) return null;
76
+ const address = metadata.pidgeon_address?.trim().toLowerCase();
77
+ if (!address?.endsWith(`@${domain}`)) return null;
78
+ return {
79
+ sub: client.client_id,
80
+ address,
81
+ kind: metadata.pidgeon_kind?.trim() === "service" ? "service" : "agent",
82
+ key,
83
+ groups: (metadata.pidgeon_groups ?? "").split(",").map((group) => group.trim()).filter(Boolean),
84
+ // An age Auth0 does not give is treated as new: the card says `new`, never older than it is.
85
+ createdAt: new Date(client.created_at ? Date.parse(client.created_at) : now()).toISOString()
86
+ };
87
+ }
88
+ async function all() {
89
+ const agents = [];
90
+ for (let page2 = 0; ; page2++) {
91
+ const body = await management(
92
+ `/clients?app_type=non_interactive&per_page=${PER_PAGE}&page=${page2}&include_totals=true`
93
+ );
94
+ const clients = body?.clients ?? [];
95
+ for (const client of clients) {
96
+ const agent = agentOf(client);
97
+ if (agent) agents.push(agent);
98
+ }
99
+ if (clients.length < PER_PAGE || (page2 + 1) * PER_PAGE >= (body?.total ?? 0)) break;
100
+ }
101
+ return agents;
102
+ }
103
+ return {
104
+ agents: async () => onePerAddress(await all()),
105
+ async agent(sub) {
106
+ const client = await management(`/clients/${encodeURIComponent(sub)}`);
107
+ const agent = client ? agentOf(client) : null;
108
+ return agent?.sub === sub ? agent : null;
109
+ }
110
+ };
111
+ }
112
+
28
113
  // src/keycloak.ts
29
114
  var SERVICE_ACCOUNT_PREFIX = "service-account-";
30
115
  var ATTRIBUTES = { key: "pidgeon.key", address: "pidgeon.address", kind: "pidgeon.kind" };
@@ -106,7 +191,7 @@ function keycloakDirectory(options) {
106
191
  const clients = await admin("/clients?first=0&max=1000") ?? [];
107
192
  const candidates = clients.filter((client) => keyOf(client) !== null);
108
193
  const agents = await Promise.all(candidates.map(agentOf));
109
- return agents.filter((agent) => agent !== null);
194
+ return onePerAddress(agents.filter((agent) => agent !== null));
110
195
  },
111
196
  /**
112
197
  * The user's record does not name its client (only a realm export
@@ -128,8 +213,121 @@ function keycloakDirectory(options) {
128
213
  };
129
214
  }
130
215
 
131
- // src/signer.ts
132
- import { readFile } from "fs/promises";
216
+ // src/okta.ts
217
+ import { createPrivateKey, randomUUID, sign } from "crypto";
218
+ var OKTA_SCOPE = "okta.apps.read";
219
+ function oktaDirectory(options) {
220
+ const org = options.org.replace(/\/$/, "");
221
+ const domain = options.domain.toLowerCase();
222
+ const fetcher = options.fetch ?? fetch;
223
+ const now = options.now ?? (() => Date.now());
224
+ const privateKey = typeof options.privateKey === "string" ? createPrivateKey(options.privateKey) : options.privateKey;
225
+ const alg = privateKey.asymmetricKeyType === "rsa" ? "RS256" : privateKey.asymmetricKeyType === "ec" && privateKey.asymmetricKeyDetails?.namedCurve === "prime256v1" ? "ES256" : null;
226
+ if (!alg) throw new Error("Okta's client assertion is signed with an RSA or P-256 key.");
227
+ let token = null;
228
+ function assertion(audience) {
229
+ const at = Math.floor(now() / 1e3);
230
+ const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
231
+ const input = `${b64({ alg, typ: "JWT", ...options.keyId ? { kid: options.keyId } : {} })}.${b64(
232
+ {
233
+ iss: options.clientId,
234
+ sub: options.clientId,
235
+ aud: audience,
236
+ iat: at,
237
+ exp: at + 300,
238
+ jti: randomUUID()
239
+ }
240
+ )}`;
241
+ const signature = sign(
242
+ "sha256",
243
+ Buffer.from(input),
244
+ alg === "ES256" ? { key: privateKey, dsaEncoding: "ieee-p1363" } : privateKey
245
+ );
246
+ return `${input}.${signature.toString("base64url")}`;
247
+ }
248
+ async function accessToken() {
249
+ if (token && token.until > now()) return token.value;
250
+ const endpoint = `${org}/oauth2/v1/token`;
251
+ const response = await fetcher(endpoint, {
252
+ method: "POST",
253
+ headers: {
254
+ "content-type": "application/x-www-form-urlencoded",
255
+ accept: "application/json"
256
+ },
257
+ body: new URLSearchParams({
258
+ grant_type: "client_credentials",
259
+ scope: OKTA_SCOPE,
260
+ client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
261
+ client_assertion: assertion(endpoint)
262
+ })
263
+ });
264
+ const body = await response.json().catch(() => ({}));
265
+ if (!response.ok || !body.access_token) {
266
+ throw new Error(
267
+ `Okta refused the issuer's app ${options.clientId} (${response.status}${body.error_description ? `: ${body.error_description}` : ""}).`
268
+ );
269
+ }
270
+ token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
271
+ return token.value;
272
+ }
273
+ async function api(url) {
274
+ const response = await fetcher(url, {
275
+ headers: { authorization: `Bearer ${await accessToken()}`, accept: "application/json" }
276
+ });
277
+ if (!response.ok && response.status !== 404) {
278
+ throw new Error(`Okta answered ${response.status} for ${new URL(url).pathname}.`);
279
+ }
280
+ return response;
281
+ }
282
+ function agentOf(app) {
283
+ if (app.status !== "ACTIVE") return null;
284
+ if (app.settings?.oauthClient?.application_type !== "service") return null;
285
+ const pidgeon = app.profile?.pidgeon;
286
+ const key = typeof pidgeon?.key === "string" ? pidgeon.key.trim() : "";
287
+ if (!key || Buffer.from(key, "base64url").length !== 32) return null;
288
+ const address = typeof pidgeon?.address === "string" ? pidgeon.address.trim().toLowerCase() : "";
289
+ if (!address.endsWith(`@${domain}`)) return null;
290
+ return {
291
+ sub: app.id,
292
+ address,
293
+ kind: pidgeon?.kind === "service" ? "service" : "agent",
294
+ key,
295
+ groups: Array.isArray(pidgeon?.groups) ? pidgeon.groups.filter((group) => typeof group === "string") : [],
296
+ createdAt: new Date(app.created ? Date.parse(app.created) : now()).toISOString()
297
+ };
298
+ }
299
+ function nextPage(response) {
300
+ for (const part of (response.headers.get("link") ?? "").split(",")) {
301
+ const match = /<([^>]+)>\s*;\s*rel="next"/.exec(part);
302
+ if (match && new URL(match[1]).origin === new URL(org).origin) return match[1];
303
+ }
304
+ return null;
305
+ }
306
+ return {
307
+ async agents() {
308
+ const agents = [];
309
+ let url = `${org}/api/v1/apps?filter=${encodeURIComponent('status eq "ACTIVE"')}&limit=200`;
310
+ for (let pages = 0; url && pages < 100; pages++) {
311
+ const response = await api(url);
312
+ for (const app of await response.json() ?? []) {
313
+ const agent = agentOf(app);
314
+ if (agent) agents.push(agent);
315
+ }
316
+ url = nextPage(response);
317
+ }
318
+ return onePerAddress(agents);
319
+ },
320
+ async agent(sub) {
321
+ const response = await api(`${org}/api/v1/apps/${encodeURIComponent(sub)}`);
322
+ if (response.status === 404) return null;
323
+ const agent = agentOf(await response.json());
324
+ return agent?.sub === sub ? agent : null;
325
+ }
326
+ };
327
+ }
328
+
329
+ // src/kms.ts
330
+ import { createPublicKey, verify as verify2 } from "crypto";
133
331
 
134
332
  // ../id/dist/chunk-QDQDZSU7.js
135
333
  import { createHash } from "crypto";
@@ -148,7 +346,7 @@ __export(post_core_wasm_exports, {
148
346
  policyDecision: () => policyDecision,
149
347
  policyMaxAge: () => policyMaxAge,
150
348
  publicKey: () => publicKey,
151
- sign: () => sign,
349
+ sign: () => sign2,
152
350
  verify: () => verify
153
351
  });
154
352
  function generateSeed() {
@@ -213,7 +411,7 @@ function publicKey(seed) {
213
411
  wasm.__wbindgen_add_to_stack_pointer(16);
214
412
  }
215
413
  }
216
- function sign(seed, message) {
414
+ function sign2(seed, message) {
217
415
  try {
218
416
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
219
417
  const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export3);
@@ -624,7 +822,307 @@ function splitTopLevel(value) {
624
822
  return out;
625
823
  }
626
824
 
825
+ // src/kms.ts
826
+ var KMS_KEY_SPEC = "ECC_NIST_EDWARDS25519";
827
+ var KMS_SIGNING_ALGORITHM = "ED25519_SHA_512";
828
+ async function sdkClient(region) {
829
+ let sdk;
830
+ try {
831
+ sdk = await import("@aws-sdk/client-kms");
832
+ } catch {
833
+ throw new Error(
834
+ "A KMS key needs @aws-sdk/client-kms: `npm install @aws-sdk/client-kms` beside the issuer."
835
+ );
836
+ }
837
+ const client = new sdk.KMSClient(region ? { region } : {});
838
+ return {
839
+ getPublicKey: (input) => client.send(new sdk.GetPublicKeyCommand(input)),
840
+ sign: (input) => client.send(
841
+ new sdk.SignCommand({
842
+ ...input,
843
+ SigningAlgorithm: input.SigningAlgorithm
844
+ })
845
+ )
846
+ };
847
+ }
848
+ async function readKey(kms, keyId) {
849
+ let key;
850
+ try {
851
+ key = await kms.getPublicKey({ KeyId: keyId });
852
+ } catch (error) {
853
+ throw new Error(
854
+ `KMS would not give the issuer ${keyId}'s public key (${error instanceof Error ? error.message : error}). Its role needs kms:GetPublicKey and kms:Sign on that key.`
855
+ );
856
+ }
857
+ if (key.KeySpec !== KMS_KEY_SPEC) {
858
+ throw new Error(`${keyId} is a ${key.KeySpec} key. The issuer signs with ${KMS_KEY_SPEC}.`);
859
+ }
860
+ if (key.KeyUsage && key.KeyUsage !== "SIGN_VERIFY") {
861
+ throw new Error(`${keyId} is not a signing key.`);
862
+ }
863
+ if (!key.SigningAlgorithms?.includes(KMS_SIGNING_ALGORITHM)) {
864
+ throw new Error(`${keyId} does not sign with ${KMS_SIGNING_ALGORITHM}.`);
865
+ }
866
+ if (!key.KeyId || !key.PublicKey) throw new Error(`KMS gave no public key for ${keyId}.`);
867
+ const publicKey2 = createPublicKey({
868
+ key: Buffer.from(key.PublicKey),
869
+ format: "der",
870
+ type: "spki"
871
+ });
872
+ const x = publicKey2.export({ format: "jwk" }).x;
873
+ const raw = new Uint8Array(Buffer.from(x ?? "", "base64url"));
874
+ if (raw.length !== 32) throw new Error(`${keyId}'s public key is not an Ed25519 key.`);
875
+ return { arn: key.KeyId, publicKey: publicKey2, raw };
876
+ }
877
+ async function kmsSigner(options) {
878
+ const kms = options.kms ?? await sdkClient(options.region);
879
+ const { arn, publicKey: publicKey2, raw } = await readKey(kms, options.keyId);
880
+ return {
881
+ kid: jwkThumbprint(raw),
882
+ publicKey: raw,
883
+ keyArn: arn,
884
+ async sign(message) {
885
+ const signed = await kms.sign({
886
+ KeyId: arn,
887
+ Message: message,
888
+ MessageType: "RAW",
889
+ SigningAlgorithm: KMS_SIGNING_ALGORITHM
890
+ });
891
+ const signature = signed.Signature ? new Uint8Array(signed.Signature) : null;
892
+ if (!signature || !verify2(null, message, publicKey2, signature)) {
893
+ throw new Error(
894
+ `KMS answered for ${arn} with a signature its public key does not verify. Nothing was signed.`
895
+ );
896
+ }
897
+ return signature;
898
+ }
899
+ };
900
+ }
901
+ async function kmsRetiredKey(options) {
902
+ const kms = options.kms ?? await sdkClient(options.region);
903
+ const { raw } = await readKey(kms, options.keyId);
904
+ return { kid: jwkThumbprint(raw), publicKey: raw, notAfter: options.notAfter };
905
+ }
906
+
907
+ // src/person.ts
908
+ import {
909
+ createCipheriv,
910
+ createDecipheriv,
911
+ createHash as createHash3,
912
+ createPublicKey as createPublicKey2,
913
+ randomBytes as randomBytes2,
914
+ timingSafeEqual,
915
+ verify as verify3
916
+ } from "crypto";
917
+ var MIN_SECRET = 32;
918
+ var FLOW_COOKIE = "pidgeon_issuer_flow";
919
+ var CODE_SECONDS = 60;
920
+ var FLOW_SECONDS = 600;
921
+ var SKEW = 60;
922
+ var UPSTREAM_ALGS = ["RS256", "ES256", "EdDSA"];
923
+ function checkSecret(secret) {
924
+ if (!secret || secret.length < MIN_SECRET) {
925
+ throw new Error(
926
+ `A person's sign-in needs PIDGEON_ISSUER_SECRET, ${MIN_SECRET} characters or more. It seals the sign-in in progress and the code; there is no default.`
927
+ );
928
+ }
929
+ return secret;
930
+ }
931
+ var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
932
+ function sealKey(secret, purpose) {
933
+ return createHash3("sha256").update(`pidgeon issuer seal\0${purpose}\0${secret}`).digest();
934
+ }
935
+ function seal(secret, purpose, value) {
936
+ const iv = randomBytes2(12);
937
+ const cipher = createCipheriv("aes-256-gcm", sealKey(secret, purpose), iv);
938
+ cipher.setAAD(Buffer.from(purpose));
939
+ const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
940
+ return b64url2(Buffer.concat([iv, body, cipher.getAuthTag()]));
941
+ }
942
+ function unseal(secret, purpose, token) {
943
+ if (!token) return null;
944
+ try {
945
+ const raw = Buffer.from(token, "base64url");
946
+ if (raw.length < 29) return null;
947
+ const decipher = createDecipheriv("aes-256-gcm", sealKey(secret, purpose), raw.subarray(0, 12));
948
+ decipher.setAAD(Buffer.from(purpose));
949
+ decipher.setAuthTag(raw.subarray(raw.length - 16));
950
+ const text2 = Buffer.concat([
951
+ decipher.update(raw.subarray(12, raw.length - 16)),
952
+ decipher.final()
953
+ ]).toString("utf8");
954
+ return JSON.parse(text2);
955
+ } catch {
956
+ return null;
957
+ }
958
+ }
959
+ function pkceChallenge(verifier) {
960
+ return b64url2(createHash3("sha256").update(verifier).digest());
961
+ }
962
+ function pkceHolds(verifier, challenge) {
963
+ const a = Buffer.from(pkceChallenge(verifier));
964
+ const b = Buffer.from(challenge);
965
+ return a.length === b.length && timingSafeEqual(a, b);
966
+ }
967
+ function cookieOf(request, name) {
968
+ for (const part of (request.headers.get("cookie") ?? "").split(";")) {
969
+ const eq = part.indexOf("=");
970
+ if (eq > 0 && part.slice(0, eq).trim() === name) {
971
+ return decodeURIComponent(part.slice(eq + 1).trim());
972
+ }
973
+ }
974
+ return null;
975
+ }
976
+ var ProviderRefused = class extends Error {
977
+ };
978
+ function provider(options, fetcher, now) {
979
+ const issuer = options.issuer.replace(/\/$/, "");
980
+ let configuration = null;
981
+ let keys = null;
982
+ function discover() {
983
+ if (!configuration || configuration.at <= now() - 36e5) {
984
+ const url = options.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`;
985
+ const value = (async () => {
986
+ const response = await fetcher(url, { headers: { accept: "application/json" } });
987
+ if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
988
+ const body = await response.json();
989
+ if (body.issuer?.replace(/\/$/, "") !== issuer) {
990
+ throw new Error(`${url} names the issuer ${body.issuer}, not ${issuer}.`);
991
+ }
992
+ return body;
993
+ })();
994
+ configuration = { at: now(), value };
995
+ value.catch(() => {
996
+ if (configuration?.value === value) configuration = null;
997
+ });
998
+ }
999
+ return configuration.value;
1000
+ }
1001
+ async function key(kid) {
1002
+ const find = () => keys?.value.find((one) => (kid ? one.kid === kid : true) && one.use !== "enc") ?? null;
1003
+ const fresh = keys && keys.at > now() - 3e5;
1004
+ if (fresh && find()) return find();
1005
+ if (keys && keys.at > now() - 1e4) return null;
1006
+ const response = await fetcher((await discover()).jwks_uri);
1007
+ const body = await response.json().catch(() => ({}));
1008
+ keys = { at: now(), value: Array.isArray(body.keys) ? body.keys : [] };
1009
+ return find();
1010
+ }
1011
+ async function authorizeUrl(input) {
1012
+ const url = new URL((await discover()).authorization_endpoint);
1013
+ url.search = new URLSearchParams({
1014
+ response_type: "code",
1015
+ client_id: options.clientId,
1016
+ redirect_uri: input.redirectUri,
1017
+ scope: "openid email",
1018
+ state: input.state,
1019
+ nonce: input.nonce,
1020
+ code_challenge: pkceChallenge(input.verifier),
1021
+ code_challenge_method: "S256"
1022
+ }).toString();
1023
+ return url.toString();
1024
+ }
1025
+ async function redeem(input) {
1026
+ const form = new URLSearchParams({
1027
+ grant_type: "authorization_code",
1028
+ code: input.code,
1029
+ redirect_uri: input.redirectUri,
1030
+ code_verifier: input.verifier
1031
+ });
1032
+ const headers = {
1033
+ "content-type": "application/x-www-form-urlencoded",
1034
+ accept: "application/json"
1035
+ };
1036
+ if (options.tokenAuth === "client_secret_post") {
1037
+ form.set("client_id", options.clientId);
1038
+ form.set("client_secret", options.clientSecret);
1039
+ } else {
1040
+ const pair = `${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`;
1041
+ headers.authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
1042
+ }
1043
+ const response = await fetcher((await discover()).token_endpoint, {
1044
+ method: "POST",
1045
+ headers,
1046
+ body: form
1047
+ });
1048
+ const body = await response.json().catch(() => ({}));
1049
+ if (!response.ok || typeof body.id_token !== "string") {
1050
+ throw new ProviderRefused(
1051
+ `The identity provider did not sign the person in (${body.error ?? response.status}).`
1052
+ );
1053
+ }
1054
+ return check(body.id_token, input.nonce);
1055
+ }
1056
+ async function check(token, nonce) {
1057
+ const parts = token.split(".");
1058
+ if (parts.length !== 3) throw new ProviderRefused("The provider sent no ID token.");
1059
+ let header;
1060
+ let claims;
1061
+ try {
1062
+ header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
1063
+ claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1064
+ } catch {
1065
+ throw new ProviderRefused("The provider sent no ID token.");
1066
+ }
1067
+ if (!header.alg || !UPSTREAM_ALGS.includes(header.alg)) {
1068
+ throw new ProviderRefused(`The provider's ID token is signed with ${header.alg}.`);
1069
+ }
1070
+ const jwk = await key(header.kid);
1071
+ if (!jwk || !signatureHolds(header.alg, jwk, `${parts[0]}.${parts[1]}`, parts[2])) {
1072
+ throw new ProviderRefused("The provider's ID token is not signed by its keys.");
1073
+ }
1074
+ const at = Math.floor(now() / 1e3);
1075
+ if (typeof claims.iss !== "string" || claims.iss.replace(/\/$/, "") !== issuer) {
1076
+ throw new ProviderRefused("The provider's ID token is from another issuer.");
1077
+ }
1078
+ const aud = claims.aud;
1079
+ const audiences = Array.isArray(aud) ? aud : [aud];
1080
+ if (!audiences.includes(options.clientId)) {
1081
+ throw new ProviderRefused("The provider's ID token is for another client.");
1082
+ }
1083
+ if (audiences.length > 1 && claims.azp !== options.clientId) {
1084
+ throw new ProviderRefused("The provider's ID token is for another client.");
1085
+ }
1086
+ if (typeof claims.exp !== "number" || claims.exp + SKEW <= at) {
1087
+ throw new ProviderRefused("The provider's ID token has expired.");
1088
+ }
1089
+ if (typeof claims.iat === "number" && claims.iat > at + SKEW) {
1090
+ throw new ProviderRefused("The provider's ID token is from the future.");
1091
+ }
1092
+ if (claims.nonce !== nonce) {
1093
+ throw new ProviderRefused("The provider's ID token is for another sign-in.");
1094
+ }
1095
+ if (typeof claims.sub !== "string" || !claims.sub) {
1096
+ throw new ProviderRefused("The provider's ID token names nobody.");
1097
+ }
1098
+ if (claims.email_verified !== true || typeof claims.email !== "string") {
1099
+ throw new ProviderRefused("The identity provider has not verified this person\u2019s address.");
1100
+ }
1101
+ return {
1102
+ sub: claims.sub,
1103
+ email: claims.email.trim().toLowerCase(),
1104
+ authTime: typeof claims.auth_time === "number" ? claims.auth_time : at
1105
+ };
1106
+ }
1107
+ return { authorizeUrl, redeem };
1108
+ }
1109
+ function signatureHolds(alg, jwk, input, signature) {
1110
+ try {
1111
+ const key = createPublicKey2({ key: jwk, format: "jwk" });
1112
+ const data = Buffer.from(input);
1113
+ const sig = Buffer.from(signature, "base64url");
1114
+ if (alg === "EdDSA") return jwk.kty === "OKP" && verify3(null, data, key, sig);
1115
+ if (alg === "ES256") {
1116
+ return jwk.kty === "EC" && jwk.crv === "P-256" && verify3("sha256", data, { key, dsaEncoding: "ieee-p1363" }, sig);
1117
+ }
1118
+ return jwk.kty === "RSA" && verify3("sha256", data, key, sig);
1119
+ } catch {
1120
+ return false;
1121
+ }
1122
+ }
1123
+
627
1124
  // src/signer.ts
1125
+ import { readFile } from "fs/promises";
628
1126
  async function fileSigner(path) {
629
1127
  let file;
630
1128
  try {
@@ -667,15 +1165,50 @@ async function loadConfig(path, env = process.env) {
667
1165
  throw new Error(`${path}: \`domain\` is the domain whose agents this issues for.`);
668
1166
  }
669
1167
  const here = (relative) => resolve(dirname(path), relative);
670
- const signer = env.PIDGEON_ISSUER_KEY ? seedSigner(env.PIDGEON_ISSUER_KEY, { domain: file.domain }) : file.key ? await fileSigner(here(file.key)) : null;
671
- if (!signer) throw new Error(`${path}: name a \`key\` file, or set PIDGEON_ISSUER_KEY.`);
1168
+ const signer = env.PIDGEON_ISSUER_KEY ? seedSigner(env.PIDGEON_ISSUER_KEY, { domain: file.domain }) : typeof file.key === "string" ? await fileSigner(here(file.key)) : file.key?.kms ? await kmsSigner({ keyId: file.key.kms, region: file.key.region }) : null;
1169
+ if (!signer) {
1170
+ throw new Error(`${path}: name a \`key\` file or a KMS key, or set PIDGEON_ISSUER_KEY.`);
1171
+ }
672
1172
  const retired = [];
673
1173
  for (const entry of file.retired ?? []) {
674
- const old = await fileSigner(here(entry.key));
675
- retired.push({ kid: old.kid, publicKey: old.publicKey, notAfter: new Date(entry.not_after) });
1174
+ const notAfter = new Date(entry.not_after);
1175
+ if (typeof entry.key === "string") {
1176
+ const old = await fileSigner(here(entry.key));
1177
+ retired.push({ kid: old.kid, publicKey: old.publicKey, notAfter });
1178
+ } else {
1179
+ retired.push(
1180
+ await kmsRetiredKey({ keyId: entry.key.kms, region: entry.key.region, notAfter })
1181
+ );
1182
+ }
1183
+ }
1184
+ let person;
1185
+ if (file.person) {
1186
+ const { oidc } = file.person;
1187
+ const secretName = oidc?.client_secret_env ?? "PIDGEON_PERSON_CLIENT_SECRET";
1188
+ if (!oidc?.issuer || !oidc.client_id) {
1189
+ throw new Error(
1190
+ `${path}: \`person.oidc\` names the provider's \`issuer\` and the \`client_id\`.`
1191
+ );
1192
+ }
1193
+ const clientSecret = env[secretName];
1194
+ if (!clientSecret) {
1195
+ throw new Error(`${path}: set ${secretName}, the issuer's client secret at ${oidc.issuer}.`);
1196
+ }
1197
+ person = {
1198
+ secret: checkSecret(env.PIDGEON_ISSUER_SECRET),
1199
+ oidc: {
1200
+ issuer: oidc.issuer,
1201
+ discoveryUrl: oidc.discovery_url,
1202
+ clientId: oidc.client_id,
1203
+ clientSecret,
1204
+ tokenAuth: oidc.token_auth
1205
+ },
1206
+ emailDomains: file.person.email_domains
1207
+ };
676
1208
  }
677
1209
  return {
678
1210
  domain: file.domain,
1211
+ person,
679
1212
  issuer: file.issuer,
680
1213
  organisation: file.organisation,
681
1214
  signer,
@@ -703,6 +1236,32 @@ function directoryFrom(file, path, env) {
703
1236
  group: keycloak.group
704
1237
  });
705
1238
  }
1239
+ if ("auth0" in directory) {
1240
+ const { auth0 } = directory;
1241
+ const secretName = auth0.client_secret_env ?? "AUTH0_CLIENT_SECRET";
1242
+ const secret = env[secretName];
1243
+ if (!secret) throw new Error(`${path}: set ${secretName}, the issuer's Auth0 client secret.`);
1244
+ return auth0Directory({
1245
+ tenant: auth0.tenant,
1246
+ clientId: auth0.client_id,
1247
+ clientSecret: secret,
1248
+ domain: file.domain
1249
+ });
1250
+ }
1251
+ if ("okta" in directory) {
1252
+ const { okta } = directory;
1253
+ const keyName = okta.private_key_env ?? "OKTA_PRIVATE_KEY";
1254
+ const privateKey = env[keyName];
1255
+ if (!privateKey)
1256
+ throw new Error(`${path}: set ${keyName}, the issuer's Okta private key (PEM).`);
1257
+ return oktaDirectory({
1258
+ org: okta.org,
1259
+ clientId: okta.client_id,
1260
+ privateKey,
1261
+ keyId: okta.key_id,
1262
+ domain: file.domain
1263
+ });
1264
+ }
706
1265
  if ("static" in directory && Array.isArray(directory.static)) {
707
1266
  for (const agent of directory.static) {
708
1267
  if (!agent.address?.toLowerCase().endsWith(`@${file.domain.toLowerCase()}`)) {
@@ -711,11 +1270,13 @@ function directoryFrom(file, path, env) {
711
1270
  }
712
1271
  return staticDirectory(directory.static);
713
1272
  }
714
- throw new Error(`${path}: \`directory\` is { "keycloak": {...} } or { "static": [...] }.`);
1273
+ throw new Error(
1274
+ `${path}: \`directory\` is { "keycloak": {...} }, { "auth0": {...} }, { "okta": {...} } or { "static": [...] }.`
1275
+ );
715
1276
  }
716
1277
 
717
1278
  // src/issuer.ts
718
- import { createHash as createHash3, createPublicKey, randomUUID, verify as verify2 } from "crypto";
1279
+ import { createHash as createHash4, createPublicKey as createPublicKey3, randomBytes as randomBytes3, randomUUID as randomUUID2, verify as verify4 } from "crypto";
719
1280
  var TOKEN_EXCHANGE2 = "urn:ietf:params:oauth:grant-type:token-exchange";
720
1281
  var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
721
1282
  var TOKEN_TYPE_ID = "urn:ietf:params:oauth:token-type:id_token";
@@ -726,6 +1287,9 @@ var PATHS = {
726
1287
  card: "/.well-known/post/card",
727
1288
  directory: "/.well-known/http-message-signatures-directory",
728
1289
  token: "/api/post/oidc/token",
1290
+ authorize: "/api/post/oidc/authorize",
1291
+ /** The issuer's own: where the company's identity provider sends a person back. Cloud has none. */
1292
+ callback: "/api/post/oidc/callback",
729
1293
  jwks: "/api/post/oidc/jwks",
730
1294
  grants: "/api/post/grants"
731
1295
  };
@@ -736,8 +1300,8 @@ var NEW_ADDRESS_DAYS = 30;
736
1300
  var EXCHANGE_SIGNATURE_SECONDS = 600;
737
1301
  var EXCHANGE_COMPONENTS = ["@method", "@authority", "@path", "from", "content-digest"];
738
1302
  var GRANT_HEADER = "post-grant";
739
- var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
740
- var encode = (value) => b64url2(JSON.stringify(value));
1303
+ var b64url3 = (bytes) => Buffer.from(bytes).toString("base64url");
1304
+ var encode = (value) => b64url3(JSON.stringify(value));
741
1305
  var iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
742
1306
  function memoryReplayStore(now = () => Date.now()) {
743
1307
  const seen = /* @__PURE__ */ new Map();
@@ -768,10 +1332,12 @@ function createIssuer(options) {
768
1332
  const replay = options.replay ?? memoryReplayStore(now);
769
1333
  const organisation = options.organisation ?? domain;
770
1334
  const nowSeconds = () => Math.floor(now() / 1e3);
1335
+ const person = options.person ? { ...options.person, secret: checkSecret(options.person.secret) } : null;
1336
+ const upstream = person ? provider(person.oidc, fetcher, now) : null;
771
1337
  async function jws(header, payload) {
772
1338
  const input = `${encode({ ...header, kid: options.signer.kid })}.${encode(payload)}`;
773
1339
  const signature = await options.signer.sign(new TextEncoder().encode(input));
774
- return `${input}.${b64url2(signature)}`;
1340
+ return `${input}.${b64url3(signature)}`;
775
1341
  }
776
1342
  function readOurs(token, typ) {
777
1343
  const parts = token.replace(/\s+/g, "").split(".");
@@ -781,11 +1347,11 @@ function createIssuer(options) {
781
1347
  if (header?.alg !== "EdDSA" || header?.typ !== typ) return null;
782
1348
  const key = publishedIssuerKeys().find((candidate) => candidate.kid === header.kid);
783
1349
  if (!key) return null;
784
- const signed = verify2(
1350
+ const signed = verify4(
785
1351
  null,
786
1352
  Buffer.from(`${parts[0]}.${parts[1]}`),
787
- createPublicKey({
788
- key: { kty: "OKP", crv: "Ed25519", x: b64url2(key.publicKey) },
1353
+ createPublicKey3({
1354
+ key: { kty: "OKP", crv: "Ed25519", x: b64url3(key.publicKey) },
789
1355
  format: "jwk"
790
1356
  }),
791
1357
  Buffer.from(parts[2], "base64url")
@@ -812,7 +1378,7 @@ function createIssuer(options) {
812
1378
  ...publishedIssuerKeys().map((key) => ({
813
1379
  kid: key.kid,
814
1380
  alg: "Ed25519",
815
- pub: b64url2(key.publicKey),
1381
+ pub: b64url3(key.publicKey),
816
1382
  not_after: iso(key.notAfter)
817
1383
  })),
818
1384
  ...unique(agents.map((agent) => agent.key)).map((x) => ({
@@ -852,7 +1418,7 @@ function createIssuer(options) {
852
1418
  keys: publishedIssuerKeys().map((key) => ({
853
1419
  kty: "OKP",
854
1420
  crv: "Ed25519",
855
- x: b64url2(key.publicKey),
1421
+ x: b64url3(key.publicKey),
856
1422
  kid: key.kid,
857
1423
  alg: "EdDSA",
858
1424
  use: "sig"
@@ -864,9 +1430,17 @@ function createIssuer(options) {
864
1430
  issuer,
865
1431
  token_endpoint: `${issuer}${PATHS.token}`,
866
1432
  jwks_uri: `${issuer}${PATHS.jwks}`,
867
- grant_types_supported: [TOKEN_EXCHANGE2],
868
- // No person signs in here yet: there is no authorization endpoint.
869
- response_types_supported: [],
1433
+ ...person ? {
1434
+ authorization_endpoint: `${issuer}${PATHS.authorize}`,
1435
+ grant_types_supported: ["authorization_code", TOKEN_EXCHANGE2],
1436
+ response_types_supported: ["code"],
1437
+ code_challenge_methods_supported: ["S256"],
1438
+ scopes_supported: ["openid", "email"]
1439
+ } : {
1440
+ grant_types_supported: [TOKEN_EXCHANGE2],
1441
+ // No person signs in here: there is no authorization endpoint.
1442
+ response_types_supported: []
1443
+ },
870
1444
  subject_types_supported: ["public"],
871
1445
  id_token_signing_alg_values_supported: ["EdDSA"],
872
1446
  token_endpoint_auth_methods_supported: ["none"],
@@ -886,6 +1460,8 @@ function createIssuer(options) {
886
1460
  "standing",
887
1461
  "card",
888
1462
  "grant",
1463
+ "grants",
1464
+ "nonce",
889
1465
  "cnf",
890
1466
  "act",
891
1467
  "sub_type"
@@ -894,7 +1470,7 @@ function createIssuer(options) {
894
1470
  };
895
1471
  }
896
1472
  function record() {
897
- const id = createHash3("sha256").update(`post policy id\0self-hosted\0${domain}`).digest("hex").slice(0, 16);
1473
+ const id = createHash4("sha256").update(`post policy id\0self-hosted\0${domain}`).digest("hex").slice(0, 16);
898
1474
  return `v=post0; id=${id}`;
899
1475
  }
900
1476
  const cardUrl = (address) => `${issuer}${PATHS.card}?address=${encodeURIComponent(address)}`;
@@ -931,7 +1507,7 @@ function createIssuer(options) {
931
1507
  const claims = cardClaims(agent);
932
1508
  return { jws: await jws({ alg: "EdDSA", typ: "agent-card+jwt" }, claims), card: claims };
933
1509
  }
934
- const grantId = (agent, rule) => `grt_${createHash3("sha256").update(
1510
+ const grantId = (agent, rule) => `grt_${createHash4("sha256").update(
935
1511
  `${agent.sub}\0${rule.group}\0${rule.scope.name}\0${rule.subject.type}:${rule.subject.id}`
936
1512
  ).digest("base64url").slice(0, 22)}`;
937
1513
  const rulesFor = (agent) => (options.grants ?? []).filter((rule) => agent.groups.includes(rule.group));
@@ -977,20 +1553,25 @@ function createIssuer(options) {
977
1553
  const clientCache = /* @__PURE__ */ new Map();
978
1554
  async function clientHost(clientId) {
979
1555
  if (!clientId) throw new Refused("invalid_target", "audience is the site's client_id.");
1556
+ return (await site(clientId)).host;
1557
+ }
1558
+ async function site(clientId) {
980
1559
  const listed = options.clients?.find((client) => client.client_id === clientId);
981
- if (listed) return listed.host.toLowerCase();
1560
+ if (listed) {
1561
+ return { host: listed.host.toLowerCase(), redirectUris: listed.redirect_uris ?? [] };
1562
+ }
982
1563
  let url;
983
1564
  try {
984
1565
  url = new URL(clientId);
985
1566
  } catch {
986
1567
  throw new Refused("invalid_target", `${clientId} is not a site this issuer knows.`);
987
1568
  }
988
- const allowed = (options.clientMetadataHosts ?? []).map((host2) => host2.toLowerCase());
1569
+ const allowed = (options.clientMetadataHosts ?? []).map((host) => host.toLowerCase());
989
1570
  if (url.protocol !== "https:" || !allowed.includes(url.hostname.toLowerCase())) {
990
1571
  throw new Refused("invalid_target", `${clientId} is not a site this issuer knows.`);
991
1572
  }
992
1573
  const held = clientCache.get(clientId);
993
- if (held && held.at > now() - 3e5) return held.host;
1574
+ if (held && held.at > now() - 3e5) return held.site;
994
1575
  const response = await fetcher(clientId, {
995
1576
  redirect: "error",
996
1577
  headers: { accept: "application/json" },
@@ -1002,16 +1583,20 @@ function createIssuer(options) {
1002
1583
  if (text2.length <= 64 * 1024) document = JSON.parse(text2);
1003
1584
  } catch {
1004
1585
  }
1005
- const redirect = Array.isArray(document.redirect_uris) ? document.redirect_uris[0] : null;
1006
- if (document.client_id !== clientId || typeof redirect !== "string") {
1586
+ const redirects = Array.isArray(document.redirect_uris) ? document.redirect_uris.filter((uri) => typeof uri === "string") : [];
1587
+ if (document.client_id !== clientId || redirects.length === 0) {
1588
+ throw new Refused("invalid_target", `${clientId} does not describe a site.`);
1589
+ }
1590
+ let found;
1591
+ try {
1592
+ found = { host: new URL(redirects[0]).hostname.toLowerCase(), redirectUris: redirects };
1593
+ } catch {
1007
1594
  throw new Refused("invalid_target", `${clientId} does not describe a site.`);
1008
1595
  }
1009
- const host = new URL(redirect).hostname.toLowerCase();
1010
- clientCache.set(clientId, { at: now(), host });
1011
- return host;
1596
+ clientCache.set(clientId, { at: now(), site: found });
1597
+ return found;
1012
1598
  }
1013
- async function exchange(request) {
1014
- const body = await request.text();
1599
+ async function exchange(request, body) {
1015
1600
  const form = new URLSearchParams(body);
1016
1601
  if (form.get("grant_type") !== TOKEN_EXCHANGE2) {
1017
1602
  throw new Refused("unsupported_grant_type", `Use ${TOKEN_EXCHANGE2}.`);
@@ -1113,7 +1698,7 @@ function createIssuer(options) {
1113
1698
  }
1114
1699
  const audience = form.get("audience");
1115
1700
  const iat = nowSeconds();
1116
- const jti = randomUUID();
1701
+ const jti = randomUUID2();
1117
1702
  const cnf = { jwk: { kty: "OKP", crv: "Ed25519", x: live.key } };
1118
1703
  const fresh = now() - Date.parse(live.createdAt) < NEW_ADDRESS_DAYS * 864e5;
1119
1704
  const idToken = await jws(
@@ -1211,6 +1796,170 @@ function createIssuer(options) {
1211
1796
  const grants = await grantsFor(agent);
1212
1797
  return json({ object: "list", data: grants.map((grant) => ({ object: "grant", ...grant })) });
1213
1798
  }
1799
+ const callbackUrl = `${issuer}${PATHS.callback}`;
1800
+ const random = () => b64url3(randomBytes3(32));
1801
+ function flowCookie(value, maxAge) {
1802
+ const secure = issuer.startsWith("https:") ? "; Secure" : "";
1803
+ return `${FLOW_COOKIE}=${value}; Path=${PATHS.callback}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax${secure}`;
1804
+ }
1805
+ function backToSite(redirectUri, state, values, headers = {}) {
1806
+ const url = new URL(redirectUri);
1807
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, value);
1808
+ if (state) url.searchParams.set("state", state);
1809
+ url.searchParams.set("iss", issuer);
1810
+ return new Response(null, {
1811
+ status: 302,
1812
+ headers: { location: url.toString(), "cache-control": "no-store", ...headers }
1813
+ });
1814
+ }
1815
+ async function authorize(url) {
1816
+ const params = url.searchParams;
1817
+ for (const name of ["client_id", "redirect_uri", "state", "nonce", "code_challenge"]) {
1818
+ if (params.getAll(name).length > 1) return page(`${name} is given more than once`);
1819
+ }
1820
+ const clientId = params.get("client_id");
1821
+ const redirectUri = params.get("redirect_uri");
1822
+ if (!clientId || !redirectUri) return page("client_id and redirect_uri are required");
1823
+ const known = await site(clientId).catch((error) => {
1824
+ if (error instanceof Refused) return null;
1825
+ throw error;
1826
+ });
1827
+ if (!known) return page(`${clientId} is not a site this issuer knows`);
1828
+ if (!known.redirectUris.includes(redirectUri)) {
1829
+ return page(`${redirectUri} is not where ${clientId} receives sign-ins`);
1830
+ }
1831
+ const state = params.get("state");
1832
+ const fail = (error, description) => backToSite(redirectUri, state, { error, error_description: description });
1833
+ if (params.get("response_type") !== "code") {
1834
+ return fail("unsupported_response_type", "response_type is code.");
1835
+ }
1836
+ const scope = params.get("scope") ?? "";
1837
+ if (!scope.split(" ").includes("openid")) {
1838
+ return fail("invalid_scope", "scope includes openid.");
1839
+ }
1840
+ const challenge = params.get("code_challenge");
1841
+ if (!challenge || !/^[A-Za-z0-9_-]{43}$/.test(challenge) || params.get("code_challenge_method") !== "S256") {
1842
+ return fail("invalid_request", "PKCE is required: code_challenge with S256.");
1843
+ }
1844
+ const flow = {
1845
+ site: { clientId, redirectUri, state, nonce: params.get("nonce"), challenge, scope },
1846
+ upstream: { state: random(), nonce: random(), verifier: random() },
1847
+ exp: nowSeconds() + FLOW_SECONDS
1848
+ };
1849
+ const location = await upstream.authorizeUrl({
1850
+ redirectUri: callbackUrl,
1851
+ state: flow.upstream.state,
1852
+ nonce: flow.upstream.nonce,
1853
+ verifier: flow.upstream.verifier
1854
+ });
1855
+ return new Response(null, {
1856
+ status: 302,
1857
+ headers: {
1858
+ location,
1859
+ "cache-control": "no-store",
1860
+ "set-cookie": flowCookie(seal(person.secret, "flow", flow), FLOW_SECONDS)
1861
+ }
1862
+ });
1863
+ }
1864
+ async function callback(request, url) {
1865
+ const flow = unseal(person.secret, "flow", cookieOf(request, FLOW_COOKIE));
1866
+ const clear = { "set-cookie": flowCookie("", 0) };
1867
+ if (!flow || flow.exp <= nowSeconds() || url.searchParams.get("state") !== flow.upstream.state) {
1868
+ return page("this sign-in was not started in this browser. Start again from the site", clear);
1869
+ }
1870
+ const back = (values) => backToSite(flow.site.redirectUri, flow.site.state, values, clear);
1871
+ const code = url.searchParams.get("code");
1872
+ if (url.searchParams.get("error") || !code) {
1873
+ return back({ error: "access_denied", error_description: "The person did not sign in." });
1874
+ }
1875
+ let who;
1876
+ try {
1877
+ who = await upstream.redeem({
1878
+ code,
1879
+ redirectUri: callbackUrl,
1880
+ verifier: flow.upstream.verifier,
1881
+ nonce: flow.upstream.nonce
1882
+ });
1883
+ } catch (error) {
1884
+ if (!(error instanceof ProviderRefused)) throw error;
1885
+ return back({ error: "access_denied", error_description: error.message });
1886
+ }
1887
+ const domains = (person.emailDomains ?? [domain]).map((one) => one.toLowerCase());
1888
+ if (!domains.includes(who.email.slice(who.email.lastIndexOf("@") + 1))) {
1889
+ return back({
1890
+ error: "access_denied",
1891
+ error_description: `${who.email} is not an address this issuer signs people in with.`
1892
+ });
1893
+ }
1894
+ if ((await options.directory.agents()).some((agent) => agent.address === who.email)) {
1895
+ return back({
1896
+ error: "access_denied",
1897
+ error_description: `${who.email} is an agent's address, not a person's.`
1898
+ });
1899
+ }
1900
+ const sealed = {
1901
+ jti: randomUUID2(),
1902
+ clientId: flow.site.clientId,
1903
+ redirectUri: flow.site.redirectUri,
1904
+ challenge: flow.site.challenge,
1905
+ nonce: flow.site.nonce,
1906
+ scope: flow.site.scope,
1907
+ sub: who.sub,
1908
+ email: who.email,
1909
+ authTime: who.authTime,
1910
+ exp: nowSeconds() + CODE_SECONDS
1911
+ };
1912
+ return back({ code: seal(person.secret, "code", sealed) });
1913
+ }
1914
+ async function redeemCode(form) {
1915
+ const verifier = form.get("code_verifier");
1916
+ const clientId = form.get("client_id");
1917
+ if (!verifier || !clientId) {
1918
+ throw new Refused("invalid_request", "code, code_verifier and client_id are required.");
1919
+ }
1920
+ const code = unseal(person.secret, "code", form.get("code"));
1921
+ if (!code) throw new Refused("invalid_grant", "That code is not valid.");
1922
+ if (code.clientId !== clientId) {
1923
+ throw new Refused("invalid_grant", "That code was issued to a different client.");
1924
+ }
1925
+ if (!await replay.use(`code ${code.jti}`, code.exp + 60)) {
1926
+ throw new Refused("invalid_grant", "That code has already been used.");
1927
+ }
1928
+ if (code.exp <= nowSeconds()) throw new Refused("invalid_grant", "That code has expired.");
1929
+ const redirectUri = form.get("redirect_uri");
1930
+ if (redirectUri !== null && redirectUri !== code.redirectUri) {
1931
+ throw new Refused("invalid_grant", "redirect_uri does not match the authorization request.");
1932
+ }
1933
+ if (!pkceHolds(verifier, code.challenge)) {
1934
+ throw new Refused("invalid_grant", "code_verifier does not match the code_challenge.");
1935
+ }
1936
+ const iat = nowSeconds();
1937
+ const idToken = await jws(
1938
+ { alg: "EdDSA", typ: "JWT" },
1939
+ {
1940
+ iss: issuer,
1941
+ aud: clientId,
1942
+ iat,
1943
+ exp: iat + ID_TOKEN_SECONDS,
1944
+ auth_time: code.authTime,
1945
+ ...code.nonce ? { nonce: code.nonce } : {},
1946
+ jti: randomUUID2(),
1947
+ sub: code.sub,
1948
+ kind: "person",
1949
+ email: code.email,
1950
+ email_verified: true,
1951
+ // Grants here are the organisation's, to its agents; a person gives none at this issuer.
1952
+ grants: []
1953
+ }
1954
+ );
1955
+ return json({
1956
+ access_token: idToken,
1957
+ token_type: "Bearer",
1958
+ expires_in: ID_TOKEN_SECONDS,
1959
+ id_token: idToken,
1960
+ scope: code.scope
1961
+ });
1962
+ }
1214
1963
  async function handle(request) {
1215
1964
  const url = new URL(request.url);
1216
1965
  const path = url.pathname;
@@ -1237,7 +1986,16 @@ function createIssuer(options) {
1237
1986
  300
1238
1987
  );
1239
1988
  }
1240
- if (request.method === "POST" && path === PATHS.token) return await exchange(request);
1989
+ if (request.method === "POST" && path === PATHS.token) {
1990
+ const body = await request.text();
1991
+ const form = new URLSearchParams(body);
1992
+ if (person && form.get("grant_type") === "authorization_code") {
1993
+ return await redeemCode(form);
1994
+ }
1995
+ return await exchange(request, body);
1996
+ }
1997
+ if (person && get && path === PATHS.authorize) return await authorize(url);
1998
+ if (person && get && path === PATHS.callback) return await callback(request, url);
1241
1999
  if (get && path === PATHS.grants) return await listGrants(request);
1242
2000
  const status = /^\/api\/post\/grants\/(grt_[A-Za-z0-9_-]+)\/status$/.exec(path);
1243
2001
  if (get && status) {
@@ -1266,6 +2024,17 @@ function createIssuer(options) {
1266
2024
  function unique(values) {
1267
2025
  return [...new Set(values)];
1268
2026
  }
2027
+ function page(reason, headers = {}) {
2028
+ return new Response(`This sign-in can\u2019t go on: ${reason}.
2029
+ `, {
2030
+ status: 400,
2031
+ headers: {
2032
+ "content-type": "text/plain; charset=utf-8",
2033
+ "cache-control": "no-store",
2034
+ ...headers
2035
+ }
2036
+ });
2037
+ }
1269
2038
  function json(value, maxAge = 0, status = 200) {
1270
2039
  return text(JSON.stringify(value), "application/json", maxAge, status);
1271
2040
  }
@@ -1414,7 +2183,11 @@ ${HELP}`);
1414
2183
  export {
1415
2184
  staticDirectory,
1416
2185
  cachedDirectory,
2186
+ auth0Directory,
1417
2187
  keycloakDirectory,
2188
+ oktaDirectory,
2189
+ kmsSigner,
2190
+ kmsRetiredKey,
1418
2191
  fileSigner,
1419
2192
  seedSigner,
1420
2193
  loadConfig,
@@ -1426,4 +2199,4 @@ export {
1426
2199
  UsageError2 as UsageError,
1427
2200
  runIssuer
1428
2201
  };
1429
- //# sourceMappingURL=chunk-CCOFMK5K.js.map
2202
+ //# sourceMappingURL=chunk-5GZMAASA.js.map