@absolutejs/auth 0.54.7 → 0.54.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  export * from './config';
2
2
  export * from './types';
3
+ export * from '../oidc/clientIdMetadata';
3
4
  export { createOidcAgentCredentialVerifier } from './oidcAdapter';
4
5
  export { agentHasScopes, resolveAgentPrincipal } from './principal';
5
6
  export { agentAuthChallenge, agentAuthPlugin } from './routes';
@@ -46,6 +46,15 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = import.meta.require;
48
48
 
49
+ // src/constants.ts
50
+ var SECONDS_IN_A_MINUTE = 60, MILLISECONDS_IN_A_SECOND = 1000, MILLISECONDS_IN_A_MINUTE, MINUTES_IN_AN_HOUR = 60, HOURS_IN_A_DAY = 24, MILLISECONDS_IN_A_DAY, MILLISECONDS_IN_AN_HOUR, COOKIE_MINUTES = 30, COOKIE_DURATION, DEFAULT_MAX_SESSIONS = 1e4;
51
+ var init_constants = __esm(() => {
52
+ MILLISECONDS_IN_A_MINUTE = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE;
53
+ MILLISECONDS_IN_A_DAY = MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTE * MINUTES_IN_AN_HOUR * HOURS_IN_A_DAY;
54
+ MILLISECONDS_IN_AN_HOUR = MILLISECONDS_IN_A_MINUTE * MINUTES_IN_AN_HOUR;
55
+ COOKIE_DURATION = SECONDS_IN_A_MINUTE * COOKIE_MINUTES;
56
+ });
57
+
49
58
  // src/agents/config.ts
50
59
  var DEFAULT_AGENT_RESOURCE_METADATA_ROUTE = "/.well-known/oauth-protected-resource";
