@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.
- package/README.md +53 -3
- package/dist/bin.js +1 -1
- package/dist/{chunk-BXZ2EI2Y.js → chunk-5GZMAASA.js} +551 -229
- package/dist/chunk-5GZMAASA.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,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/
|
|
132
|
-
import {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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 ||
|
|
274
|
-
throw new
|
|
275
|
-
`
|
|
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
|
-
|
|
270
|
+
token = { value: body.access_token, until: now() + ((body.expires_in ?? 120) - 60) * 1e3 };
|
|
271
|
+
return token.value;
|
|
279
272
|
}
|
|
280
|
-
async function
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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:
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
} catch {
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
326
|
+
};
|
|
346
327
|
}
|
|
347
328
|
|
|
348
|
-
// src/
|
|
349
|
-
import {
|
|
329
|
+
// src/kms.ts
|
|
330
|
+
import { createPublicKey, verify as verify2 } from "crypto";
|
|
350
331
|
|
|
351
332
|
// ../id/dist/chunk-QDQDZSU7.js
|
|
352
|
-
import { createHash
|
|
353
|
-
import { createHash as
|
|
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: () =>
|
|
369
|
-
verify: () =>
|
|
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
|
|
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
|
|
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
|
|
681
|
+
var b64url = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
701
682
|
function jwkThumbprint(publicKey2) {
|
|
702
|
-
return
|
|
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=:${
|
|
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)
|
|
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
|
|
892
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
1350
|
+
const signed = verify4(
|
|
1033
1351
|
null,
|
|
1034
1352
|
Buffer.from(`${parts[0]}.${parts[1]}`),
|
|
1035
|
-
|
|
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 =
|
|
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_${
|
|
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 =
|
|
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:
|
|
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:
|
|
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-
|
|
2202
|
+
//# sourceMappingURL=chunk-5GZMAASA.js.map
|