@pidgeon-ai/issuer 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.
@@ -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,229 +213,125 @@ function keycloakDirectory(options) {
128
213
  };
129
214
  }
130
215
 
131
- // src/person.ts
132
- import {
133
- createCipheriv,
134
- createDecipheriv,
135
- createHash,
136
- createPublicKey,
137
- randomBytes,
138
- timingSafeEqual,
139
- verify
140
- } from "crypto";
141
- var MIN_SECRET = 32;
142
- var FLOW_COOKIE = "pidgeon_issuer_flow";
143
- var CODE_SECONDS = 60;
144
- var FLOW_SECONDS = 600;
145
- var SKEW = 60;
146
- var UPSTREAM_ALGS = ["RS256", "ES256", "EdDSA"];
147
- function checkSecret(secret) {
148
- if (!secret || secret.length < MIN_SECRET) {
149
- throw new Error(
150
- `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.`
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
151
245
  );
246
+ return `${input}.${signature.toString("base64url")}`;
152
247
  }
153
- return secret;
154
- }
155
- var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
156
- function sealKey(secret, purpose) {
157
- return createHash("sha256").update(`pidgeon issuer seal\0${purpose}\0${secret}`).digest();
158
- }
159
- function seal(secret, purpose, value) {
160
- const iv = randomBytes(12);
161
- const cipher = createCipheriv("aes-256-gcm", sealKey(secret, purpose), iv);
162
- cipher.setAAD(Buffer.from(purpose));
163
- const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
164
- return b64url(Buffer.concat([iv, body, cipher.getAuthTag()]));
165
- }
166
- function unseal(secret, purpose, token) {
167
- if (!token) return null;
168
- try {
169
- const raw = Buffer.from(token, "base64url");
170
- if (raw.length < 29) return null;
171
- const decipher = createDecipheriv("aes-256-gcm", sealKey(secret, purpose), raw.subarray(0, 12));
172
- decipher.setAAD(Buffer.from(purpose));
173
- decipher.setAuthTag(raw.subarray(raw.length - 16));
174
- const text2 = Buffer.concat([
175
- decipher.update(raw.subarray(12, raw.length - 16)),
176
- decipher.final()
177
- ]).toString("utf8");
178
- return JSON.parse(text2);
179
- } catch {
180
- return null;
181
- }
182
- }
183
- function pkceChallenge(verifier) {
184
- return b64url(createHash("sha256").update(verifier).digest());
185
- }
186
- function pkceHolds(verifier, challenge) {
187
- const a = Buffer.from(pkceChallenge(verifier));
188
- const b = Buffer.from(challenge);
189
- return a.length === b.length && timingSafeEqual(a, b);
190
- }
191
- function cookieOf(request, name) {
192
- for (const part of (request.headers.get("cookie") ?? "").split(";")) {
193
- const eq = part.indexOf("=");
194
- if (eq > 0 && part.slice(0, eq).trim() === name) {
195
- return decodeURIComponent(part.slice(eq + 1).trim());
196
- }
197
- }
198
- return null;
199
- }
200
- var ProviderRefused = class extends Error {
201
- };
202
- function provider(options, fetcher, now) {
203
- const issuer = options.issuer.replace(/\/$/, "");
204
- let configuration = null;
205
- let keys = null;
206
- function discover() {
207
- if (!configuration || configuration.at <= now() - 36e5) {
208
- const url = options.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`;
209
- const value = (async () => {
210
- const response = await fetcher(url, { headers: { accept: "application/json" } });
211
- if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
212
- const body = await response.json();
213
- if (body.issuer?.replace(/\/$/, "") !== issuer) {
214
- throw new Error(`${url} names the issuer ${body.issuer}, not ${issuer}.`);
215
- }
216
- return body;
217
- })();
218
- configuration = { at: now(), value };
219
- value.catch(() => {
220
- if (configuration?.value === value) configuration = null;
221
- });
222
- }
223
- return configuration.value;
224
- }
225
- async function key(kid) {
226
- const find = () => keys?.value.find((one) => (kid ? one.kid === kid : true) && one.use !== "enc") ?? null;
227
- const fresh = keys && keys.at > now() - 3e5;
228
- if (fresh && find()) return find();
229
- if (keys && keys.at > now() - 1e4) return null;
230
- const response = await fetcher((await discover()).jwks_uri);
231
- const body = await response.json().catch(() => ({}));
232
- keys = { at: now(), value: Array.isArray(body.keys) ? body.keys : [] };
233
- return find();
234
- }
235
- async function authorizeUrl(input) {
236
- const url = new URL((await discover()).authorization_endpoint);
237
- url.search = new URLSearchParams({
238
- response_type: "code",
239
- client_id: options.clientId,
240
- redirect_uri: input.redirectUri,
241
- scope: "openid email",
242
- state: input.state,
243
- nonce: input.nonce,
244
- code_challenge: pkceChallenge(input.verifier),
245
- code_challenge_method: "S256"
246
- }).toString();
247
- return url.toString();
248
- }
249
- async function redeem(input) {
250
- const form = new URLSearchParams({
251
- grant_type: "authorization_code",
252
- code: input.code,
253
- redirect_uri: input.redirectUri,
254
- code_verifier: input.verifier
255
- });
256
- const headers = {
257
- "content-type": "application/x-www-form-urlencoded",
258
- accept: "application/json"
259
- };
260
- if (options.tokenAuth === "client_secret_post") {
261
- form.set("client_id", options.clientId);
262
- form.set("client_secret", options.clientSecret);
263
- } else {
264
- const pair = `${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`;
265
- headers.authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
266
- }
267
- const response = await fetcher((await discover()).token_endpoint, {
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, {
268
252
  method: "POST",
269
- headers,
270
- body: form
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
+ })
271
263
  });
272
264
  const body = await response.json().catch(() => ({}));
273
- if (!response.ok || typeof body.id_token !== "string") {
274
- throw new ProviderRefused(
275
- `The identity provider did not sign the person in (${body.error ?? response.status}).`
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}` : ""}).`
276
268
  );
277
269
  }
278
- return check(body.id_token, input.nonce);
270
+ token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
271
+ return token.value;
279
272
  }
280
- async function check(token, nonce) {
281
- const parts = token.split(".");
282
- if (parts.length !== 3) throw new ProviderRefused("The provider sent no ID token.");
283
- let header;
284
- let claims;
285
- try {
286
- header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
287
- claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
288
- } catch {
289
- throw new ProviderRefused("The provider sent no ID token.");
290
- }
291
- if (!header.alg || !UPSTREAM_ALGS.includes(header.alg)) {
292
- throw new ProviderRefused(`The provider's ID token is signed with ${header.alg}.`);
293
- }
294
- const jwk = await key(header.kid);
295
- if (!jwk || !signatureHolds(header.alg, jwk, `${parts[0]}.${parts[1]}`, parts[2])) {
296
- throw new ProviderRefused("The provider's ID token is not signed by its keys.");
297
- }
298
- const at = Math.floor(now() / 1e3);
299
- if (typeof claims.iss !== "string" || claims.iss.replace(/\/$/, "") !== issuer) {
300
- throw new ProviderRefused("The provider's ID token is from another issuer.");
301
- }
302
- const aud = claims.aud;
303
- const audiences = Array.isArray(aud) ? aud : [aud];
304
- if (!audiences.includes(options.clientId)) {
305
- throw new ProviderRefused("The provider's ID token is for another client.");
306
- }
307
- if (audiences.length > 1 && claims.azp !== options.clientId) {
308
- throw new ProviderRefused("The provider's ID token is for another client.");
309
- }
310
- if (typeof claims.exp !== "number" || claims.exp + SKEW <= at) {
311
- throw new ProviderRefused("The provider's ID token has expired.");
312
- }
313
- if (typeof claims.iat === "number" && claims.iat > at + SKEW) {
314
- throw new ProviderRefused("The provider's ID token is from the future.");
315
- }
316
- if (claims.nonce !== nonce) {
317
- throw new ProviderRefused("The provider's ID token is for another sign-in.");
318
- }
319
- if (typeof claims.sub !== "string" || !claims.sub) {
320
- throw new ProviderRefused("The provider's ID token names nobody.");
321
- }
322
- if (claims.email_verified !== true || typeof claims.email !== "string") {
323
- throw new ProviderRefused("The identity provider has not verified this person\u2019s address.");
324
- }
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;
325
290
  return {
326
- sub: claims.sub,
327
- email: claims.email.trim().toLowerCase(),
328
- authTime: typeof claims.auth_time === "number" ? claims.auth_time : at
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()
329
297
  };
330
298
  }
