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