@hyper.authio/server 0.7.0 → 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.d.ts CHANGED
@@ -359,8 +359,14 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
359
359
  introspectionCacheDuration?: number;
360
360
  /** Seconds of clock skew tolerated on `exp` and `nbf`. Defaults to 30. */
361
361
  clockSkew?: number;
362
- /** Accepted signing algorithms. Defaults to the asymmetric ones (RS*, PS*, ES*). */
363
- validAlgorithms?: readonly string[];
362
+ /**
363
+ * The algorithm (or algorithms) access tokens are signed with, as the client registered in Authio signs them:
364
+ * RS/PS/ES 256/384/512, or HS256/384/512 verified with the `clientSecret` of `client` (its UTF-8 octets, at least
365
+ * 32, 48 or 64 bytes as RFC 7518 §3.2 asks). Only the client's own API holds that secret, so only it validates an HS*
366
+ * token locally; any other API validates such tokens with introspection. Left out, the asymmetric algorithms of the
367
+ * realm's keys are accepted, and never HS*.
368
+ */
369
+ accessTokenSignedResponseAlg?: string | readonly string[];
364
370
  /** Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. */
365
371
  rejectNonAccessTokens?: boolean;
366
372
  /**
@@ -370,6 +376,13 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
370
376
  clientEncryptionKey?: AuthioEncryptionKeySource;
371
377
  /** A PEM or JWK file holding `clientEncryptionKey` (Node only). */
372
378
  clientEncryptionKeyPath?: string;
379
+ /**
380
+ * The key management algorithms (`alg`) decrypted. Defaults to RSA-OAEP and RSA-OAEP-256; RSA1_5 cannot be listed,
381
+ * because Node cannot decrypt it.
382
+ */
383
+ encryptionAlgorithms?: readonly string[];
384
+ /** The content encryption algorithms (`enc`) decrypted. Defaults to A128CBC-HS256, A192CBC-HS384, A256CBC-HS512. */
385
+ encryptionMethods?: readonly string[];
373
386
  /** Accepted for parity with the other Authio SDKs; RSA1_5 stays refused, because Node cannot decrypt it. */
374
387
  allowLegacyEncryptionAlgorithms?: boolean;
375
388
  }
@@ -416,6 +429,7 @@ declare const AuthioBearerErrorDescriptions: {
416
429
  readonly Empty: "The token is empty.";
417
430
  readonly Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').";
418
431
  readonly SigningKeysUnavailable: "Authio could not be reached to read its signing keys.";
432
+ readonly 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').";
419
433
  };
420
434
  /**
421
435
  * Creates a token verifier.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, createAuthioClient, toScopeString, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, verifyAuthioJwt, AuthioTokenTypes } from '@hyper.authio/client';
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';
2
3
 
3
4
  // src/user.ts
4
5
  var accessTokenTypes = /* @__PURE__ */ new Set(["bearer", "api_key"]);
@@ -551,7 +552,8 @@ var AuthioBearerErrorDescriptions = {
551
552
  Unconfirmed: "Authio could not be reached to confirm that the token is active.",
552
553
  Empty: "The token is empty.",
553
554
  Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').",
554
- SigningKeysUnavailable: "Authio could not be reached to read its signing keys."
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')."
555
557
  };
556
558
  var maxCachedAnswers = 1e4;
557
559
  function createAuthioVerifier(options) {
@@ -571,7 +573,9 @@ function createAuthioVerifier(options) {
571
573
  const resourceMetadataUrl = resolveResourceMetadata(options.resourceMetadata, resource);
572
574
  const cacheDurationMs = seconds(options.introspectionCacheDuration, 30, "introspectionCacheDuration");
573
575
  const clockSkew = seconds(options.clockSkew, 30, "clockSkew") / 1e3;
574
- const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : void 0;
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;
575
579
  const rejectNonAccessTokens = options.rejectNonAccessTokens ?? true;
576
580
  const claimSettings = { roleClaims: options.roleClaims, groupClaims: options.groupClaims };
577
581
  const cache = /* @__PURE__ */ new Map();
@@ -579,7 +583,9 @@ function createAuthioVerifier(options) {
579
583
  const encryptionKey = options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0);
580
584
  const decrypt = encryptionKey === void 0 ? void 0 : createAuthioTokenDecrypter(encryptionKey, {
581
585
  setting: options.clientEncryptionKey === void 0 ? "clientEncryptionKeyPath" : "clientEncryptionKey",
582
- allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms
586
+ allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms,
587
+ encryptionAlgorithms: options.encryptionAlgorithms,
588
+ encryptionMethods: options.encryptionMethods
583
589
  });
584
590
  const requiredScopes = Object.freeze([...new Set((options.requiredScopes ?? []).filter((scope) => scope?.trim()))]);
585
591
  const introspect = async (token, key, signal) => {
@@ -609,7 +615,7 @@ function createAuthioVerifier(options) {
609
615
  }
610
616
  let keySet;
611
617
  try {
612
- keySet = await signingKeys(signal);
618
+ keySet = withClientSecret(await signingKeys(signal), clientSecret);
613
619
  } catch (error) {
614
620
  throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error, reason: "signing_keys_unavailable" });
615
621
  }
@@ -623,6 +629,8 @@ function createAuthioVerifier(options) {
623
629
  now: connection.clock.now()
624
630
  }));
625
631
  } catch (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 });
626
634
  if (error instanceof AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
627
635
  throw error;
628
636
  }
@@ -704,6 +712,35 @@ function resolveResourceMetadata(setting, resource) {
704
712
  throw new AuthioConfigurationError("resourceMetadata: true needs the API's resource identifier: set options.resource, or pass the metadata URL.");
705
713
  return protectedResourceMetadataUrl(resource);
706
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
+ }
707
744
  function resolveClient(options) {
708
745
  const given = options.client;
709
746
  if (!given) return void 0;