331
- return { authorizeUrl, redeem };
332
- }
333
- function signatureHolds(alg, jwk, input, signature) {
334
- try {
335
- const key = createPublicKey({ key: jwk, format: "jwk" });
336
- const data = Buffer.from(input);
337
- const sig = Buffer.from(signature, "base64url");
338
- if (alg === "EdDSA") return jwk.kty === "OKP" && verify(null, data, key, sig);
339
- if (alg === "ES256") {
340
- return jwk.kty === "EC" && jwk.crv === "P-256" && verify("sha256", data, { key, dsaEncoding: "ieee-p1363" }, sig);
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;
341
325
  }
342
- return jwk.kty === "RSA" && verify("sha256", data, key, sig);
343
- } catch {
344
- return false;
345
- }
326
+ };
346
327
  }
347
328
 
348
- // src/signer.ts
349
- import { readFile } from "fs/promises";
329
+ // src/kms.ts
330
+ import { createPublicKey, verify as verify2 } from "crypto";
350
331
 
351
332
  // ../id/dist/chunk-QDQDZSU7.js
352
- import { createHash as createHash2 } from "crypto";
353
- import { createHash as createHash22, randomBytes as randomBytes2 } from "crypto";
333
+ import { createHash } from "crypto";
334
+ import { createHash as createHash2, randomBytes } from "crypto";
354
335
  var __defProp = Object.defineProperty;
