@hyper.authio/server 0.1.1 → 0.7.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 CHANGED
@@ -1,10 +1,45 @@
1
- import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, AuthioAsymmetricAlgorithms, createAuthioClient, toScopeString, AuthioDefaults, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenTypes } from '@hyper.authio/client';
2
- import { jwtVerify, createRemoteJWKSet, customFetch, errors, decodeProtectedHeader } from 'jose';
1
+ import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, createAuthioClient, toScopeString, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, verifyAuthioJwt, AuthioTokenTypes } from '@hyper.authio/client';
3
2
 
4
3
  // src/user.ts
5
- var AuthioUser = class {
4
+ var accessTokenTypes = /* @__PURE__ */ new Set(["bearer", "api_key"]);
5
+ var AuthioUser = class _AuthioUser {
6
6
  /** `sub`: the person's id, or the credential's for a machine caller. */
7
7
  subject;
8
+ /**
9
+ * Who called: `user` (a person signed in), `client` (a client as itself), `consumer` (a machine consumer of a
10
+ * client) or `account_credential` (a credential of an Authio account).
11
+ */
12
+ kind;
13
+ /**
14
+ * How a machine caller proved who it is (`client_secret`, `personal_access_token`, `api_key`,
15
+ * `consumer_api_key`); `undefined` for a person or a client.
16
+ */
17
+ credentialForm;
18
+ /** `consumer_name`, for a consumer. */
19
+ consumerName;
20
+ /** `credential_name`, for an account credential. */
21
+ credentialName;
22
+ /** `owner_user_id`: the person who made the credential. */
23
+ ownerUserId;
24
+ /**
25
+ * `owner_username`: the person who made the credential. On an account credential Authio wrote it to
26
+ * `consumer_name` before, which is read when `owner_username` is absent.
27
+ */
28
+ ownerUsername;
29
+ /** `credential_realm`: the one realm an account credential is limited to. */
30
+ credentialRealm;
31
+ /** `credential_client`: the one client an account credential is limited to. */
32
+ credentialClient;
33
+ /** Whether an administrator impersonates the subject. */
34
+ impersonated;
35
+ /** The administrator impersonating the subject. */
36
+ impersonator;
37
+ /** Whether a client acts for the subject through a delegation (`act` with `client_id`). */
38
+ delegated;
39
+ /** The clients acting for the subject, outermost first. */
40
+ actorChain;
41
+ /** `auth_time`, in seconds since the epoch. */
42
+ authTime;
8
43
  /** The client the token was issued to (`azp`, else `client_id`). */
9
44
  clientId;
10
45
  /** `sid`. */
@@ -58,8 +93,11 @@ var AuthioUser = class {
58
93
  * @param claims The token's payload, or the introspection answer's members.
59
94
  * @param settings Where flat roles and groups are read from.
60
95
  * @param token The access token, kept for forwarding and on-behalf-of.
96
+ * @param source Where the claims come from: a JWT (default) or an introspection answer. An introspection answer for
97
+ * a consumer that reports no `exp` and no `actor_type` is the consumer's API key (Authio reports no lifetime for a
98
+ * key), so its `credentialForm` is `consumer_api_key`.
61
99
  */
62
- constructor(claims, settings = {}, token) {
100
+ constructor(claims, settings = {}, token, source = "token") {
63
101
  const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
64
102
  this.claims = Object.freeze({ ...claims });
65
103
  this.subject = text(AuthioClaimTypes.Subject) ?? "";
@@ -89,11 +127,29 @@ var AuthioUser = class {
89
127
  this.preferredUsername = text(AuthioClaimTypes.PreferredUsername);
90
128
  this.credentialType = text(AuthioClaimTypes.CredentialType);
91
129
  this.credentialId = text(AuthioClaimTypes.CredentialId);
130
+ this.authTime = toSeconds(claims[AuthioClaimTypes.AuthTime]);
131
+ this.kind = this.credentialType === "client_consumer" ? "consumer" : this.credentialType === "account_secret" ? "account_credential" : this.authTime !== void 0 ? "user" : "client";
132
+ const machine = this.kind === "consumer" || this.kind === "account_credential";
133
+ const keyIntrospected = source === "introspection" && this.kind === "consumer" && claims[AuthioClaimTypes.Expiration] === void 0;
134
+ this.credentialForm = machine ? text(AuthioClaimTypes.ActorType) ?? (keyIntrospected ? "consumer_api_key" : "client_secret") : void 0;
135
+ this.consumerName = this.kind === "consumer" ? text(AuthioClaimTypes.ConsumerName) : void 0;
136
+ this.credentialName = text(AuthioClaimTypes.CredentialName);
137
+ this.ownerUserId = text(AuthioClaimTypes.OwnerUserId);
138
+ this.ownerUsername = text(AuthioClaimTypes.OwnerUsername) ?? (this.kind === "account_credential" ? text(AuthioClaimTypes.ConsumerName) : void 0);
139
+ this.credentialRealm = text(AuthioClaimTypes.CredentialRealm);
140
+ this.credentialClient = text(AuthioClaimTypes.CredentialClient);
141
+ this.delegated = this.actor?.clientId !== void 0;
142
+ this.actorChain = Object.freeze(chainOf(this.actor));
143
+ this.impersonated = !this.delegated && this.actor?.subject !== void 0;
144
+ this.impersonator = this.impersonated ? Object.freeze({ subject: this.actor.subject, name: this.actor.name }) : void 0;
92
145
  Object.defineProperty(this, "token", { value: token, enumerable: false });
93
146
  }
94
- /** Whether an actor exchanged the token on the subject's behalf. */
95
- get isDelegated() {
96
- return this.actor !== void 0;
147
+ /**
148
+ * Reads the caller from an introspection answer: `client_id` becomes `azp`, `username` becomes
149
+ * `preferred_username`, and `token_type` becomes `typ` unless it is `api_key` (a consumer's API key).
150
+ */
151
+ static fromIntrospection(result, settings = {}, token) {
152
+ return new _AuthioUser(claimsOfIntrospection(result), settings, token, "introspection");
97
153
  }
98
154
  /** Whether the scope was granted. */
99
155
  hasScope(scope) {
@@ -137,9 +193,20 @@ var AuthioUser = class {
137
193
  /** Every member but the raw token. */
138
194
  toJSON() {
139
195
  const { flatRoles: _flatRoles, ...members } = this;
140
- return { ...members, resourceAccess: this.resourceAccess.entries, isDelegated: this.isDelegated };
196
+ return { ...members, resourceAccess: this.resourceAccess.entries };
141
197
  }
142
198
  };
199
+ function claimsOfIntrospection(result) {
200
+ const { active: _active, token_type: tokenType, username, ...members } = result;
201
+ const claims = { ...members };
202
+ if (typeof tokenType === "string" && claims.typ === void 0 && tokenType.toLowerCase() !== "api_key") claims.typ = tokenType;
203
+ if (typeof username === "string" && claims.preferred_username === void 0) claims.preferred_username = username;
204
+ if (typeof result.client_id === "string" && claims.azp === void 0) claims.azp = result.client_id;
205
+ return claims;
206
+ }
207
+ function isAccessTokenType(type) {
208
+ return accessTokenTypes.has(type.toLowerCase());
209
+ }
143
210
  function valuesOf(value) {
144
211
  if (value === void 0 || value === null) return [];
145
212
  if (Array.isArray(value)) return value.flatMap((item) => item === null || item === void 0 ? [] : [toText(item)]);
@@ -168,11 +235,20 @@ function readActor(value, depth = 0) {
168
235
  if (!actor || typeof actor !== "object" || Array.isArray(actor) || depth > 16) return void 0;
169
236
  const record = actor;
170
237
  return Object.freeze({
171
- clientId: typeof record.client_id === "string" ? record.client_id : void 0,
172
- subject: typeof record.sub === "string" ? record.sub : void 0,
238
+ clientId: typeof record.client_id === "string" && record.client_id ? record.client_id : void 0,
239
+ subject: typeof record.sub === "string" && record.sub ? record.sub : void 0,
240
+ name: typeof record.name === "string" && record.name ? record.name : void 0,
173
241
  actor: readActor(record.act, depth + 1)
174
242
  });
175
243
  }
244
+ function chainOf(actor) {
245
+ const chain = [];
246
+ for (let current = actor; current; current = current.actor) if (current.clientId) chain.push(current.clientId);
247
+ return chain;
248
+ }
249
+ function toSeconds(value) {
250
+ return typeof value === "number" && Number.isFinite(value) ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : void 0;
251
+ }
176
252
  function toDate(value) {
177
253
  const seconds3 = typeof value === "number" ? value : typeof value === "string" && /^\d+$/.test(value) ? Number(value) : void 0;
178
254
  return seconds3 === void 0 ? void 0 : new Date(seconds3 * 1e3);
@@ -195,16 +271,19 @@ function distinctIgnoringCase(values) {
195
271
  return true;
196
272
  });
197
273
  }
198
-
199
- // src/rules.ts
200
274
  function authorize(user, ...rules) {
201
275
  const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
202
276
  if (failures.length === 0) return void 0;
203
- const bearer = failures.find((failure) => failure.error === "insufficient_user_authentication") ?? failures.find((failure) => failure.error);
277
+ const stepUps = failures.filter((failure) => failure.error === "insufficient_user_authentication");
278
+ const bearer = stepUps[0] ?? failures.find((failure) => failure.error);
279
+ const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
280
+ const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
204
281
  return {
205
282
  message: failures.map((failure) => failure.message).join(" "),
206
283
  ...bearer?.error ? { error: bearer.error } : {},
207
- ...bearer?.scope ? { scope: bearer.scope } : {}
284
+ ...bearer?.scope ? { scope: bearer.scope } : {},
285
+ ...acrValues ? { acrValues } : {},
286
+ ...maxAges.length > 0 ? { maxAge: Math.min(...maxAges) } : {}
208
287
  };
209
288
  }
210
289
  function requireScope(scopes, match = "any") {
@@ -259,23 +338,60 @@ function requireOrganization(organization) {
259
338
  }
260
339
  };
261
340
  }
262
- function requireAuthenticationLevel(minimumLevel) {
341
+ function requireAuthenticationLevel(minimumLevel, options = {}) {
263
342
  if (!Number.isInteger(minimumLevel) || minimumLevel < 0) throw new RangeError("The minimum level must be a non-negative integer.");
343
+ const recency = options.maxAge === void 0 ? void 0 : requireRecentAuthentication(options.maxAge, { clock: options.clock });
264
344
  return {
265
- description: `Authio authentication level >= ${minimumLevel}`,
266
- check: (user) => user.authenticationLevel !== void 0 && user.authenticationLevel >= minimumLevel ? void 0 : { message: `The authentication level must be at least ${minimumLevel}.`, error: "insufficient_user_authentication" }
345
+ description: `Authio authentication level >= ${minimumLevel}${recency ? `, ${recency.description}` : ""}`,
346
+ check: (user) => {
347
+ const levelMet = user.authenticationLevel !== void 0 && user.authenticationLevel >= minimumLevel;
348
+ const recent = recency?.check(user);
349
+ if (levelMet && !recent) return void 0;
350
+ return {
351
+ message: [levelMet ? void 0 : `The authentication level must be at least ${minimumLevel}.`, recent?.message].filter(Boolean).join(" "),
352
+ error: "insufficient_user_authentication",
353
+ acrValues: String(minimumLevel),
354
+ ...options.maxAge !== void 0 ? { maxAge: options.maxAge } : {}
355
+ };
356
+ }
267
357
  };
268
358
  }
269
- function requireAuthenticationMethod(methods, match = "any") {
359
+ var methodLevels = { mfa: "2", otp: "2", mca: "2", sms: "2", swk: "3", hwk: "3" };
360
+ function requireAuthenticationMethod(methods, match = "any", options = {}) {
270
361
  const required = listOf(methods, "authentication method");
362
+ const levels = required.map((method) => methodLevels[method.toLowerCase()]).filter((level) => level !== void 0);
363
+ const acrValues = options.acrValues ?? (levels.length === 0 ? void 0 : match === "all" ? strongestAcr(levels) : weakestAcr(levels));
271
364
  return {
272
365
  description: `Authio authentication method (${match}): ${required.join(", ")}`,
273
366
  check: (user) => {
274
367
  const used = user.authenticationMethods.map((method) => method.toLowerCase());
275
- return satisfies(required, match, (method) => used.includes(method.toLowerCase())) ? void 0 : { message: `The user must authenticate with (${match}): ${required.join(", ")}.`, error: "insufficient_user_authentication" };
368
+ return satisfies(required, match, (method) => used.includes(method.toLowerCase())) ? void 0 : {
369
+ message: `The user must authenticate with (${match}): ${required.join(", ")}.`,
370
+ error: "insufficient_user_authentication",
371
+ ...acrValues ? { acrValues } : {}
372
+ };
276
373
  }
277
374
  };
278
375
  }
376
+ function requireRecentAuthentication(maxAge, options = {}) {
377
+ if (!Number.isInteger(maxAge) || maxAge < 0) throw new RangeError("maxAge must be a non-negative whole number of seconds.");
378
+ const clock = options.clock ?? systemClock;
379
+ return {
380
+ description: `Authio sign-in at most ${maxAge} s old`,
381
+ check: (user) => user.authTime !== void 0 && clock.now() - user.authTime * 1e3 <= maxAge * 1e3 ? void 0 : {
382
+ message: user.authTime === void 0 ? `The token carries no sign-in time; a sign-in at most ${maxAge} seconds old is required.` : `The sign-in must be at most ${maxAge} seconds old.`,
383
+ error: "insufficient_user_authentication",
384
+ maxAge
385
+ }
386
+ };
387
+ }
388
+ function requireCallerKind(kinds) {
389
+ const required = listOf(kinds, "caller kind");
390
+ return {
391
+ description: `Authio caller kind: ${required.join(", ")}`,
392
+ check: (user) => required.includes(user.kind) ? void 0 : { message: `The caller is a ${user.kind}, not one of: ${required.join(", ")}.` }
393
+ };
394
+ }
279
395
  function requireClient(clientIds) {
280
396
  const required = listOf(clientIds, "client id");
281
397
  return {
@@ -307,24 +423,49 @@ function listOf(values, name) {
307
423
  function satisfies(required, match, holds) {
308
424
  return match === "all" ? required.every(holds) : required.some(holds);
309
425
  }
426
+ function strongestAcr(values) {
427
+ return rankAcr(values, (left, right) => right - left);
428
+ }
429
+ function weakestAcr(values) {
430
+ return rankAcr(values, (left, right) => left - right);
431
+ }
432
+ function rankAcr(values, order) {
433
+ const known = values.filter((value) => !!value);
434
+ const numeric = known.filter((value) => /^\d+$/.test(value)).sort((left, right) => order(Number(left), Number(right)));
435
+ return numeric[0] ?? known[0];
436
+ }
310
437
  function describeSources(sources) {
311
438
  return sources && sources.length > 0 ? ` from ${[...new Set(sources)].join(", ")}` : "";
312
439
  }
313
- var AuthioBearerError = class extends AuthioError {
440
+ var AuthioBearerError = class _AuthioBearerError extends AuthioError {
314
441
  status = 401;
315
442
  /** `invalid_token`, or `undefined` when the request had no token at all (RFC 6750 §3.1). */
316
443
  error;
317
444
  /** Why the token was refused, safe to send to the caller. */
318
445
  description;
446
+ /** Why the token was refused, as a stable code; `undefined` when there was no token or Authio could not be asked. */
447
+ reason;
448
+ /** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` when configured. */
449
+ resourceMetadata;
319
450
  constructor(description, options = {}) {
320
451
  super(description ?? "A valid Authio access token is required.", { cause: options.cause });
321
452
  this.error = "error" in options ? options.error : "invalid_token";
322
453
  this.description = description === void 0 ? void 0 : toHeaderText(description);
454
+ this.reason = options.reason;
455
+ this.resourceMetadata = options.resourceMetadata;
456
+ }
457
+ /** The same refusal, pointing at the API's protected resource metadata. */
458
+ withResourceMetadata(resourceMetadata) {
459
+ if (!resourceMetadata || resourceMetadata === this.resourceMetadata) return this;
460
+ return new _AuthioBearerError(this.description, { error: this.error, cause: this.cause, reason: this.reason, resourceMetadata });
323
461
  }
324
462
  /** The `WWW-Authenticate` value. */
325
463
  get wwwAuthenticate() {
326
- if (!this.error) return "Bearer";
327
- return this.description ? `Bearer error="${this.error}", error_description="${this.description}"` : `Bearer error="${this.error}"`;
464
+ const parameters = [];
465
+ if (this.error) parameters.push(`error="${this.error}"`);
466
+ if (this.error && this.description) parameters.push(`error_description="${this.description}"`);
467
+ if (this.resourceMetadata) parameters.push(`resource_metadata="${escapeQuoted(this.resourceMetadata)}"`);
468
+ return parameters.length === 0 ? "Bearer" : `Bearer ${parameters.join(", ")}`;
328
469
  }
329
470
  };
330
471
  var AuthioForbiddenError = class extends AuthioError {
@@ -341,8 +482,11 @@ var AuthioForbiddenError = class extends AuthioError {
341
482
  };
342
483
  function toForbiddenChallenge(failure) {
343
484
  if (!failure.error) return void 0;
344
- const value = `Bearer error="${failure.error}", error_description="${escapeQuoted(failure.message)}"`;
345
- return failure.scope ? `${value}, scope="${escapeQuoted(failure.scope)}"` : value;
485
+ const parameters = [`error="${failure.error}"`, `error_description="${escapeQuoted(toHeaderText(failure.message))}"`];
486
+ if (failure.scope) parameters.push(`scope="${escapeQuoted(failure.scope)}"`);
487
+ if (failure.acrValues) parameters.push(`acr_values="${escapeQuoted(failure.acrValues)}"`);
488
+ if (failure.maxAge !== void 0) parameters.push(`max_age=${failure.maxAge}`);
489
+ return `Bearer ${parameters.join(", ")}`;
346
490
  }
347
491
  function toProblemDetails(error) {
348
492
  if (error instanceof AuthioBearerError)
@@ -358,7 +502,9 @@ function toProblemDetails(error) {
358
502
  status: 403,
359
503
  detail: error.failure.message || "The caller is not allowed to access this resource.",
360
504
  ...error.failure.error ? { error: error.failure.error } : {},
361
- ...error.failure.scope ? { scope: error.failure.scope } : {}
505
+ ...error.failure.scope ? { scope: error.failure.scope } : {},
506
+ ...error.failure.acrValues ? { acr_values: error.failure.acrValues } : {},
507
+ ...error.failure.maxAge !== void 0 ? { max_age: error.failure.maxAge } : {}
362
508
  };
363
509
  }
364
510
  function toHeaderText(text) {
@@ -367,17 +513,53 @@ function toHeaderText(text) {
367
513
  function escapeQuoted(value) {
368
514
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
369
515
  }
516
+ function protectedResourceMetadataUrl(resource) {
517
+ const url = parseResource(resource);
518
+ const path = url.pathname.replace(/\/+$/, "");
519
+ return `${url.origin}${AuthioDefaults.ProtectedResourceMetadataPath}${path}`;
520
+ }
521
+ function protectedResourceMetadataPath(resource) {
522
+ return new URL(protectedResourceMetadataUrl(resource)).pathname;
523
+ }
524
+ function createProtectedResourceMetadata(options) {
525
+ parseResource(options.resource);
526
+ const document = {
527
+ ...options.extra,
528
+ resource: options.resource,
529
+ authorization_servers: [resolveIssuer(options)],
530
+ bearer_methods_supported: ["header"]
531
+ };
532
+ if (options.scopesSupported && options.scopesSupported.length > 0) document.scopes_supported = [...options.scopesSupported];
533
+ if (options.resourceName) document.resource_name = options.resourceName;
534
+ if (options.resourceDocumentation) document.resource_documentation = options.resourceDocumentation;
535
+ return document;
536
+ }
537
+ function parseResource(resource) {
538
+ let url;
539
+ try {
540
+ url = new URL(resource);
541
+ } catch (error) {
542
+ throw new AuthioConfigurationError(`The resource '${resource}' is not an absolute URL.`, { cause: error });
543
+ }
544
+ if (url.hash) throw new AuthioConfigurationError(`The resource '${resource}' must not carry a fragment (RFC 8707).`);
545
+ return url;
546
+ }
547
+
548
+ // src/verifier.ts
370
549
  var AuthioBearerErrorDescriptions = {
371
550
  Inactive: "Authio reports the token as not active to this API's client. It was revoked or has expired or its session ended; or this API's client is not the introspection client of the resource server the token is for.",
372
551
  Unconfirmed: "Authio could not be reached to confirm that the token is active.",
373
552
  Empty: "The token is empty.",
374
- Encrypted: "The token is encrypted; validate it with introspection ('introspectionOnly').",
553
+ Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').",
375
554
  SigningKeysUnavailable: "Authio could not be reached to read its signing keys."
376
555
  };
377
556
  var maxCachedAnswers = 1e4;
378
557
  function createAuthioVerifier(options) {
379
- const audiences = [...new Set((typeof options.audiences === "string" ? [options.audiences] : [...options.audiences ?? []]).filter((value) => value?.trim()))];
380
- if (audiences.length === 0) throw new AuthioConfigurationError("The Authio verifier needs at least one audience: the API's resource identifier or client id.");
558
+ const resource = options.resource?.trim() || void 0;
559
+ const given = typeof options.audiences === "string" ? [options.audiences] : [...options.audiences ?? []];
560
+ const audiences = [...new Set([...given, ...resource ? [resource] : []].filter((value) => value?.trim()))];
561
+ if (audiences.length === 0)
562
+ throw new AuthioConfigurationError("The Authio verifier needs at least one audience: the API's resource identifier or client id.");
381
563
  const connection = resolveConnection(options);
382
564
  const discovery = createAuthioDiscoveryClient(connection);
383
565
  const introspection = options.introspection ?? "none";
@@ -386,13 +568,20 @@ function createAuthioVerifier(options) {
386
568
  throw new AuthioConfigurationError(
387
569
  `Authio introspection ('${introspection}') needs the API's confidential client: pass options.client with its clientId and clientSecret or private key.`
388
570
  );
571
+ const resourceMetadataUrl = resolveResourceMetadata(options.resourceMetadata, resource);
389
572
  const cacheDurationMs = seconds(options.introspectionCacheDuration, 30, "introspectionCacheDuration");
390
573
  const clockSkew = seconds(options.clockSkew, 30, "clockSkew") / 1e3;
391
- const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : [...AuthioAsymmetricAlgorithms];
574
+ const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : void 0;
392
575
  const rejectNonAccessTokens = options.rejectNonAccessTokens ?? true;
393
576
  const claimSettings = { roleClaims: options.roleClaims, groupClaims: options.groupClaims };
394
577
  const cache = /* @__PURE__ */ new Map();
395
- let keys;
578
+ const signingKeys = createAuthioSigningKeys(connection, discovery);
579
+ const encryptionKey = options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0);
580
+ const decrypt = encryptionKey === void 0 ? void 0 : createAuthioTokenDecrypter(encryptionKey, {
581
+ setting: options.clientEncryptionKey === void 0 ? "clientEncryptionKeyPath" : "clientEncryptionKey",
582
+ allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms
583
+ });
584
+ const requiredScopes = Object.freeze([...new Set((options.requiredScopes ?? []).filter((scope) => scope?.trim()))]);
396
585
  const introspect = async (token, key, signal) => {
397
586
  const now = connection.clock.now();
398
587
  const cached = cache.get(key);
@@ -407,37 +596,39 @@ function createAuthioVerifier(options) {
407
596
  }
408
597
  return result;
409
598
  };
410
- const signingKeys = async (signal) => {
411
- const document = await discovery.get(signal);
412
- const uri = document.jwks_uri;
413
- if (!uri) throw new AuthioError("The Authio discovery document has no jwks_uri.");
414
- if (!keys || keys.uri !== uri) keys = { uri, set: createRemoteJWKSet(new URL(uri), { [customFetch]: connection.fetch, timeoutDuration: connection.httpTimeoutMs }) };
415
- return keys.set;
416
- };
417
599
  const verifyJwt = async (token, signal) => {
418
- if (isAuthioTokenEncrypted(token)) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Encrypted);
600
+ let signed = token;
601
+ if (isAuthioTokenEncrypted(token)) {
602
+ if (!decrypt) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Encrypted, { reason: "decryption_failed" });
603
+ try {
604
+ signed = await decrypt(token);
605
+ } catch (error) {
606
+ if (error instanceof AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
607
+ throw error;
608
+ }
609
+ }
419
610
  let keySet;
420
611
  try {
421
612
  keySet = await signingKeys(signal);
422
613
  } catch (error) {
423
- throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error });
614
+ throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error, reason: "signing_keys_unavailable" });
424
615
  }
425
616
  let payload;
426
617
  try {
427
- ({ payload } = await jwtVerify(token, keySet, {
618
+ ({ payload } = await verifyAuthioJwt(signed, keySet, {
428
619
  issuer: connection.issuer,
429
- audience: audiences,
620
+ audiences,
430
621
  algorithms,
431
- clockTolerance: clockSkew,
432
- requiredClaims: ["exp"],
433
- currentDate: new Date(connection.clock.now())
622
+ clockSkew,
623
+ now: connection.clock.now()
434
624
  }));
435
625
  } catch (error) {
436
- throw new AuthioBearerError(describeJwtFailure(error, audiences, token), { cause: error });
626
+ if (error instanceof AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
627
+ throw error;
437
628
  }
438
629
  const type = typeof payload.typ === "string" ? payload.typ : void 0;
439
630
  if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
440
- throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`);
631
+ throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
441
632
  if (introspection === "activeCheck") await confirmActive(token, payload, signal);
442
633
  return new AuthioUser(payload, claimSettings, token);
443
634
  };
@@ -448,9 +639,9 @@ function createAuthioVerifier(options) {
448
639
  result = await introspect(token, key, signal);
449
640
  } catch (error) {
450
641
  if (signal?.aborted) throw error;
451
- throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error });
642
+ throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
452
643
  }
453
- if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive);
644
+ if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
454
645
  };
455
646
  const verifyByIntrospection = async (token, signal) => {
456
647
  let result;
@@ -458,38 +649,42 @@ function createAuthioVerifier(options) {
458
649
  result = await introspect(token, await keyForToken(token), signal);
459
650
  } catch (error) {
460
651
  if (signal?.aborted) throw error;
461
- throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error });
652
+ throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
462
653
  }
463
- if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive);
464
- if (result.iss && result.iss !== connection.issuer) throw new AuthioBearerError(`The issuer '${result.iss}' is invalid`);
654
+ if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
655
+ if (result.iss && result.iss !== connection.issuer) throw new AuthioBearerError(`The issuer '${result.iss}' is invalid`, { reason: "invalid_issuer" });
465
656
  const tokenAudiences = [result.aud ?? []].flat();
466
- if (!tokenAudiences.some((audience) => audiences.includes(audience))) throw new AuthioBearerError(describeAudience(tokenAudiences, audiences));
657
+ if (!tokenAudiences.some((audience) => audiences.includes(audience)))
658
+ throw new AuthioBearerError(describeAudience(tokenAudiences, audiences), { reason: "invalid_audience" });
467
659
  if (typeof result.exp === "number" && (result.exp + clockSkew) * 1e3 <= connection.clock.now())
468
- throw new AuthioBearerError(`The token expired at '${new Date(result.exp * 1e3).toISOString()}'`);
469
- const { active: _active, token_type: tokenType, username, ...members } = result;
470
- const claims = { ...members };
471
- if (typeof tokenType === "string" && claims.typ === void 0) claims.typ = tokenType;
472
- if (typeof username === "string" && claims.preferred_username === void 0) claims.preferred_username = username;
473
- if (typeof result.client_id === "string" && claims.azp === void 0) claims.azp = result.client_id;
660
+ throw new AuthioBearerError(`The token expired at '${new Date(result.exp * 1e3).toISOString()}'`, { reason: "expired" });
661
+ const claims = claimsOfIntrospection(result);
474
662
  const type = typeof claims.typ === "string" ? claims.typ : void 0;
475
- if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
476
- throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`);
477
- return new AuthioUser(claims, claimSettings, token);
663
+ if (rejectNonAccessTokens && type !== void 0 && !isAccessTokenType(type))
664
+ throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
665
+ return new AuthioUser(claims, claimSettings, token, "introspection");
478
666
  };
479
667
  const verify = async (token, signal) => {
480
- if (typeof token !== "string" || !token.trim()) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Empty);
481
- return introspection === "introspectionOnly" ? verifyByIntrospection(token.trim(), signal) : verifyJwt(token.trim(), signal);
668
+ try {
669
+ if (typeof token !== "string" || !token.trim()) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Empty, { reason: "malformed" });
670
+ return introspection === "introspectionOnly" ? await verifyByIntrospection(token.trim(), signal) : await verifyJwt(token.trim(), signal);
671
+ } catch (error) {
672
+ throw error instanceof AuthioBearerError ? error.withResourceMetadata(resourceMetadataUrl) : error;
673
+ }
482
674
  };
483
675
  return {
484
676
  connection,
485
677
  discovery,
486
678
  audiences,
679
+ resource,
680
+ resourceMetadataUrl,
681
+ requiredScopes,
487
682
  introspection,
488
683
  client,
489
684
  verify,
490
685
  async verifyAuthorizationHeader(header, signal) {
491
686
  const token = readBearerToken(header);
492
- if (token === void 0) throw new AuthioBearerError(void 0, { error: void 0 });
687
+ if (token === void 0) throw new AuthioBearerError(void 0, { error: void 0, resourceMetadata: resourceMetadataUrl });
493
688
  return verify(token, signal);
494
689
  }
495
690
  };
@@ -502,6 +697,13 @@ async function keyForToken(token) {
502
697
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)));
503
698
  return `token:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
504
699
  }
700
+ function resolveResourceMetadata(setting, resource) {
701
+ if (!setting) return void 0;
702
+ if (typeof setting === "string") return setting;
703
+ if (!resource)
704
+ throw new AuthioConfigurationError("resourceMetadata: true needs the API's resource identifier: set options.resource, or pass the metadata URL.");
705
+ return protectedResourceMetadataUrl(resource);
706
+ }
505
707
  function resolveClient(options) {
506
708
  const given = options.client;
507
709
  if (!given) return void 0;
@@ -521,44 +723,11 @@ function resolveClient(options) {
521
723
  function isClient(value) {
522
724
  return typeof value.introspect === "function" && typeof value.tokens === "object";
523
725
  }
524
- function describeJwtFailure(error, audiences, token) {
525
- if (error instanceof errors.JWTExpired) {
526
- const exp = error.payload.exp;
527
- return typeof exp === "number" ? `The token expired at '${new Date(exp * 1e3).toISOString()}'` : "The token has expired";
528
- }
529
- if (error instanceof errors.JWTClaimValidationFailed) {
530
- switch (error.claim) {
531
- case "aud":
532
- return describeAudience([error.payload.aud ?? []].flat(), audiences);
533
- case "iss":
534
- return `The issuer '${error.payload.iss ?? ""}' is invalid`;
535
- case "nbf":
536
- return typeof error.payload.nbf === "number" ? `The token is not valid before '${new Date(error.payload.nbf * 1e3).toISOString()}'` : "The token is not valid yet";
537
- case "exp":
538
- return "The token has no expiration";
539
- default:
540
- return `The token's ${error.claim} claim is invalid`;
541
- }
542
- }
543
- if (error instanceof errors.JWSSignatureVerificationFailed) return "The signature is invalid";
544
- if (error instanceof errors.JWKSNoMatchingKey) return "The signature key was not found";
545
- if (error instanceof errors.JOSEAlgNotAllowed) return `The algorithm '${algorithmOf(token)}' is not available`;
546
- if (error instanceof errors.JWKSTimeout || error instanceof errors.JWKSInvalid) return AuthioBearerErrorDescriptions.SigningKeysUnavailable;
547
- if (error instanceof errors.JWTInvalid || error instanceof errors.JWSInvalid) return "The token is malformed";
548
- return void 0;
549
- }
550
726
  function describeAudience(tokenAudiences, audiences) {
551
727
  const known = tokenAudiences.length === 0 ? "" : ` ${tokenAudiences.map((audience) => `'${audience}'`).join(" and ")}`;
552
728
  const accepted = audiences.map((audience) => `'${audience}'`).join(" or ");
553
729
  return audiences.length === 1 ? `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for that resource (RFC 8707).` : `The token is not addressed to this API: its audience${known} is not ${accepted}. Request the token for one of those resources (RFC 8707).`;
554
730
  }
555
- function algorithmOf(token) {
556
- try {
557
- return String(decodeProtectedHeader(token).alg ?? "");
558
- } catch {
559
- return "";
560
- }
561
- }
562
731
  function createAuthioWebhooks(options) {
563
732
  const secrets = [...new Set([options.secret, ...options.secrets ?? []].filter((secret) => !!secret?.trim()))];
564
733
  if (secrets.length === 0) throw new AuthioConfigurationError("Authio webhooks need the webhook's secret.");
@@ -709,6 +878,6 @@ function tokenOf(subject) {
709
878
  return typeof subject === "string" ? subject : subject.token;
710
879
  }
711
880
 
712
- export { AuthioBearerError, AuthioBearerErrorDescriptions, AuthioForbiddenError, AuthioUser, allOf, anyOf, authorize, createAuthioVerifier, createAuthioWebhooks, createForwardingFetch, createMemoryDeduplicationStore, createOnBehalfOf, readBearerToken, requireAuthenticationLevel, requireAuthenticationMethod, requireClaim, requireClient, requireGroup, requireOrganization, requirePermission, requireRole, requireScope, toForbiddenChallenge, toProblemDetails };
881
+ export { AuthioBearerError, AuthioBearerErrorDescriptions, AuthioForbiddenError, AuthioUser, allOf, anyOf, authorize, createAuthioVerifier, createAuthioWebhooks, createForwardingFetch, createMemoryDeduplicationStore, createOnBehalfOf, createProtectedResourceMetadata, protectedResourceMetadataPath, protectedResourceMetadataUrl, readBearerToken, requireAuthenticationLevel, requireAuthenticationMethod, requireCallerKind, requireClaim, requireClient, requireGroup, requireOrganization, requirePermission, requireRecentAuthentication, requireRole, requireScope, toForbiddenChallenge, toProblemDetails };
713
882
  //# sourceMappingURL=index.js.map
714
883
  //# sourceMappingURL=index.js.map