@pidgeon-ai/issuer 0.2.0 → 0.4.1
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 +54 -3
- package/dist/bin.js +1 -1
- package/dist/{chunk-BXZ2EI2Y.js → chunk-IKGE6SNR.js} +573 -229
- package/dist/chunk-IKGE6SNR.js.map +1 -0
- package/dist/index.d.ts +166 -7
- package/dist/index.js +9 -1
- package/package.json +13 -2
- package/dist/chunk-BXZ2EI2Y.js.map +0 -1
|
@@ -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,104 @@ function cachedDirectory(inner, seconds = 60, now = () => Date.now()) {
|
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
// src/rate-limit.ts
|
|
34
|
+
async function withBackoff(attempt, now = () => Date.now(), sleep = (ms) => new Promise((done) => setTimeout(done, ms))) {
|
|
35
|
+
for (let tries = 0; ; tries++) {
|
|
36
|
+
const response = await attempt();
|
|
37
|
+
if (response.status !== 429 || tries >= 2) return response;
|
|
38
|
+
const reset = Number(
|
|
39
|
+
response.headers.get("x-ratelimit-reset") ?? response.headers.get("x-rate-limit-reset")
|
|
40
|
+
);
|
|
41
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
42
|
+
const wait = Number.isFinite(reset) && reset > 0 ? reset * 1e3 - now() : Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : 1e3;
|
|
43
|
+
await sleep(Math.min(2e3, Math.max(250, wait)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/auth0.ts
|
|
48
|
+
var PER_PAGE = 100;
|
|
49
|
+
function auth0Directory(options) {
|
|
50
|
+
const base = `https://${options.tenant.replace(/^https:\/\//, "").replace(/\/$/, "")}`;
|
|
51
|
+
const domain = options.domain.toLowerCase();
|
|
52
|
+
const fetcher = options.fetch ?? fetch;
|
|
53
|
+
const now = options.now ?? (() => Date.now());
|
|
54
|
+
let token = null;
|
|
55
|
+
async function accessToken() {
|
|
56
|
+
if (token && token.until > now()) return token.value;
|
|
57
|
+
const response = await fetcher(`${base}/oauth/token`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
60
|
+
body: new URLSearchParams({
|
|
61
|
+
grant_type: "client_credentials",
|
|
62
|
+
client_id: options.clientId,
|
|
63
|
+
client_secret: options.clientSecret,
|
|
64
|
+
audience: `${base}/api/v2/`
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
const body = await response.json().catch(() => ({}));
|
|
68
|
+
if (!response.ok || !body.access_token) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Auth0 refused the issuer's application ${options.clientId} (${response.status}).`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
|
|
74
|
+
return token.value;
|
|
75
|
+
}
|
|
76
|
+
async function management(path) {
|
|
77
|
+
const bearer = await accessToken();
|
|
78
|
+
const response = await withBackoff(
|
|
79
|
+
() => fetcher(`${base}/api/v2${path}`, {
|
|
80
|
+
headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }
|
|
81
|
+
}),
|
|
82
|
+
now
|
|
83
|
+
);
|
|
84
|
+
if (response.status === 404) return null;
|
|
85
|
+
if (!response.ok) throw new Error(`Auth0 answered ${response.status} for ${path}.`);
|
|
86
|
+
return await response.json();
|
|
87
|
+
}
|
|
88
|
+
function agentOf(client) {
|
|
89
|
+
if (client.app_type !== "non_interactive") return null;
|
|
90
|
+
const metadata = client.client_metadata ?? {};
|
|
91
|
+
if (metadata.pidgeon_disabled?.trim().toLowerCase() === "true") return null;
|
|
92
|
+
const key = metadata.pidgeon_key?.trim();
|
|
93
|
+
if (!key || Buffer.from(key, "base64url").length !== 32) return null;
|
|
94
|
+
const address = metadata.pidgeon_address?.trim().toLowerCase();
|
|
95
|
+
if (!address?.endsWith(`@${domain}`)) return null;
|
|
96
|
+
return {
|
|
97
|
+
sub: client.client_id,
|
|
98
|
+
address,
|
|
99
|
+
kind: metadata.pidgeon_kind?.trim() === "service" ? "service" : "agent",
|
|
100
|
+
key,
|
|
101
|
+
groups: (metadata.pidgeon_groups ?? "").split(",").map((group) => group.trim()).filter(Boolean),
|
|
102
|
+
// An age Auth0 does not give is treated as new: the card says `new`, never older than it is.
|
|
103
|
+
createdAt: new Date(client.created_at ? Date.parse(client.created_at) : now()).toISOString()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function all() {
|
|
107
|
+
const agents = [];
|
|
108
|
+
for (let page2 = 0; ; page2++) {
|
|
109
|
+
const body = await management(
|
|
110
|
+
`/clients?app_type=non_interactive&per_page=${PER_PAGE}&page=${page2}&include_totals=true`
|
|
111
|
+
);
|
|
112
|
+
const clients = body?.clients ?? [];
|
|
113
|
+
for (const client of clients) {
|
|
114
|
+
const agent = agentOf(client);
|
|
115
|
+
if (agent) agents.push(agent);
|
|
116
|
+
}
|
|
117
|
+
if (clients.length < PER_PAGE || (page2 + 1) * PER_PAGE >= (body?.total ?? 0)) break;
|
|
118
|
+
}
|
|
119
|
+
return agents;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
agents: async () => onePerAddress(await all()),
|
|
123
|
+
async agent(sub) {
|
|
124
|
+
const client = await management(`/clients/${encodeURIComponent(sub)}`);
|
|
125
|
+
const agent = client ? agentOf(client) : null;
|
|
126
|
+
return agent?.sub === sub ? agent : null;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
28
131
|
// src/keycloak.ts
|
|
29
132
|
var SERVICE_ACCOUNT_PREFIX = "service-account-";
|
|
30
133
|
var ATTRIBUTES = { key: "pidgeon.key", address: "pidgeon.address", kind: "pidgeon.kind" };
|
|
@@ -106,7 +209,7 @@ function keycloakDirectory(options) {
|
|
|
106
209
|
const clients = await admin("/clients?first=0&max=1000") ?? [];
|
|
107
210
|
const candidates = clients.filter((client) => keyOf(client) !== null);
|
|
108
211
|
const agents = await Promise.all(candidates.map(agentOf));
|
|
109
|
-
return agents.filter((agent) => agent !== null);
|
|
212
|
+
return onePerAddress(agents.filter((agent) => agent !== null));
|
|
110
213
|
},
|
|
111
214
|
/**
|
|
112
215
|
* The user's record does not name its client (only a realm export
|
|
@@ -128,229 +231,129 @@ function keycloakDirectory(options) {
|
|
|
128
231
|
};
|
|
129
232
|
}
|
|
130
233
|
|
|
131
|
-
// src/
|
|
132
|
-
import {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
234
|
+
// src/okta.ts
|
|
235
|
+
import { createPrivateKey, randomUUID, sign } from "crypto";
|
|
236
|
+
var OKTA_SCOPE = "okta.apps.read";
|
|
237
|
+
function oktaDirectory(options) {
|
|
238
|
+
const org = options.org.replace(/\/$/, "");
|
|
239
|
+
const domain = options.domain.toLowerCase();
|
|
240
|
+
const fetcher = options.fetch ?? fetch;
|
|
241
|
+
const now = options.now ?? (() => Date.now());
|
|
242
|
+
const privateKey = typeof options.privateKey === "string" ? createPrivateKey(options.privateKey) : options.privateKey;
|
|
243
|
+
const alg = privateKey.asymmetricKeyType === "rsa" ? "RS256" : privateKey.asymmetricKeyType === "ec" && privateKey.asymmetricKeyDetails?.namedCurve === "prime256v1" ? "ES256" : null;
|
|
244
|
+
if (!alg) throw new Error("Okta's client assertion is signed with an RSA or P-256 key.");
|
|
245
|
+
let token = null;
|
|
246
|
+
function assertion(audience) {
|
|
247
|
+
const at = Math.floor(now() / 1e3);
|
|
248
|
+
const b64 = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
249
|
+
const input = `${b64({ alg, typ: "JWT", ...options.keyId ? { kid: options.keyId } : {} })}.${b64(
|
|
250
|
+
{
|
|
251
|
+
iss: options.clientId,
|
|
252
|
+
sub: options.clientId,
|
|
253
|
+
aud: audience,
|
|
254
|
+
iat: at,
|
|
255
|
+
exp: at + 300,
|
|
256
|
+
jti: randomUUID()
|
|
257
|
+
}
|
|
258
|
+
)}`;
|
|
259
|
+
const signature = sign(
|
|
260
|
+
"sha256",
|
|
261
|
+
Buffer.from(input),
|
|
262
|
+
alg === "ES256" ? { key: privateKey, dsaEncoding: "ieee-p1363" } : privateKey
|
|
151
263
|
);
|
|
264
|
+
return `${input}.${signature.toString("base64url")}`;
|
|
152
265
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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, {
|
|
266
|
+
async function accessToken() {
|
|
267
|
+
if (token && token.until > now()) return token.value;
|
|
268
|
+
const endpoint = `${org}/oauth2/v1/token`;
|
|
269
|
+
const response = await fetcher(endpoint, {
|
|
268
270
|
method: "POST",
|
|
269
|
-
headers
|
|
270
|
-
|
|
271
|
+
headers: {
|
|
272
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
273
|
+
accept: "application/json"
|
|
274
|
+
},
|
|
275
|
+
body: new URLSearchParams({
|
|
276
|
+
grant_type: "client_credentials",
|
|
277
|
+
scope: OKTA_SCOPE,
|
|
278
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
279
|
+
client_assertion: assertion(endpoint)
|
|
280
|
+
})
|
|
271
281
|
});
|
|
272
282
|
const body = await response.json().catch(() => ({}));
|
|
273
|
-
if (!response.ok ||
|
|
274
|
-
throw new
|
|
275
|
-
`
|
|
283
|
+
if (!response.ok || !body.access_token) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Okta refused the issuer's app ${options.clientId} (${response.status}${body.error_description ? `: ${body.error_description}` : ""}).`
|
|
276
286
|
);
|
|
277
287
|
}
|
|
278
|
-
|
|
288
|
+
token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
|
|
289
|
+
return token.value;
|
|
279
290
|
}
|
|
280
|
-
async function
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
throw new
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
}
|
|
291
|
+
async function api(url) {
|
|
292
|
+
const bearer = await accessToken();
|
|
293
|
+
const response = await withBackoff(
|
|
294
|
+
() => fetcher(url, {
|
|
295
|
+
headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }
|
|
296
|
+
}),
|
|
297
|
+
now
|
|
298
|
+
);
|
|
299
|
+
if (!response.ok && response.status !== 404) {
|
|
300
|
+
throw new Error(`Okta answered ${response.status} for ${new URL(url).pathname}.`);
|
|
301
|
+
}
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
function agentOf(app) {
|
|
305
|
+
if (app.status !== "ACTIVE") return null;
|
|
306
|
+
if (app.settings?.oauthClient?.application_type !== "service") return null;
|
|
307
|
+
const pidgeon = app.profile?.pidgeon;
|
|
308
|
+
const key = typeof pidgeon?.key === "string" ? pidgeon.key.trim() : "";
|
|
309
|
+
if (!key || Buffer.from(key, "base64url").length !== 32) return null;
|
|
310
|
+
const address = typeof pidgeon?.address === "string" ? pidgeon.address.trim().toLowerCase() : "";
|
|
311
|
+
if (!address.endsWith(`@${domain}`)) return null;
|
|
325
312
|
return {
|
|
326
|
-
sub:
|
|
327
|
-
|
|
328
|
-
|
|
313
|
+
sub: app.id,
|
|
314
|
+
address,
|
|
315
|
+
kind: pidgeon?.kind === "service" ? "service" : "agent",
|
|
316
|
+
key,
|
|
317
|
+
groups: Array.isArray(pidgeon?.groups) ? pidgeon.groups.filter((group) => typeof group === "string") : [],
|
|
318
|
+
createdAt: new Date(app.created ? Date.parse(app.created) : now()).toISOString()
|
|
329
319
|
};
|
|
330
320
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
321
|
+
function nextPage(response) {
|
|
322
|
+
for (const part of (response.headers.get("link") ?? "").split(",")) {
|
|
323
|
+
const match = /<([^>]+)>\s*;\s*rel="next"/.exec(part);
|
|
324
|
+
if (match && new URL(match[1]).origin === new URL(org).origin) return match[1];
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
async agents() {
|
|
330
|
+
const agents = [];
|
|
331
|
+
let url = `${org}/api/v1/apps?filter=${encodeURIComponent('status eq "ACTIVE"')}&limit=200`;
|
|
332
|
+
for (let pages = 0; url && pages < 100; pages++) {
|
|
333
|
+
const response = await api(url);
|
|
334
|
+
for (const app of await response.json() ?? []) {
|
|
335
|
+
const agent = agentOf(app);
|
|
336
|
+
if (agent) agents.push(agent);
|
|
337
|
+
}
|
|
338
|
+
url = nextPage(response);
|
|
339
|
+
}
|
|
340
|
+
return onePerAddress(agents);
|
|
341
|
+
},
|
|
342
|
+
async agent(sub) {
|
|
343
|
+
const response = await api(`${org}/api/v1/apps/${encodeURIComponent(sub)}`);
|
|
344
|
+
if (response.status === 404) return null;
|
|
345
|
+
const agent = agentOf(await response.json());
|
|
346
|
+
return agent?.sub === sub ? agent : null;
|
|
341
347
|
}
|
|
342
|
-
|
|
343
|
-
} catch {
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
348
|
+
};
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
// src/
|
|
349
|
-
import {
|
|
351
|
+
// src/kms.ts
|
|
352
|
+
import { createPublicKey, verify as verify2 } from "crypto";
|
|
350
353
|
|
|
351
354
|
// ../id/dist/chunk-QDQDZSU7.js
|
|
352
|
-
import { createHash
|
|
353
|
-
import { createHash as
|
|
355
|
+
import { createHash } from "crypto";
|
|
356
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
354
357
|
var __defProp = Object.defineProperty;
|
|
355
358
|
var __export = (target, all) => {
|
|
356
359
|
for (var name in all)
|
|
@@ -365,8 +368,8 @@ __export(post_core_wasm_exports, {
|
|
|
365
368
|
policyDecision: () => policyDecision,
|
|
366
369
|
policyMaxAge: () => policyMaxAge,
|
|
367
370
|
publicKey: () => publicKey,
|
|
368
|
-
sign: () =>
|
|
369
|
-
verify: () =>
|
|
371
|
+
sign: () => sign2,
|
|
372
|
+
verify: () => verify
|
|
370
373
|
});
|
|
371
374
|
function generateSeed() {
|
|
372
375
|
try {
|
|
@@ -430,7 +433,7 @@ function publicKey(seed) {
|
|
|
430
433
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
431
434
|
}
|
|
432
435
|
}
|
|
433
|
-
function
|
|
436
|
+
function sign2(seed, message) {
|
|
434
437
|
try {
|
|
435
438
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
436
439
|
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_export3);
|
|
@@ -452,7 +455,7 @@ function sign(seed, message) {
|
|
|
452
455
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
453
456
|
}
|
|
454
457
|
}
|
|
455
|
-
function
|
|
458
|
+
function verify(public_key, message, signature) {
|
|
456
459
|
try {
|
|
457
460
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
458
461
|
const ptr0 = passArray8ToWasm0(public_key, wasm.__wbindgen_export3);
|
|
@@ -697,9 +700,9 @@ function wasm2() {
|
|
|
697
700
|
}
|
|
698
701
|
return post_core_wasm_exports;
|
|
699
702
|
}
|
|
700
|
-
var
|
|
703
|
+
var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
701
704
|
function jwkThumbprint(publicKey2) {
|
|
702
|
-
return
|
|
705
|
+
return createHash("sha256").update(`{"crv":"Ed25519","kty":"OKP","x":"${b64url(publicKey2)}"}`).digest("base64url");
|
|
703
706
|
}
|
|
704
707
|
function verifyBytes(publicKey2, message, signature) {
|
|
705
708
|
try {
|
|
@@ -732,7 +735,7 @@ function bytesOf(body) {
|
|
|
732
735
|
return typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
733
736
|
}
|
|
734
737
|
function contentDigest(body) {
|
|
735
|
-
return `sha-256=:${
|
|
738
|
+
return `sha-256=:${createHash2("sha256").update(bytesOf(body)).digest("base64")}:`;
|
|
736
739
|
}
|
|
737
740
|
async function verifyRequest(input, options) {
|
|
738
741
|
const headers = /* @__PURE__ */ new Map();
|
|
@@ -841,7 +844,307 @@ function splitTopLevel(value) {
|
|
|
841
844
|
return out;
|
|
842
845
|
}
|
|
843
846
|
|
|
847
|
+
// src/kms.ts
|
|
848
|
+
var KMS_KEY_SPEC = "ECC_NIST_EDWARDS25519";
|
|
849
|
+
var KMS_SIGNING_ALGORITHM = "ED25519_SHA_512";
|
|
850
|
+
async function sdkClient(region) {
|
|
851
|
+
let sdk;
|
|
852
|
+
try {
|
|
853
|
+
sdk = await import("@aws-sdk/client-kms");
|
|
854
|
+
} catch {
|
|
855
|
+
throw new Error(
|
|
856
|
+
"A KMS key needs @aws-sdk/client-kms: `npm install @aws-sdk/client-kms` beside the issuer."
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const client = new sdk.KMSClient(region ? { region } : {});
|
|
860
|
+
return {
|
|
861
|
+
getPublicKey: (input) => client.send(new sdk.GetPublicKeyCommand(input)),
|
|
862
|
+
sign: (input) => client.send(
|
|
863
|
+
new sdk.SignCommand({
|
|
864
|
+
...input,
|
|
865
|
+
SigningAlgorithm: input.SigningAlgorithm
|
|
866
|
+
})
|
|
867
|
+
)
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
async function readKey(kms, keyId) {
|
|
871
|
+
let key;
|
|
872
|
+
try {
|
|
873
|
+
key = await kms.getPublicKey({ KeyId: keyId });
|
|
874
|
+
} catch (error) {
|
|
875
|
+
throw new Error(
|
|
876
|
+
`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.`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (key.KeySpec !== KMS_KEY_SPEC) {
|
|
880
|
+
throw new Error(`${keyId} is a ${key.KeySpec} key. The issuer signs with ${KMS_KEY_SPEC}.`);
|
|
881
|
+
}
|
|
882
|
+
if (key.KeyUsage && key.KeyUsage !== "SIGN_VERIFY") {
|
|
883
|
+
throw new Error(`${keyId} is not a signing key.`);
|
|
884
|
+
}
|
|
885
|
+
if (!key.SigningAlgorithms?.includes(KMS_SIGNING_ALGORITHM)) {
|
|
886
|
+
throw new Error(`${keyId} does not sign with ${KMS_SIGNING_ALGORITHM}.`);
|
|
887
|
+
}
|
|
888
|
+
if (!key.KeyId || !key.PublicKey) throw new Error(`KMS gave no public key for ${keyId}.`);
|
|
889
|
+
const publicKey2 = createPublicKey({
|
|
890
|
+
key: Buffer.from(key.PublicKey),
|
|
891
|
+
format: "der",
|
|
892
|
+
type: "spki"
|
|
893
|
+
});
|
|
894
|
+
const x = publicKey2.export({ format: "jwk" }).x;
|
|
895
|
+
const raw = new Uint8Array(Buffer.from(x ?? "", "base64url"));
|
|
896
|
+
if (raw.length !== 32) throw new Error(`${keyId}'s public key is not an Ed25519 key.`);
|
|
897
|
+
return { arn: key.KeyId, publicKey: publicKey2, raw };
|
|
898
|
+
}
|
|
899
|
+
async function kmsSigner(options) {
|
|
900
|
+
const kms = options.kms ?? await sdkClient(options.region);
|
|
901
|
+
const { arn, publicKey: publicKey2, raw } = await readKey(kms, options.keyId);
|
|
902
|
+
return {
|
|
903
|
+
kid: jwkThumbprint(raw),
|
|
904
|
+
publicKey: raw,
|
|
905
|
+
keyArn: arn,
|
|
906
|
+
async sign(message) {
|
|
907
|
+
const signed = await kms.sign({
|
|
908
|
+
KeyId: arn,
|
|
909
|
+
Message: message,
|
|
910
|
+
MessageType: "RAW",
|
|
911
|
+
SigningAlgorithm: KMS_SIGNING_ALGORITHM
|
|
912
|
+
});
|
|
913
|
+
const signature = signed.Signature ? new Uint8Array(signed.Signature) : null;
|
|
914
|
+
if (!signature || !verify2(null, message, publicKey2, signature)) {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`KMS answered for ${arn} with a signature its public key does not verify. Nothing was signed.`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
return signature;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async function kmsRetiredKey(options) {
|
|
924
|
+
const kms = options.kms ?? await sdkClient(options.region);
|
|
925
|
+
const { raw } = await readKey(kms, options.keyId);
|
|
926
|
+
return { kid: jwkThumbprint(raw), publicKey: raw, notAfter: options.notAfter };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/person.ts
|
|
930
|
+
import {
|
|
931
|
+
createCipheriv,
|
|
932
|
+
createDecipheriv,
|
|
933
|
+
createHash as createHash3,
|
|
934
|
+
createPublicKey as createPublicKey2,
|
|
935
|
+
randomBytes as randomBytes2,
|
|
936
|
+
timingSafeEqual,
|
|
937
|
+
verify as verify3
|
|
938
|
+
} from "crypto";
|
|
939
|
+
var MIN_SECRET = 32;
|
|
940
|
+
var FLOW_COOKIE = "pidgeon_issuer_flow";
|
|
941
|
+
var CODE_SECONDS = 60;
|
|
942
|
+
var FLOW_SECONDS = 600;
|
|
943
|
+
var SKEW = 60;
|
|
944
|
+
var UPSTREAM_ALGS = ["RS256", "ES256", "EdDSA"];
|
|
945
|
+
function checkSecret(secret) {
|
|
946
|
+
if (!secret || secret.length < MIN_SECRET) {
|
|
947
|
+
throw new Error(
|
|
948
|
+
`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.`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return secret;
|
|
952
|
+
}
|
|
953
|
+
var b64url2 = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
954
|
+
function sealKey(secret, purpose) {
|
|
955
|
+
return createHash3("sha256").update(`pidgeon issuer seal\0${purpose}\0${secret}`).digest();
|
|
956
|
+
}
|
|
957
|
+
function seal(secret, purpose, value) {
|
|
958
|
+
const iv = randomBytes2(12);
|
|
959
|
+
const cipher = createCipheriv("aes-256-gcm", sealKey(secret, purpose), iv);
|
|
960
|
+
cipher.setAAD(Buffer.from(purpose));
|
|
961
|
+
const body = Buffer.concat([cipher.update(JSON.stringify(value), "utf8"), cipher.final()]);
|
|
962
|
+
return b64url2(Buffer.concat([iv, body, cipher.getAuthTag()]));
|
|
963
|
+
}
|
|
964
|
+
function unseal(secret, purpose, token) {
|
|
965
|
+
if (!token) return null;
|
|
966
|
+
try {
|
|
967
|
+
const raw = Buffer.from(token, "base64url");
|
|
968
|
+
if (raw.length < 29) return null;
|
|
969
|
+
const decipher = createDecipheriv("aes-256-gcm", sealKey(secret, purpose), raw.subarray(0, 12));
|
|
970
|
+
decipher.setAAD(Buffer.from(purpose));
|
|
971
|
+
decipher.setAuthTag(raw.subarray(raw.length - 16));
|
|
972
|
+
const text2 = Buffer.concat([
|
|
973
|
+
decipher.update(raw.subarray(12, raw.length - 16)),
|
|
974
|
+
decipher.final()
|
|
975
|
+
]).toString("utf8");
|
|
976
|
+
return JSON.parse(text2);
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function pkceChallenge(verifier) {
|
|
982
|
+
return b64url2(createHash3("sha256").update(verifier).digest());
|
|
983
|
+
}
|
|
984
|
+
function pkceHolds(verifier, challenge) {
|
|
985
|
+
const a = Buffer.from(pkceChallenge(verifier));
|
|
986
|
+
const b = Buffer.from(challenge);
|
|
987
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
988
|
+
}
|
|
989
|
+
function cookieOf(request, name) {
|
|
990
|
+
for (const part of (request.headers.get("cookie") ?? "").split(";")) {
|
|
991
|
+
const eq = part.indexOf("=");
|
|
992
|
+
if (eq > 0 && part.slice(0, eq).trim() === name) {
|
|
993
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
var ProviderRefused = class extends Error {
|
|
999
|
+
};
|
|
1000
|
+
function provider(options, fetcher, now) {
|
|
1001
|
+
const issuer = options.issuer.replace(/\/$/, "");
|
|
1002
|
+
let configuration = null;
|
|
1003
|
+
let keys = null;
|
|
1004
|
+
function discover() {
|
|
1005
|
+
if (!configuration || configuration.at <= now() - 36e5) {
|
|
1006
|
+
const url = options.discoveryUrl ?? `${issuer}/.well-known/openid-configuration`;
|
|
1007
|
+
const value = (async () => {
|
|
1008
|
+
const response = await fetcher(url, { headers: { accept: "application/json" } });
|
|
1009
|
+
if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
|
|
1010
|
+
const body = await response.json();
|
|
1011
|
+
if (body.issuer?.replace(/\/$/, "") !== issuer) {
|
|
1012
|
+
throw new Error(`${url} names the issuer ${body.issuer}, not ${issuer}.`);
|
|
1013
|
+
}
|
|
1014
|
+
return body;
|
|
1015
|
+
})();
|
|
1016
|
+
configuration = { at: now(), value };
|
|
1017
|
+
value.catch(() => {
|
|
1018
|
+
if (configuration?.value === value) configuration = null;
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return configuration.value;
|
|
1022
|
+
}
|
|
1023
|
+
async function key(kid) {
|
|
1024
|
+
const find = () => keys?.value.find((one) => (kid ? one.kid === kid : true) && one.use !== "enc") ?? null;
|
|
1025
|
+
const fresh = keys && keys.at > now() - 3e5;
|
|
1026
|
+
if (fresh && find()) return find();
|
|
1027
|
+
if (keys && keys.at > now() - 1e4) return null;
|
|
1028
|
+
const response = await fetcher((await discover()).jwks_uri);
|
|
1029
|
+
const body = await response.json().catch(() => ({}));
|
|
1030
|
+
keys = { at: now(), value: Array.isArray(body.keys) ? body.keys : [] };
|
|
1031
|
+
return find();
|
|
1032
|
+
}
|
|
1033
|
+
async function authorizeUrl(input) {
|
|
1034
|
+
const url = new URL((await discover()).authorization_endpoint);
|
|
1035
|
+
url.search = new URLSearchParams({
|
|
1036
|
+
response_type: "code",
|
|
1037
|
+
client_id: options.clientId,
|
|
1038
|
+
redirect_uri: input.redirectUri,
|
|
1039
|
+
scope: "openid email",
|
|
1040
|
+
state: input.state,
|
|
1041
|
+
nonce: input.nonce,
|
|
1042
|
+
code_challenge: pkceChallenge(input.verifier),
|
|
1043
|
+
code_challenge_method: "S256"
|
|
1044
|
+
}).toString();
|
|
1045
|
+
return url.toString();
|
|
1046
|
+
}
|
|
1047
|
+
async function redeem(input) {
|
|
1048
|
+
const form = new URLSearchParams({
|
|
1049
|
+
grant_type: "authorization_code",
|
|
1050
|
+
code: input.code,
|
|
1051
|
+
redirect_uri: input.redirectUri,
|
|
1052
|
+
code_verifier: input.verifier
|
|
1053
|
+
});
|
|
1054
|
+
const headers = {
|
|
1055
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
1056
|
+
accept: "application/json"
|
|
1057
|
+
};
|
|
1058
|
+
if (options.tokenAuth === "client_secret_post") {
|
|
1059
|
+
form.set("client_id", options.clientId);
|
|
1060
|
+
form.set("client_secret", options.clientSecret);
|
|
1061
|
+
} else {
|
|
1062
|
+
const pair = `${encodeURIComponent(options.clientId)}:${encodeURIComponent(options.clientSecret)}`;
|
|
1063
|
+
headers.authorization = `Basic ${Buffer.from(pair).toString("base64")}`;
|
|
1064
|
+
}
|
|
1065
|
+
const response = await fetcher((await discover()).token_endpoint, {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
headers,
|
|
1068
|
+
body: form
|
|
1069
|
+
});
|
|
1070
|
+
const body = await response.json().catch(() => ({}));
|
|
1071
|
+
if (!response.ok || typeof body.id_token !== "string") {
|
|
1072
|
+
throw new ProviderRefused(
|
|
1073
|
+
`The identity provider did not sign the person in (${body.error ?? response.status}).`
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
return check(body.id_token, input.nonce);
|
|
1077
|
+
}
|
|
1078
|
+
async function check(token, nonce) {
|
|
1079
|
+
const parts = token.split(".");
|
|
1080
|
+
if (parts.length !== 3) throw new ProviderRefused("The provider sent no ID token.");
|
|
1081
|
+
let header;
|
|
1082
|
+
let claims;
|
|
1083
|
+
try {
|
|
1084
|
+
header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
|
|
1085
|
+
claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
|
|
1086
|
+
} catch {
|
|
1087
|
+
throw new ProviderRefused("The provider sent no ID token.");
|
|
1088
|
+
}
|
|
1089
|
+
if (!header.alg || !UPSTREAM_ALGS.includes(header.alg)) {
|
|
1090
|
+
throw new ProviderRefused(`The provider's ID token is signed with ${header.alg}.`);
|
|
1091
|
+
}
|
|
1092
|
+
const jwk = await key(header.kid);
|
|
1093
|
+
if (!jwk || !signatureHolds(header.alg, jwk, `${parts[0]}.${parts[1]}`, parts[2])) {
|
|
1094
|
+
throw new ProviderRefused("The provider's ID token is not signed by its keys.");
|
|
1095
|
+
}
|
|
1096
|
+
const at = Math.floor(now() / 1e3);
|
|
1097
|
+
if (typeof claims.iss !== "string" || claims.iss.replace(/\/$/, "") !== issuer) {
|
|
1098
|
+
throw new ProviderRefused("The provider's ID token is from another issuer.");
|
|
1099
|
+
}
|
|
1100
|
+
const aud = claims.aud;
|
|
1101
|
+
const audiences = Array.isArray(aud) ? aud : [aud];
|
|
1102
|
+
if (!audiences.includes(options.clientId)) {
|
|
1103
|
+
throw new ProviderRefused("The provider's ID token is for another client.");
|
|
1104
|
+
}
|
|
1105
|
+
if (audiences.length > 1 && claims.azp !== options.clientId) {
|
|
1106
|
+
throw new ProviderRefused("The provider's ID token is for another client.");
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof claims.exp !== "number" || claims.exp + SKEW <= at) {
|
|
1109
|
+
throw new ProviderRefused("The provider's ID token has expired.");
|
|
1110
|
+
}
|
|
1111
|
+
if (typeof claims.iat === "number" && claims.iat > at + SKEW) {
|
|
1112
|
+
throw new ProviderRefused("The provider's ID token is from the future.");
|
|
1113
|
+
}
|
|
1114
|
+
if (claims.nonce !== nonce) {
|
|
1115
|
+
throw new ProviderRefused("The provider's ID token is for another sign-in.");
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof claims.sub !== "string" || !claims.sub) {
|
|
1118
|
+
throw new ProviderRefused("The provider's ID token names nobody.");
|
|
1119
|
+
}
|
|
1120
|
+
if (claims.email_verified !== true || typeof claims.email !== "string") {
|
|
1121
|
+
throw new ProviderRefused("The identity provider has not verified this person\u2019s address.");
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
sub: claims.sub,
|
|
1125
|
+
email: claims.email.trim().toLowerCase(),
|
|
1126
|
+
authTime: typeof claims.auth_time === "number" ? claims.auth_time : at
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
return { authorizeUrl, redeem };
|
|
1130
|
+
}
|
|
1131
|
+
function signatureHolds(alg, jwk, input, signature) {
|
|
1132
|
+
try {
|
|
1133
|
+
const key = createPublicKey2({ key: jwk, format: "jwk" });
|
|
1134
|
+
const data = Buffer.from(input);
|
|
1135
|
+
const sig = Buffer.from(signature, "base64url");
|
|
1136
|
+
if (alg === "EdDSA") return jwk.kty === "OKP" && verify3(null, data, key, sig);
|
|
1137
|
+
if (alg === "ES256") {
|
|
1138
|
+
return jwk.kty === "EC" && jwk.crv === "P-256" && verify3("sha256", data, { key, dsaEncoding: "ieee-p1363" }, sig);
|
|
1139
|
+
}
|
|
1140
|
+
return jwk.kty === "RSA" && verify3("sha256", data, key, sig);
|
|
1141
|
+
} catch {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
844
1146
|
// src/signer.ts
|
|
1147
|
+
import { readFile } from "fs/promises";
|
|
845
1148
|
async function fileSigner(path) {
|
|
846
1149
|
let file;
|
|
847
1150
|
try {
|
|
@@ -884,12 +1187,21 @@ async function loadConfig(path, env = process.env) {
|
|
|
884
1187
|
throw new Error(`${path}: \`domain\` is the domain whose agents this issues for.`);
|
|
885
1188
|
}
|
|
886
1189
|
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)
|
|
1190
|
+
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;
|
|
1191
|
+
if (!signer) {
|
|
1192
|
+
throw new Error(`${path}: name a \`key\` file or a KMS key, or set PIDGEON_ISSUER_KEY.`);
|
|
1193
|
+
}
|
|
889
1194
|
const retired = [];
|
|
890
1195
|
for (const entry of file.retired ?? []) {
|
|
891
|
-
const
|
|
892
|
-
|
|
1196
|
+
const notAfter = new Date(entry.not_after);
|
|
1197
|
+
if (typeof entry.key === "string") {
|
|
1198
|
+
const old = await fileSigner(here(entry.key));
|
|
1199
|
+
retired.push({ kid: old.kid, publicKey: old.publicKey, notAfter });
|
|
1200
|
+
} else {
|
|
1201
|
+
retired.push(
|
|
1202
|
+
await kmsRetiredKey({ keyId: entry.key.kms, region: entry.key.region, notAfter })
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
893
1205
|
}
|
|
894
1206
|
let person;
|
|
895
1207
|
if (file.person) {
|
|
@@ -946,6 +1258,32 @@ function directoryFrom(file, path, env) {
|
|
|
946
1258
|
group: keycloak.group
|
|
947
1259
|
});
|
|
948
1260
|
}
|
|
1261
|
+
if ("auth0" in directory) {
|
|
1262
|
+
const { auth0 } = directory;
|
|
1263
|
+
const secretName = auth0.client_secret_env ?? "AUTH0_CLIENT_SECRET";
|
|
1264
|
+
const secret = env[secretName];
|
|
1265
|
+
if (!secret) throw new Error(`${path}: set ${secretName}, the issuer's Auth0 client secret.`);
|
|
1266
|
+
return auth0Directory({
|
|
1267
|
+
tenant: auth0.tenant,
|
|
1268
|
+
clientId: auth0.client_id,
|
|
1269
|
+
clientSecret: secret,
|
|
1270
|
+
domain: file.domain
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
if ("okta" in directory) {
|
|
1274
|
+
const { okta } = directory;
|
|
1275
|
+
const keyName = okta.private_key_env ?? "OKTA_PRIVATE_KEY";
|
|
1276
|
+
const privateKey = env[keyName];
|
|
1277
|
+
if (!privateKey)
|
|
1278
|
+
throw new Error(`${path}: set ${keyName}, the issuer's Okta private key (PEM).`);
|
|
1279
|
+
return oktaDirectory({
|
|
1280
|
+
org: okta.org,
|
|
1281
|
+
clientId: okta.client_id,
|
|
1282
|
+
privateKey,
|
|
1283
|
+
keyId: okta.key_id,
|
|
1284
|
+
domain: file.domain
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
949
1287
|
if ("static" in directory && Array.isArray(directory.static)) {
|
|
950
1288
|
for (const agent of directory.static) {
|
|
951
1289
|
if (!agent.address?.toLowerCase().endsWith(`@${file.domain.toLowerCase()}`)) {
|
|
@@ -954,11 +1292,13 @@ function directoryFrom(file, path, env) {
|
|
|
954
1292
|
}
|
|
955
1293
|
return staticDirectory(directory.static);
|
|
956
1294
|
}
|
|
957
|
-
throw new Error(
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`${path}: \`directory\` is { "keycloak": {...} }, { "auth0": {...} }, { "okta": {...} } or { "static": [...] }.`
|
|
1297
|
+
);
|
|
958
1298
|
}
|
|
959
1299
|
|
|
960
1300
|
// src/issuer.ts
|
|
961
|
-
import { createHash as
|
|
1301
|
+
import { createHash as createHash4, createPublicKey as createPublicKey3, randomBytes as randomBytes3, randomUUID as randomUUID2, verify as verify4 } from "crypto";
|
|
962
1302
|
var TOKEN_EXCHANGE2 = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
963
1303
|
var TOKEN_TYPE_JWT = "urn:ietf:params:oauth:token-type:jwt";
|
|
964
1304
|
var TOKEN_TYPE_ID = "urn:ietf:params:oauth:token-type:id_token";
|
|
@@ -1029,10 +1369,10 @@ function createIssuer(options) {
|
|
|
1029
1369
|
if (header?.alg !== "EdDSA" || header?.typ !== typ) return null;
|
|
1030
1370
|
const key = publishedIssuerKeys().find((candidate) => candidate.kid === header.kid);
|
|
1031
1371
|
if (!key) return null;
|
|
1032
|
-
const signed =
|
|
1372
|
+
const signed = verify4(
|
|
1033
1373
|
null,
|
|
1034
1374
|
Buffer.from(`${parts[0]}.${parts[1]}`),
|
|
1035
|
-
|
|
1375
|
+
createPublicKey3({
|
|
1036
1376
|
key: { kty: "OKP", crv: "Ed25519", x: b64url3(key.publicKey) },
|
|
1037
1377
|
format: "jwk"
|
|
1038
1378
|
}),
|
|
@@ -1152,7 +1492,7 @@ function createIssuer(options) {
|
|
|
1152
1492
|
};
|
|
1153
1493
|
}
|
|
1154
1494
|
function record() {
|
|
1155
|
-
const id =
|
|
1495
|
+
const id = createHash4("sha256").update(`post policy id\0self-hosted\0${domain}`).digest("hex").slice(0, 16);
|
|
1156
1496
|
return `v=post0; id=${id}`;
|
|
1157
1497
|
}
|
|
1158
1498
|
const cardUrl = (address) => `${issuer}${PATHS.card}?address=${encodeURIComponent(address)}`;
|
|
@@ -1189,7 +1529,7 @@ function createIssuer(options) {
|
|
|
1189
1529
|
const claims = cardClaims(agent);
|
|
1190
1530
|
return { jws: await jws({ alg: "EdDSA", typ: "agent-card+jwt" }, claims), card: claims };
|
|
1191
1531
|
}
|
|
1192
|
-
const grantId = (agent, rule) => `grt_${
|
|
1532
|
+
const grantId = (agent, rule) => `grt_${createHash4("sha256").update(
|
|
1193
1533
|
`${agent.sub}\0${rule.group}\0${rule.scope.name}\0${rule.subject.type}:${rule.subject.id}`
|
|
1194
1534
|
).digest("base64url").slice(0, 22)}`;
|
|
1195
1535
|
const rulesFor = (agent) => (options.grants ?? []).filter((rule) => agent.groups.includes(rule.group));
|
|
@@ -1380,7 +1720,7 @@ function createIssuer(options) {
|
|
|
1380
1720
|
}
|
|
1381
1721
|
const audience = form.get("audience");
|
|
1382
1722
|
const iat = nowSeconds();
|
|
1383
|
-
const jti =
|
|
1723
|
+
const jti = randomUUID2();
|
|
1384
1724
|
const cnf = { jwk: { kty: "OKP", crv: "Ed25519", x: live.key } };
|
|
1385
1725
|
const fresh = now() - Date.parse(live.createdAt) < NEW_ADDRESS_DAYS * 864e5;
|
|
1386
1726
|
const idToken = await jws(
|
|
@@ -1580,7 +1920,7 @@ function createIssuer(options) {
|
|
|
1580
1920
|
});
|
|
1581
1921
|
}
|
|
1582
1922
|
const sealed = {
|
|
1583
|
-
jti:
|
|
1923
|
+
jti: randomUUID2(),
|
|
1584
1924
|
clientId: flow.site.clientId,
|
|
1585
1925
|
redirectUri: flow.site.redirectUri,
|
|
1586
1926
|
challenge: flow.site.challenge,
|
|
@@ -1625,7 +1965,7 @@ function createIssuer(options) {
|
|
|
1625
1965
|
exp: iat + ID_TOKEN_SECONDS,
|
|
1626
1966
|
auth_time: code.authTime,
|
|
1627
1967
|
...code.nonce ? { nonce: code.nonce } : {},
|
|
1628
|
-
jti:
|
|
1968
|
+
jti: randomUUID2(),
|
|
1629
1969
|
sub: code.sub,
|
|
1630
1970
|
kind: "person",
|
|
1631
1971
|
email: code.email,
|
|
@@ -1865,7 +2205,11 @@ ${HELP}`);
|
|
|
1865
2205
|
export {
|
|
1866
2206
|
staticDirectory,
|
|
1867
2207
|
cachedDirectory,
|
|
2208
|
+
auth0Directory,
|
|
1868
2209
|
keycloakDirectory,
|
|
2210
|
+
oktaDirectory,
|
|
2211
|
+
kmsSigner,
|
|
2212
|
+
kmsRetiredKey,
|
|
1869
2213
|
fileSigner,
|
|
1870
2214
|
seedSigner,
|
|
1871
2215
|
loadConfig,
|
|
@@ -1877,4 +2221,4 @@ export {
|
|
|
1877
2221
|
UsageError2 as UsageError,
|
|
1878
2222
|
runIssuer
|
|
1879
2223
|
};
|
|
1880
|
-
//# sourceMappingURL=chunk-
|
|
2224
|
+
//# sourceMappingURL=chunk-IKGE6SNR.js.map
|