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