51
60
  var agentProtectedResourceMetadata = (config) => ({
@@ -56,6 +65,91 @@ var agentProtectedResourceMetadata = (config) => ({
56
65
  resource: config.resource,
57
66
  scopes_supported: config.scopes
58
67
  });
68
+ // src/oidc/clientIdMetadata.ts
69
+ var secureUrl = (value) => {
70
+ try {
71
+ return new URL(value).protocol === "https:";
72
+ } catch {
73
+ return false;
74
+ }
75
+ };
76
+ var validateClientIdMetadataDocument = (document, expectedClientId) => {
77
+ const errors = [];
78
+ if (document.client_id !== expectedClientId)
79
+ errors.push("client_id does not match the metadata document URL");
80
+ if (!secureUrl(document.client_id))
81
+ errors.push("client_id must use HTTPS");
82
+ if (!Array.isArray(document.redirect_uris) || document.redirect_uris.length === 0)
83
+ errors.push("redirect_uris is required");
84
+ for (const uri of document.redirect_uris ?? []) {
85
+ try {
86
+ const parsed = new URL(uri);
87
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost")
88
+ errors.push("redirect URIs must use HTTPS or localhost");
89
+ } catch {
90
+ errors.push("redirect URI is invalid");
91
+ }
92
+ }
93
+ for (const [name, value] of [
94
+ ["client_uri", document.client_uri],
95
+ ["logo_uri", document.logo_uri],
96
+ ["policy_uri", document.policy_uri],
97
+ ["tos_uri", document.tos_uri],
98
+ ["jwks_uri", document.jwks_uri]
99
+ ]) {
100
+ if (value !== undefined && !secureUrl(value))
101
+ errors.push(`${name} must use HTTPS`);
102
+ }
103
+ return errors;
104
+ };
105
+ var clientIdMetadataToOAuthClient = (document) => ({
106
+ clientId: document.client_id,
107
+ grantTypes: document.grant_types ?? ["authorization_code", "refresh_token"],
108
+ ...document.jwks === undefined ? {} : { jwks: document.jwks.keys },
109
+ ...document.jwks_uri === undefined ? {} : { jwksUri: document.jwks_uri },
110
+ name: document.client_name ?? new URL(document.client_id).hostname,
111
+ redirectUris: [...document.redirect_uris],
112
+ scopes: document.scope?.split(" ").filter(Boolean) ?? []
113
+ });
114
+ var createClientIdMetadataResolver = ({
115
+ fetch: fetcher,
116
+ allow = async () => true,
117
+ cacheTtlMs = 5 * 60 * 1000,
118
+ maxBytes = 5 * 1024,
119
+ now = Date.now
120
+ }) => {
121
+ const cache = new Map;
122
+ return async (clientId) => {
123
+ if (!secureUrl(clientId) || !await allow(clientId))
124
+ return;
125
+ const cached = cache.get(clientId);
126
+ if (cached !== undefined && cached.expiresAt > now())
127
+ return cached.client;
128
+ const response = await fetcher(clientId, {
129
+ headers: { accept: "application/json" },
130
+ redirect: "error"
131
+ });
132
+ if (!response.ok)
133
+ return;
134
+ const declaredLength = Number(response.headers.get("content-length") ?? "0");
135
+ if (declaredLength > maxBytes)
136
+ return;
137
+ const bytes = new Uint8Array(await response.arrayBuffer());
138
+ if (bytes.byteLength > maxBytes)
139
+ return;
140
+ let document;
141
+ try {
142
+ document = JSON.parse(new TextDecoder().decode(bytes));
143
+ } catch {
144
+ return;
145
+ }
146
+ if (validateClientIdMetadataDocument(document, clientId).length > 0)
147
+ return;
148
+ const client = clientIdMetadataToOAuthClient(document);
149
+ cache.set(clientId, { client, expiresAt: now() + cacheTtlMs });
150
+ return client;
151
+ };
152
+ };
59
153
  // src/oidc/keys.ts
60
154
  var ENCODER = new TextEncoder;
61
155
  var ES256 = { hash: "SHA-256", name: "ECDSA" };
@@ -112,6 +206,96 @@ var verifyJwt = async (token, publicJwk) => {
112
206
  };
113
207
  };
114
208
 
209
+ // src/oidc/dpop.ts
210
+ init_constants();
211
+ var DEFAULT_MAX_AGE_MS = 60000;
212
+ var SECONDS_TO_MS = 1000;
213
+ var NONCE_WINDOW_SECONDS = 120;
214
+ var NONCE_WINDOW_MS = NONCE_WINDOW_SECONDS * MILLISECONDS_IN_A_SECOND;
215
+ var NONCE_PREVIOUS_WINDOWS_ACCEPTED = 1;
216
+ var hmacSha256 = async (secret, message) => {
217
+ const encoder = new TextEncoder;
218
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
219
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
220
+ return Buffer.from(new Uint8Array(signature)).toString("base64url");
221
+ };
222
+ var extractDpopNonceClaim = (proof) => {
223
+ const [, payloadSegment] = proof.split(".");
224
+ if (payloadSegment === undefined)
225
+ return;
226
+ try {
227
+ const payload = JSON.parse(Buffer.from(payloadSegment, "base64url").toString("utf8"));
228
+ if (typeof payload !== "object" || payload === null)
229
+ return;
230
+ const value = payload.nonce;
231
+ return typeof value === "string" ? value : undefined;
232
+ } catch {
233
+ return;
234
+ }
235
+ };
236
+ var mintDpopNonce = async ({
237
+ now = Date.now(),
238
+ secret
239
+ }) => {
240
+ const window = Math.floor(now / NONCE_WINDOW_MS);
241
+ return hmacSha256(secret, String(window));
242
+ };
243
+ var verifyDpopNonce = async ({
244
+ now = Date.now(),
245
+ nonce,
246
+ secret
247
+ }) => {
248
+ const currentWindow = Math.floor(now / NONCE_WINDOW_MS);
249
+ const candidates = await Promise.all(Array.from({ length: NONCE_PREVIOUS_WINDOWS_ACCEPTED + 1 }, (_, offset) => hmacSha256(secret, String(currentWindow - offset))));
250
+ return candidates.some((expected) => expected === nonce);
251
+ };
252
+ var decodeHeader = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
253
+ var normalizeHtu = (value) => {
254
+ try {
255
+ const url = new URL(String(value));
256
+ return `${url.origin}${url.pathname}`;
257
+ } catch {
258
+ return "";
259
+ }
260
+ };
261
+ var verifyDpopProof = async ({
262
+ accessToken,
263
+ htm,
264
+ htu,
265
+ isUsedJti,
266
+ maxAgeMs = DEFAULT_MAX_AGE_MS,
267
+ now = Date.now(),
268
+ proof
269
+ }) => {
270
+ if (proof === undefined)
271
+ return;
272
+ const [headerSegment] = proof.split(".");
273
+ if (headerSegment === undefined)
274
+ return;
275
+ const header = decodeHeader(headerSegment);
276
+ if (header?.typ !== "dpop+jwt" || header.alg !== "ES256" || header.jwk === undefined) {
277
+ return;
278
+ }
279
+ const verified = await verifyJwt(proof, header.jwk);
280
+ if (verified === undefined)
281
+ return;
282
+ const { payload } = verified;
283
+ if (accessToken !== undefined) {
284
+ const expectedAth = Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken))).toString("base64url");
285
+ if (payload.ath !== expectedAth)
286
+ return;
287
+ }
288
+ const iatMs = typeof payload.iat === "number" ? payload.iat * SECONDS_TO_MS : 0;
289
+ if (payload.htm !== htm || normalizeHtu(payload.htu) !== normalizeHtu(htu) || iatMs === 0 || Math.abs(now - iatMs) > maxAgeMs) {
290
+ return;
291
+ }
292
+ const jti = typeof payload.jti === "string" ? payload.jti : undefined;
293
+ if (jti !== undefined && isUsedJti !== undefined && await isUsedJti(jti)) {
294
+ return;
295
+ }
296
+ return { jkt: await jwkThumbprint(header.jwk), jti };
297
+ };
298
+
115
299
  // src/agents/oidcAdapter.ts
