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