@axiom-lattice/auth-oidc 0.0.0-stage → 0.2.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/dist/index.js ADDED
@@ -0,0 +1,743 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ OIDC_STATE_COOKIE: () => OIDC_STATE_COOKIE,
34
+ OIDC_STATE_PATH: () => OIDC_STATE_PATH,
35
+ OidcAuthProvider: () => OidcAuthProvider,
36
+ assertSafeEndpointSyntax: () => assertSafeEndpointSyntax,
37
+ assertSafeOidcEndpoint: () => assertSafeOidcEndpoint,
38
+ assertTenantSpecificAzureIssuer: () => assertTenantSpecificAzureIssuer,
39
+ clearStateCookie: () => clearStateCookie,
40
+ isLinkLocalOrMetadataAddress: () => isLinkLocalOrMetadataAddress,
41
+ loadOpenidClient: () => loadOpenidClient,
42
+ oidcDescriptor: () => oidcDescriptor,
43
+ parseAllowedPrivateHosts: () => parseAllowedPrivateHosts,
44
+ principalFromClaims: () => principalFromClaims,
45
+ probeOidcProvider: () => probeOidcProvider,
46
+ readCookie: () => readCookie,
47
+ readOidcConfig: () => readOidcConfig,
48
+ serializeCookie: () => serializeCookie,
49
+ signStateCookie: () => signStateCookie,
50
+ stateCookie: () => stateCookie,
51
+ verifyStateCookie: () => verifyStateCookie
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/endpointSafety.ts
56
+ var import_promises = require("dns/promises");
57
+ var import_node_net = require("net");
58
+ function hostAddress(hostname) {
59
+ return hostname.replace(/^\[/, "").replace(/\]$/, "");
60
+ }
61
+ function normalizeHostEntry(raw) {
62
+ const trimmed = raw.trim();
63
+ if (!trimmed) return void 0;
64
+ let host = trimmed;
65
+ if (trimmed.includes("://")) {
66
+ try {
67
+ host = new URL(trimmed).hostname;
68
+ } catch {
69
+ return void 0;
70
+ }
71
+ }
72
+ return hostAddress(host).toLowerCase().replace(/\.$/, "");
73
+ }
74
+ function parseAllowedPrivateHosts(value) {
75
+ if (!value) return [];
76
+ const hosts = /* @__PURE__ */ new Set();
77
+ for (const entry of value.split(",")) {
78
+ const host = normalizeHostEntry(entry);
79
+ if (host) hosts.add(host);
80
+ }
81
+ return [...hosts];
82
+ }
83
+ function isAllowedPrivateHost(hostname, options) {
84
+ const host = normalizeHostEntry(hostname);
85
+ if (!host) return false;
86
+ return (options.allowedPrivateHosts ?? []).some(
87
+ (entry) => normalizeHostEntry(entry) === host
88
+ );
89
+ }
90
+ function isLocalHostname(hostname) {
91
+ const host = hostAddress(hostname);
92
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
93
+ }
94
+ function isPrivateIPv4(ip) {
95
+ const parts = ip.split(".").map(Number);
96
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
97
+ return true;
98
+ }
99
+ const [a, b, c] = parts;
100
+ return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 && c === 0 || a === 192 && b === 0 && c === 2 || a === 198 && b === 51 && c === 100 || a === 203 && b === 0 && c === 113 || a === 100 && b >= 64 && b <= 127 || a === 198 && (b === 18 || b === 19) || a >= 224;
101
+ }
102
+ var IPV6_8_BITS = 8;
103
+ var IPV6_GROUPS = 8;
104
+ function ipv6Bytes(ip) {
105
+ let value = ip.toLowerCase();
106
+ const zone = value.indexOf("%");
107
+ if (zone !== -1) value = value.slice(0, zone);
108
+ const lastColon = value.lastIndexOf(":");
109
+ if (lastColon !== -1 && value.slice(lastColon + 1).includes(".")) {
110
+ const ipv4 = value.slice(lastColon + 1).split(".").map(Number);
111
+ if (ipv4.length !== 4 || ipv4.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
112
+ return null;
113
+ }
114
+ const high = (ipv4[0] << 8 | ipv4[1]).toString(16);
115
+ const low = (ipv4[2] << 8 | ipv4[3]).toString(16);
116
+ value = `${value.slice(0, lastColon + 1)}${high}:${low}`;
117
+ }
118
+ const halves = value.split("::");
119
+ if (halves.length > 2) return null;
120
+ const head = halves[0] ? halves[0].split(":") : [];
121
+ const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
122
+ let groups;
123
+ if (halves.length === 2) {
124
+ const missing = IPV6_GROUPS - head.length - tail.length;
125
+ if (missing < 1) return null;
126
+ groups = [...head, ...Array(missing).fill("0"), ...tail];
127
+ } else {
128
+ groups = head;
129
+ }
130
+ if (groups.length !== IPV6_GROUPS) return null;
131
+ const bytes = [];
132
+ for (const group of groups) {
133
+ if (!/^[0-9a-f]{1,4}$/.test(group)) return null;
134
+ const part = Number.parseInt(group, 16);
135
+ bytes.push(part >> IPV6_8_BITS & 255, part & 255);
136
+ }
137
+ return bytes;
138
+ }
139
+ function hasPrefix(bytes, prefix) {
140
+ return prefix.every((value, index) => bytes[index] === value);
141
+ }
142
+ function isPrivateIPv6(ip) {
143
+ const bytes = ipv6Bytes(ip);
144
+ if (!bytes) return true;
145
+ if (bytes.every((value) => value === 0)) return true;
146
+ const loopback = bytes.slice(0, 15).every((value) => value === 0) && bytes[15] === 1;
147
+ if (loopback) return true;
148
+ if ((bytes[0] & 254) === 252) return true;
149
+ if (bytes[0] === 254 && (bytes[1] & 192) === 128) return true;
150
+ if (bytes[0] === 255) return true;
151
+ if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 0 && bytes[3] === 0 && bytes[4] === 0 && bytes[5] === 0 && bytes[6] === 0 && bytes[7] === 0 && bytes[8] === 0 && bytes[9] === 0 && bytes[10] === 255 && bytes[11] === 255) {
152
+ return isPrivateIPv4(
153
+ `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`
154
+ );
155
+ }
156
+ if (hasPrefix(bytes, [0, 100, 255, 155, 0, 0, 0, 0, 0, 0, 0, 0])) {
157
+ return true;
158
+ }
159
+ if (hasPrefix(bytes, [0, 100, 255, 155, 0, 1])) return true;
160
+ if (hasPrefix(bytes, [32, 2])) return true;
161
+ if (hasPrefix(bytes, [32, 1, 0, 0])) return true;
162
+ if (hasPrefix(bytes, [32, 1, 13, 184])) return true;
163
+ if (hasPrefix(bytes, [32, 1, 0, 2, 0, 0])) return true;
164
+ if (hasPrefix(bytes, [1, 0, 0, 0, 0, 0, 0, 0])) return true;
165
+ return false;
166
+ }
167
+ function isPrivateAddress(ip) {
168
+ const version = (0, import_node_net.isIP)(ip);
169
+ if (version === 4) return isPrivateIPv4(ip);
170
+ if (version === 6) return isPrivateIPv6(ip);
171
+ return true;
172
+ }
173
+ function isLinkLocalOrMetadataAddress(address) {
174
+ const ip = hostAddress(address);
175
+ const version = (0, import_node_net.isIP)(ip);
176
+ if (version === 4) {
177
+ const parts = ip.split(".").map(Number);
178
+ if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
179
+ return false;
180
+ }
181
+ return parts[0] === 0 || parts[0] === 169 && parts[1] === 254;
182
+ }
183
+ if (version !== 6) return false;
184
+ const bytes = ipv6Bytes(ip);
185
+ if (!bytes) return false;
186
+ if (bytes.every((value) => value === 0)) return true;
187
+ if (bytes[0] === 254 && (bytes[1] & 192) === 128) return true;
188
+ const ipv4Mapped = bytes.slice(0, 10).every((value) => value === 0) && bytes[10] === 255 && bytes[11] === 255;
189
+ if (ipv4Mapped) {
190
+ return isLinkLocalOrMetadataAddress(
191
+ `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`
192
+ );
193
+ }
194
+ return false;
195
+ }
196
+ function assertSafeEndpointSyntax(raw, options) {
197
+ let url;
198
+ try {
199
+ url = new URL(raw);
200
+ } catch {
201
+ throw new Error("OIDC endpoint must be an absolute URL");
202
+ }
203
+ if (url.username || url.password) {
204
+ throw new Error("OIDC endpoint must not contain userinfo");
205
+ }
206
+ if (url.search || url.hash) {
207
+ throw new Error("OIDC endpoint must not contain query or fragment");
208
+ }
209
+ const host = hostAddress(url.hostname);
210
+ if (url.protocol !== "https:") {
211
+ const localMock = options.allowInsecure && url.protocol === "http:" && isLocalHostname(host);
212
+ if (!localMock) throw new Error("OIDC endpoint must use https");
213
+ }
214
+ if (isLocalHostname(host) && !options.allowInsecure) {
215
+ throw new Error("OIDC endpoint must not target localhost");
216
+ }
217
+ if ((0, import_node_net.isIP)(host) && isLinkLocalOrMetadataAddress(host)) {
218
+ throw new Error(
219
+ "OIDC endpoint must not target a link-local or metadata address"
220
+ );
221
+ }
222
+ if ((0, import_node_net.isIP)(host) && isPrivateAddress(host)) {
223
+ if (!(options.allowInsecure && isLocalHostname(host)) && !isAllowedPrivateHost(host, options)) {
224
+ throw new Error("OIDC endpoint must not target a private address");
225
+ }
226
+ }
227
+ return url;
228
+ }
229
+ async function assertSafeOidcEndpoint(raw, options) {
230
+ const url = assertSafeEndpointSyntax(raw, options);
231
+ const host = hostAddress(url.hostname);
232
+ const allowlisted = isAllowedPrivateHost(host, options);
233
+ if (options.allowInsecure && isLocalHostname(host) || (0, import_node_net.isIP)(host)) {
234
+ return url;
235
+ }
236
+ const addresses = await (0, import_promises.lookup)(host, { all: true, verbatim: true });
237
+ if (addresses.length === 0) throw new Error("OIDC endpoint host did not resolve");
238
+ for (const { address } of addresses) {
239
+ if (isLinkLocalOrMetadataAddress(address)) {
240
+ throw new Error(
241
+ "OIDC endpoint host resolves to a link-local or metadata address"
242
+ );
243
+ }
244
+ if (isPrivateAddress(address) && !allowlisted) {
245
+ throw new Error("OIDC endpoint host resolves to a private address");
246
+ }
247
+ }
248
+ return url;
249
+ }
250
+
251
+ // src/config.ts
252
+ function required(env, name) {
253
+ const value = env[name]?.trim();
254
+ if (!value) throw new Error(`${name} is required when OIDC_ENABLED=true`);
255
+ return value;
256
+ }
257
+ function boolean(value, fallback = false) {
258
+ if (value === void 0 || value.trim() === "") return fallback;
259
+ return value === "true";
260
+ }
261
+ function isLocalHost(hostname) {
262
+ const host = hostname.replace(/^\[/, "").replace(/\]$/, "");
263
+ return host === "localhost" || host === "127.0.0.1" || host === "::1";
264
+ }
265
+ function assertNoCredentialsQueryOrFragment(url, name) {
266
+ if (url.username || url.password) {
267
+ throw new Error(`${name} must not contain userinfo`);
268
+ }
269
+ if (url.search || url.hash) {
270
+ throw new Error(`${name} must not contain query or fragment`);
271
+ }
272
+ }
273
+ function assertTenantSpecificAzureIssuer(url, name = "OIDC_ISSUER") {
274
+ if (url.hostname.toLowerCase() !== "login.microsoftonline.com") return;
275
+ const firstSegment = url.pathname.split("/").filter(Boolean)[0]?.toLowerCase();
276
+ if (firstSegment === "common" || firstSegment === "organizations") {
277
+ throw new Error(
278
+ `${name} must be tenant-specific; login.microsoftonline.com/common and /organizations are not supported`
279
+ );
280
+ }
281
+ }
282
+ function parseIssuer(raw, allowInsecure) {
283
+ let url;
284
+ try {
285
+ url = new URL(raw);
286
+ } catch {
287
+ throw new Error("OIDC_ISSUER must be an absolute URL");
288
+ }
289
+ assertNoCredentialsQueryOrFragment(url, "OIDC_ISSUER");
290
+ assertTenantSpecificAzureIssuer(url);
291
+ if (url.protocol !== "https:") {
292
+ if (!(allowInsecure && url.protocol === "http:" && isLocalHost(url.hostname))) {
293
+ throw new Error("OIDC_ISSUER must use https");
294
+ }
295
+ }
296
+ return url.toString().replace(/\/$/, "");
297
+ }
298
+ function parseRedirect(raw, nodeEnv) {
299
+ let url;
300
+ try {
301
+ url = new URL(raw);
302
+ } catch {
303
+ throw new Error("OIDC redirect URLs must be absolute URLs");
304
+ }
305
+ assertNoCredentialsQueryOrFragment(url, "OIDC redirect URLs");
306
+ if (url.protocol !== "https:") {
307
+ const localAllowed = (nodeEnv === "test" || nodeEnv === "development") && url.protocol === "http:" && isLocalHost(url.hostname);
308
+ if (!localAllowed) {
309
+ throw new Error("OIDC redirect URLs must use https outside test/development");
310
+ }
311
+ }
312
+ return url.toString();
313
+ }
314
+ function readOidcConfig(env = process.env, options = {}) {
315
+ if (!boolean(env.OIDC_ENABLED)) return null;
316
+ const nodeEnv = options.nodeEnv ?? env.NODE_ENV ?? "production";
317
+ const allowInsecure = boolean(env.OIDC_ALLOW_INSECURE);
318
+ if (allowInsecure && nodeEnv !== "test" && nodeEnv !== "development") {
319
+ throw new Error("OIDC_ALLOW_INSECURE is only allowed when NODE_ENV=test|development");
320
+ }
321
+ const issuer = parseIssuer(required(env, "OIDC_ISSUER"), allowInsecure);
322
+ const redirectUri = parseRedirect(required(env, "OIDC_REDIRECT_URI"), nodeEnv);
323
+ const postLoginRedirectUri = parseRedirect(
324
+ required(env, "OIDC_POST_LOGIN_REDIRECT_URI"),
325
+ nodeEnv
326
+ );
327
+ const jitProvisioning = boolean(env.OIDC_JIT_PROVISIONING);
328
+ const defaultTenantId = env.OIDC_DEFAULT_TENANT_ID?.trim() || void 0;
329
+ const stateSecret = (options.stateSecret ?? env.AUTH_TOKEN_SECRET ?? env.JWT_SECRET)?.trim();
330
+ if (!stateSecret) {
331
+ throw new Error("AUTH_TOKEN_SECRET or JWT_SECRET is required for OIDC state signing");
332
+ }
333
+ if (stateSecret.length < 16) {
334
+ throw new Error(
335
+ "AUTH_TOKEN_SECRET or JWT_SECRET must be at least 16 characters for OIDC state signing"
336
+ );
337
+ }
338
+ const allowedPrivateHosts = parseAllowedPrivateHosts(
339
+ env.OIDC_ALLOWED_PRIVATE_HOSTS
340
+ );
341
+ return {
342
+ issuer,
343
+ clientId: required(env, "OIDC_CLIENT_ID"),
344
+ clientSecret: required(env, "OIDC_CLIENT_SECRET"),
345
+ redirectUri,
346
+ postLoginRedirectUri,
347
+ scopes: env.OIDC_SCOPES?.trim() || "openid profile email",
348
+ displayName: env.OIDC_DISPLAY_NAME?.trim() || "Microsoft",
349
+ ...defaultTenantId ? { defaultTenantId } : {},
350
+ jitProvisioning,
351
+ autoLinkEmail: boolean(env.OIDC_AUTO_LINK_EMAIL),
352
+ allowInsecure,
353
+ insecureCookies: redirectUri.startsWith("http://"),
354
+ stateSecret,
355
+ ...allowedPrivateHosts.length ? { allowedPrivateHosts } : {}
356
+ };
357
+ }
358
+ function oidcDescriptor(config) {
359
+ return {
360
+ id: "oidc",
361
+ displayName: config.displayName,
362
+ interaction: "redirect",
363
+ surfaces: ["console"]
364
+ };
365
+ }
366
+
367
+ // src/stateCookie.ts
368
+ var import_node_crypto = require("crypto");
369
+ var OIDC_STATE_COOKIE = "axiom_oidc_state";
370
+ var OIDC_STATE_PATH = "/api/auth/login";
371
+ function isPayload(value) {
372
+ if (typeof value !== "object" || value === null) return false;
373
+ const payload = value;
374
+ return typeof payload.state === "string" && typeof payload.codeVerifier === "string" && typeof payload.nonce === "string" && typeof payload.providerId === "string" && payload.providerId.length > 0 && typeof payload.exp === "number" && Number.isFinite(payload.exp);
375
+ }
376
+ function stateKey(secret) {
377
+ return (0, import_node_crypto.createHmac)("sha256", secret).update("axiom-oidc-state-v1").digest();
378
+ }
379
+ function signStateCookie(payload, secret) {
380
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
381
+ const signature = (0, import_node_crypto.createHmac)("sha256", stateKey(secret)).update(encoded).digest("base64url");
382
+ return `${encoded}.${signature}`;
383
+ }
384
+ function verifyStateCookie(value, secret, now = Date.now()) {
385
+ const [encoded, signature, extra] = value.split(".");
386
+ if (!encoded || !signature || extra !== void 0) return null;
387
+ const expected = (0, import_node_crypto.createHmac)("sha256", stateKey(secret)).update(encoded).digest();
388
+ let actual;
389
+ try {
390
+ actual = Buffer.from(signature, "base64url");
391
+ } catch {
392
+ return null;
393
+ }
394
+ if (actual.length !== expected.length || actual.toString("base64url") !== signature || !(0, import_node_crypto.timingSafeEqual)(actual, expected)) {
395
+ return null;
396
+ }
397
+ try {
398
+ const payload = JSON.parse(
399
+ Buffer.from(encoded, "base64url").toString("utf8")
400
+ );
401
+ if (!isPayload(payload) || payload.exp < now) return null;
402
+ return payload;
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ function stateCookie(value, options) {
408
+ return {
409
+ name: OIDC_STATE_COOKIE,
410
+ value,
411
+ options: {
412
+ httpOnly: true,
413
+ secure: options.secure,
414
+ sameSite: "lax",
415
+ path: OIDC_STATE_PATH,
416
+ maxAge: options.maxAge ?? 600
417
+ }
418
+ };
419
+ }
420
+ function clearStateCookie(options) {
421
+ return {
422
+ name: OIDC_STATE_COOKIE,
423
+ value: "",
424
+ options: {
425
+ httpOnly: true,
426
+ secure: options.secure,
427
+ sameSite: "lax",
428
+ path: OIDC_STATE_PATH,
429
+ maxAge: 0
430
+ }
431
+ };
432
+ }
433
+ function serializeCookie(cookie) {
434
+ const parts = [`${cookie.name}=${encodeURIComponent(cookie.value)}`];
435
+ if (cookie.options.httpOnly) parts.push("HttpOnly");
436
+ if (cookie.options.secure) parts.push("Secure");
437
+ if (cookie.options.sameSite) {
438
+ parts.push(
439
+ `SameSite=${cookie.options.sameSite[0].toUpperCase()}${cookie.options.sameSite.slice(1)}`
440
+ );
441
+ }
442
+ if (cookie.options.path) parts.push(`Path=${cookie.options.path}`);
443
+ if (cookie.options.maxAge !== void 0) parts.push(`Max-Age=${cookie.options.maxAge}`);
444
+ return parts.join("; ");
445
+ }
446
+ function readCookie(header, name) {
447
+ if (!header) return void 0;
448
+ for (const part of header.split(";")) {
449
+ const [rawName, ...rest] = part.trim().split("=");
450
+ if (rawName === name) {
451
+ try {
452
+ return decodeURIComponent(rest.join("="));
453
+ } catch (error) {
454
+ if (error instanceof URIError) return void 0;
455
+ throw error;
456
+ }
457
+ }
458
+ }
459
+ return void 0;
460
+ }
461
+
462
+ // src/openidClient.ts
463
+ async function loadOpenidClient() {
464
+ const mod = await import("openid-client");
465
+ return {
466
+ discovery: (issuer, clientId, clientSecret, options) => {
467
+ const execute = options.allowInsecure ? [mod.allowInsecureRequests] : void 0;
468
+ return mod.discovery(issuer, clientId, clientSecret, void 0, {
469
+ timeout: options.timeoutSeconds,
470
+ ...execute ? { execute } : {}
471
+ });
472
+ },
473
+ randomState: () => mod.randomState(),
474
+ randomPKCECodeVerifier: () => mod.randomPKCECodeVerifier(),
475
+ calculatePKCECodeChallenge: (verifier) => mod.calculatePKCECodeChallenge(verifier),
476
+ randomNonce: () => mod.randomNonce(),
477
+ buildAuthorizationUrl: (config, parameters) => mod.buildAuthorizationUrl(config, parameters),
478
+ authorizationCodeGrant: async (config, currentUrl, checks) => {
479
+ const tokens = await mod.authorizationCodeGrant(config, currentUrl, checks);
480
+ return {
481
+ claims: () => tokens.claims()
482
+ };
483
+ }
484
+ };
485
+ }
486
+
487
+ // src/probe.ts
488
+ function requiredMetadataString(metadata, key) {
489
+ const value = metadata[key];
490
+ if (typeof value !== "string" || !value) {
491
+ throw new Error("OIDC discovery returned incomplete metadata");
492
+ }
493
+ return value;
494
+ }
495
+ async function probeOidcProvider(config) {
496
+ const client = config.client ?? await loadOpenidClient();
497
+ const validateEndpoint = config.validateEndpoint ?? assertSafeOidcEndpoint;
498
+ const safety = {
499
+ allowInsecure: config.allowInsecure,
500
+ allowedPrivateHosts: config.allowedPrivateHosts ?? []
501
+ };
502
+ await validateEndpoint(config.issuer, safety);
503
+ const discovered = await client.discovery(
504
+ new URL(config.issuer),
505
+ config.clientId,
506
+ config.clientSecret,
507
+ { timeoutSeconds: 5, allowInsecure: config.allowInsecure }
508
+ );
509
+ const metadata = discovered.serverMetadata();
510
+ const authorizationEndpoint = requiredMetadataString(
511
+ metadata,
512
+ "authorization_endpoint"
513
+ );
514
+ const tokenEndpoint = requiredMetadataString(metadata, "token_endpoint");
515
+ const jwksUri = requiredMetadataString(metadata, "jwks_uri");
516
+ const userinfoEndpoint = typeof metadata.userinfo_endpoint === "string" && metadata.userinfo_endpoint ? metadata.userinfo_endpoint : void 0;
517
+ for (const endpoint of [
518
+ authorizationEndpoint,
519
+ tokenEndpoint,
520
+ jwksUri,
521
+ ...userinfoEndpoint ? [userinfoEndpoint] : []
522
+ ]) {
523
+ await validateEndpoint(endpoint, safety);
524
+ }
525
+ const issuer = typeof metadata.issuer === "string" && metadata.issuer ? metadata.issuer : config.issuer;
526
+ const scopesSupported = Array.isArray(metadata.scopes_supported) ? metadata.scopes_supported.filter(
527
+ (scope) => typeof scope === "string"
528
+ ) : void 0;
529
+ return {
530
+ issuer,
531
+ authorizationEndpoint,
532
+ tokenEndpoint,
533
+ jwksUri,
534
+ ...userinfoEndpoint ? { userinfoEndpoint } : {},
535
+ ...scopesSupported ? { scopesSupported } : {}
536
+ };
537
+ }
538
+
539
+ // src/OidcAuthProvider.ts
540
+ var import_protocols = require("@axiom-lattice/protocols");
541
+ function stringClaim(claims, ...names) {
542
+ for (const name of names) {
543
+ const value = claims[name];
544
+ if (typeof value === "string" && value.trim()) return value;
545
+ }
546
+ return void 0;
547
+ }
548
+ function providerError(message, cause) {
549
+ const error = new import_protocols.AuthError("PROVIDER_ERROR", 502, message);
550
+ if (cause !== void 0) {
551
+ Object.defineProperty(error, "cause", {
552
+ value: cause,
553
+ enumerable: false,
554
+ configurable: true,
555
+ writable: true
556
+ });
557
+ }
558
+ return error;
559
+ }
560
+ function principalFromClaims(claims) {
561
+ const subject = stringClaim(claims, "oid", "sub");
562
+ if (!subject) {
563
+ throw new import_protocols.AuthError("PROVIDER_ERROR", 502, "OIDC ID token has no subject");
564
+ }
565
+ const emailClaim = stringClaim(claims, "email");
566
+ const rawEmail = emailClaim ?? stringClaim(claims, "preferred_username", "upn");
567
+ const email = rawEmail?.trim().toLowerCase();
568
+ const name = stringClaim(claims, "name");
569
+ const picture = stringClaim(claims, "picture");
570
+ return {
571
+ subject,
572
+ method: "oidc",
573
+ ...email ? { email } : {},
574
+ ...email ? { emailVerified: emailClaim !== void 0 && claims.email_verified === true } : {},
575
+ ...name ? { name } : {},
576
+ ...picture ? { picture } : {},
577
+ claims
578
+ };
579
+ }
580
+ var OidcAuthProvider = class {
581
+ constructor(options) {
582
+ this.options = options;
583
+ this.descriptor = oidcDescriptor(options.config);
584
+ }
585
+ async getClient() {
586
+ return this.options.client ?? await loadOpenidClient();
587
+ }
588
+ async getConfiguration(client) {
589
+ if (!this.configurationPromise) {
590
+ this.configurationPromise = (async () => {
591
+ try {
592
+ const validateEndpoint = this.options.validateEndpoint ?? assertSafeOidcEndpoint;
593
+ const safety = {
594
+ allowInsecure: this.options.config.allowInsecure,
595
+ allowedPrivateHosts: this.options.config.allowedPrivateHosts ?? []
596
+ };
597
+ await validateEndpoint(this.options.config.issuer, safety);
598
+ const discovered = await client.discovery(
599
+ new URL(this.options.config.issuer),
600
+ this.options.config.clientId,
601
+ this.options.config.clientSecret,
602
+ {
603
+ timeoutSeconds: 5,
604
+ allowInsecure: this.options.config.allowInsecure
605
+ }
606
+ );
607
+ const metadata = discovered.serverMetadata();
608
+ const { authorization_endpoint, token_endpoint, jwks_uri } = metadata;
609
+ if (typeof authorization_endpoint !== "string" || !authorization_endpoint || typeof token_endpoint !== "string" || !token_endpoint || typeof jwks_uri !== "string" || !jwks_uri) {
610
+ throw new Error("OIDC discovery returned incomplete metadata");
611
+ }
612
+ for (const endpoint of [
613
+ authorization_endpoint,
614
+ token_endpoint,
615
+ jwks_uri
616
+ ]) {
617
+ await validateEndpoint(endpoint, safety);
618
+ }
619
+ return discovered;
620
+ } catch (error) {
621
+ throw providerError("OIDC discovery failed", error);
622
+ }
623
+ })().catch((error) => {
624
+ this.configurationPromise = void 0;
625
+ throw error;
626
+ });
627
+ }
628
+ return this.configurationPromise;
629
+ }
630
+ async begin() {
631
+ try {
632
+ const client = await this.getClient();
633
+ const config = await this.getConfiguration(client);
634
+ const state = client.randomState();
635
+ const codeVerifier = client.randomPKCECodeVerifier();
636
+ const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
637
+ const nonce = client.randomNonce();
638
+ const url = client.buildAuthorizationUrl(config, {
639
+ redirect_uri: this.options.config.redirectUri,
640
+ response_type: "code",
641
+ scope: this.options.config.scopes,
642
+ state,
643
+ nonce,
644
+ code_challenge: codeChallenge,
645
+ code_challenge_method: "S256"
646
+ });
647
+ const now = this.options.now?.() ?? Date.now();
648
+ const cookieValue = signStateCookie(
649
+ {
650
+ state,
651
+ codeVerifier,
652
+ nonce,
653
+ providerId: this.options.providerId,
654
+ exp: now + 6e5
655
+ },
656
+ this.options.config.stateSecret
657
+ );
658
+ return {
659
+ kind: "redirect",
660
+ url: url.href,
661
+ cookies: [
662
+ stateCookie(cookieValue, { secure: !this.options.config.insecureCookies })
663
+ ]
664
+ };
665
+ } catch (error) {
666
+ if (error instanceof import_protocols.AuthError) throw error;
667
+ throw providerError("OIDC authorization request failed", error);
668
+ }
669
+ }
670
+ async callback(ctx) {
671
+ try {
672
+ const rawCookie = ctx.cookies?.[OIDC_STATE_COOKIE];
673
+ if (!rawCookie) {
674
+ throw new import_protocols.AuthError("INVALID_STATE", 400, "OIDC state cookie is missing");
675
+ }
676
+ const now = this.options.now?.() ?? Date.now();
677
+ const stored = verifyStateCookie(
678
+ rawCookie,
679
+ this.options.config.stateSecret,
680
+ now
681
+ );
682
+ if (!stored) {
683
+ throw new import_protocols.AuthError(
684
+ "INVALID_STATE",
685
+ 400,
686
+ "OIDC state cookie is invalid or expired"
687
+ );
688
+ }
689
+ if (stored.providerId !== this.options.providerId) {
690
+ throw new import_protocols.AuthError(
691
+ "INVALID_STATE",
692
+ 400,
693
+ "OIDC state provider does not match"
694
+ );
695
+ }
696
+ if (ctx.params?.state !== stored.state) {
697
+ throw new import_protocols.AuthError("INVALID_STATE", 400, "OIDC state does not match");
698
+ }
699
+ const client = await this.getClient();
700
+ const config = await this.getConfiguration(client);
701
+ const currentUrl = new URL(this.options.config.redirectUri);
702
+ for (const [key, value] of Object.entries(ctx.params ?? {})) {
703
+ currentUrl.searchParams.set(key, value);
704
+ }
705
+ const tokens = await client.authorizationCodeGrant(config, currentUrl, {
706
+ pkceCodeVerifier: stored.codeVerifier,
707
+ expectedState: stored.state,
708
+ expectedNonce: stored.nonce
709
+ });
710
+ const claims = tokens.claims();
711
+ if (!claims) {
712
+ throw new import_protocols.AuthError("PROVIDER_ERROR", 502, "OIDC ID token is missing");
713
+ }
714
+ return principalFromClaims(claims);
715
+ } catch (error) {
716
+ if (error instanceof import_protocols.AuthError) throw error;
717
+ throw providerError("OIDC token exchange failed", error);
718
+ }
719
+ }
720
+ };
721
+ // Annotate the CommonJS export names for ESM import in node:
722
+ 0 && (module.exports = {
723
+ OIDC_STATE_COOKIE,
724
+ OIDC_STATE_PATH,
725
+ OidcAuthProvider,
726
+ assertSafeEndpointSyntax,
727
+ assertSafeOidcEndpoint,
728
+ assertTenantSpecificAzureIssuer,
729
+ clearStateCookie,
730
+ isLinkLocalOrMetadataAddress,
731
+ loadOpenidClient,
732
+ oidcDescriptor,
733
+ parseAllowedPrivateHosts,
734
+ principalFromClaims,
735
+ probeOidcProvider,
736
+ readCookie,
737
+ readOidcConfig,
738
+ serializeCookie,
739
+ signStateCookie,
740
+ stateCookie,
741
+ verifyStateCookie
742
+ });
743
+ //# sourceMappingURL=index.js.map