116
300
  var BEARER_PREFIX = "Bearer ";
117
301
  var MS_PER_SECOND = 1000;
@@ -125,15 +309,18 @@ var readAudience = (audience) => {
125
309
  };
126
310
  var createOidcAgentCredentialVerifier = ({
127
311
  issuer,
312
+ isUsedDpopJti,
313
+ maxDpopAgeMs,
128
314
  publicJwk,
315
+ requireDpop = false,
129
316
  resource
130
317
  }) => {
131
318
  const verifier = async (request) => {
132
319
  const authorization = request.headers.get("authorization");
133
- if (authorization === null || !authorization.startsWith(BEARER_PREFIX)) {
320
+ if (authorization === null || !authorization.startsWith(BEARER_PREFIX) && !authorization.startsWith("DPoP ")) {
134
321
  return;
135
322
  }
136
- const token = authorization.slice(BEARER_PREFIX.length).trim();
323
+ const token = authorization.slice(authorization.indexOf(" ") + 1).trim();
137
324
  if (token.length === 0)
138
325
  return;
139
326
  const verified = await verifyJwt(token, publicJwk);
@@ -141,6 +328,22 @@ var createOidcAgentCredentialVerifier = ({
141
328
  if (payload === undefined || payload.iss !== issuer || typeof payload.exp !== "number" || payload.exp <= Math.floor(Date.now() / MS_PER_SECOND) || !readAudience(payload.aud).includes(resource) || typeof payload.client_id !== "string") {
142
329
  return;
143
330
  }
331
+ const confirmation = payload.cnf;
332
+ const boundJkt = typeof confirmation === "object" && confirmation !== null && typeof confirmation.jkt === "string" ? confirmation.jkt : undefined;
333
+ if (boundJkt !== undefined || requireDpop) {
334
+ if (!authorization.startsWith("DPoP "))
335
+ return;
336
+ const proof = await verifyDpopProof({
337
+ accessToken: token,
338
+ htm: request.method,
339
+ htu: request.url,
340
+ isUsedJti: isUsedDpopJti,
341
+ ...maxDpopAgeMs === undefined ? {} : { maxAgeMs: maxDpopAgeMs },
342
+ proof: request.headers.get("dpop") ?? undefined
343
+ });
344
+ if (proof === undefined || boundJkt === undefined || proof.jkt !== boundJkt)
345
+ return;
346
+ }
144
347
  return {
145
348
  agentId: payload.client_id,
146
349
  claims: payload,
@@ -11278,6 +11481,7 @@ var createPostgresAgentRegistrationStore = (db) => ({
11278
11481
  }
11279
11482
  });
11280
11483
  export {
11484
+ validateClientIdMetadataDocument,
11281
11485
  resolveAgentPrincipal,
11282
11486
  createPostgresAgentRegistrationStore,
11283
11487
  createPostgresAgentDelegationStore,
@@ -11286,6 +11490,8 @@ export {
11286
11490
  createNeonAgentDelegationStore,
11287
11491
  createInMemoryAgentRegistrationStore,
11288
11492
  createInMemoryAgentDelegationStore,
11493
+ createClientIdMetadataResolver,
11494
+ clientIdMetadataToOAuthClient,
11289
11495
  agentRegistrationsTable,
11290
11496
  agentProtectedResourceMetadata,
11291
11497
  agentHasScopes,
@@ -11295,5 +11501,5 @@ export {
11295
11501
  DEFAULT_AGENT_RESOURCE_METADATA_ROUTE
11296
11502
  };
11297
11503
 
11298
- //# debugId=F730056D05217ECB64756E2164756E21
11504
+ //# debugId=BF8C5C40CCB4AFB564756E2164756E21
11299
11505
  //# sourceMappingURL=index.js.map