355
336
  var __export = (target, all) => {
356
337
  for (var name in all)
@@ -365,8 +346,8 @@ __export(post_core_wasm_exports, {
365
346
  policyDecision: () => policyDecision,
366
347
  policyMaxAge: () => policyMaxAge,
367
348
  publicKey: () => publicKey,
368
- sign: () => sign,
369
- verify: () => verify2
349
+ sign: () => sign2,
350
+ verify: () => verify
370
351
  });
371
352
  function generateSeed() {
372
353
  try {
@@ -430,7 +411,7 @@ function publicKey(seed) {
430
411
  wasm.__wbindgen_add_to_stack_pointer(16);
431
412
  }
432
413
  }
433
- function sign(seed, message) {
414
+ function sign2(seed, message) {
434
415
  try {
435
416
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
436
417
  const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export3);
@@ -452,7 +433,7 @@ function sign(seed, message) {
452
433
  wasm.__wbindgen_add_to_stack_pointer(16);
453
434
  }
454
435
  }
455
- function verify2(public_key, message, signature) {
436
+ function verify(public_key, message, signature) {
456
437
  try {
457
438
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
458
439
  const ptr0 = passArray8ToWasm0(public_key, wasm.__wbindgen_export3);
@@ -697,9 +678,9 @@ function wasm2() {
697
678
  }
698
679
  return post_core_wasm_exports;
699
680
  }
700
- var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
681
+ var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
701
682
  function jwkThumbprint(publicKey2) {
702
- return createHash2("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url2(publicKey2)}"}`).digest("base64url");
683
+ return createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url(publicKey2)}"}`).digest("base64url");
703
684
  }
704
685
  function verifyBytes(publicKey2, message, signature) {
705
686
  try {
@@ -732,7 +713,7 @@ function bytesOf(body) {
732
713
  return typeof body === "string" ? new TextEncoder().encode(body) : body;
733
714
  }
734
715
  function contentDigest(body) {
735
- return `sha-256=:${createHash22("sha256").update(bytesOf(body)).digest("base64")}:`;
716
+ return `sha-256=:${createHash2("sha256").update(bytesOf(body)).digest("base64")}:`;
736
717
  }
737
718
  async function verifyRequest(input, options) {
738
719
  const headers = /* @__PURE__ */ new Map();
@@ -841,7 +822,307 @@ function splitTopLevel(value) {
841
822
  return out;
842
823
  }
843
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
+
844
1124
  // src/signer.ts
1125
+ import { readFile } from "fs/promises";
845
1126
  async function fileSigner(path) {
846
1127
  let file;
847
1128
  try {
@@ -884,12 +1165,21 @@ async function loadConfig(path, env = process.env) {
884
1165
  throw new Error(`${path}: \`domain\` is the domain whose agents this issues for.`);
885
1166
  }
886
1167
  const here = (relative) => resolve(dirname(path), relative);
887
- const signer = env.PIDGEON_ISSUER_KEY ? seedSigner(env.PIDGEON_ISSUER_KEY, { domain: file.domain }) : file.key ? await fileSigner(here(file.key)) : null;
888
- 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
+ }
889
1172
  const retired = [];
890
1173
  for (const entry of file.retired ?? []) {
891
- const old = await fileSigner(here(entry.key));
892
- 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
+ }
893
1183
  }
894
1184
  let person;
895
1185
  if (file.person) {
@@ -946,6 +1236,32 @@ function directoryFrom(file, path, env) {
946
1236
  group: keycloak.group
947
1237
  });
948
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
+ }
949
1265
  if ("static" in directory && Array.isArray(directory.static)) {
950
1266
  for (const agent of directory.static) {
951
1267
  if (!agent.address?.toLowerCase().endsWith(`@${file.domain.toLowerCase()}`)) {
@@ -954,11 +1270,13 @@ function directoryFrom(file, path, env) {
954
1270
  }
955
1271
  return staticDirectory(directory.static);
956
1272
  }
957
- throw new Error(`${path}: \`directory\` is { "keycloak": {...} } or { "static": [...] }.`);
1273
+ throw new Error(
1274
+ `${path}: \`directory\` is { "keycloak": {...} }, { "auth0": {...} }, { "okta": {...} } or { "static": [...] }.`
1275
+ );
958
1276
  }
959
1277
 
960
1278
  // src/issuer.ts
961
- import { createHash as createHash3, createPublicKey as createPublicKey2, randomBytes as randomBytes3, randomUUID, verify as verify3 } from "crypto";
1279
+ import { createHash as createHash4, createPublicKey as createPublicKey3, randomBytes as randomBytes3, randomUUID as randomUUID2, verify as verify4 } from "crypto";
962
1280
  var TOKEN_EXCHANGE2 = "urn:ietf:params:oauth:grant-type:token-exchange";
963
1281
  var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
964
1282
  var TOKEN_TYPE_ID = "urn:ietf:params:oauth:token-type:id_token";
@@ -1029,10 +1347,10 @@ function createIssuer(options) {
1029
1347
  if (header?.alg !== "EdDSA" || header?.typ !== typ) return null;
1030
1348
  const key = publishedIssuerKeys().find((candidate) => candidate.kid === header.kid);
1031
1349
  if (!key) return null;
1032
- const signed = verify3(
1350
+ const signed = verify4(
1033
1351
  null,
1034
1352
  Buffer.from(`${parts[0]}.${parts[1]}`),
1035
- createPublicKey2({
1353
+ createPublicKey3({
1036
1354
  key: { kty: "OKP", crv: "Ed25519", x: b64url3(key.publicKey) },
1037
1355
  format: "jwk"
1038
1356
  }),
@@ -1152,7 +1470,7 @@ function createIssuer(options) {
1152
1470
  };
1153
1471
  }
1154
1472
  function record() {
1155
- 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);
1156
1474
  return `v=post0; id=${id}`;
1157
1475
  }
1158
1476
  const cardUrl = (address) => `${issuer}${PATHS.card}?address=${encodeURIComponent(address)}`;
@@ -1189,7 +1507,7 @@ function createIssuer(options) {
1189
1507
  const claims = cardClaims(agent);
1190
1508
  return { jws: await jws({ alg: "EdDSA", typ: "agent-card+jwt" }, claims), card: claims };
1191
1509
  }
1192
- const grantId = (agent, rule) => `grt_${createHash3("sha256").update(
1510
+ const grantId = (agent, rule) => `grt_${createHash4("sha256").update(
1193
1511
  `${agent.sub}\0${rule.group}\0${rule.scope.name}\0${rule.subject.type}:${rule.subject.id}`
1194
1512
  ).digest("base64url").slice(0, 22)}`;
1195
1513
  const rulesFor = (agent) => (options.grants ?? []).filter((rule) => agent.groups.includes(rule.group));
@@ -1380,7 +1698,7 @@ function createIssuer(options) {
1380
1698
  }
1381
1699
  const audience = form.get("audience");
1382
1700
  const iat = nowSeconds();
1383
- const jti = randomUUID();
1701
+ const jti = randomUUID2();
1384
1702
  const cnf = { jwk: { kty: "OKP", crv: "Ed25519", x: live.key } };
1385
1703
  const fresh = now() - Date.parse(live.createdAt) < NEW_ADDRESS_DAYS * 864e5;
1386
1704
  const idToken = await jws(
@@ -1580,7 +1898,7 @@ function createIssuer(options) {
1580
1898
  });
1581
1899
  }
1582
1900
  const sealed = {
1583
- jti: randomUUID(),
1901
+ jti: randomUUID2(),
1584
1902
  clientId: flow.site.clientId,
1585
1903
  redirectUri: flow.site.redirectUri,
1586
1904
  challenge: flow.site.challenge,
@@ -1625,7 +1943,7 @@ function createIssuer(options) {
1625
1943
  exp: iat + ID_TOKEN_SECONDS,
1626
1944
  auth_time: code.authTime,
1627
1945
  ...code.nonce ? { nonce: code.nonce } : {},
1628
- jti: randomUUID(),
1946
+ jti: randomUUID2(),
1629
1947
  sub: code.sub,
1630
1948
  kind: "person",
1631
1949
  email: code.email,
@@ -1865,7 +2183,11 @@ ${HELP}`);
1865
2183
  export {
1866
2184
  staticDirectory,
1867
2185
  cachedDirectory,
2186
+ auth0Directory,
1868
2187
  keycloakDirectory,
2188
+ oktaDirectory,
2189
+ kmsSigner,
2190
+ kmsRetiredKey,
1869
2191
  fileSigner,
1870
2192
  seedSigner,
1871
2193
  loadConfig,
@@ -1877,4 +2199,4 @@ export {
1877
2199
  UsageError2 as UsageError,
1878
2200
  runIssuer
1879
2201
  };
1880
- //# sourceMappingURL=chunk-BXZ2EI2Y.js.map
2202
+ //# sourceMappingURL=chunk-5GZMAASA.js.map