@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/README.md CHANGED
@@ -34,12 +34,13 @@ try {
34
34
  | `resource` | | The API's own resource identifier (RFC 8707); added to `audiences` and used for RFC 9728. |
35
35
  | `resourceMetadata` | off | `true` (or the URL) adds `resource_metadata="{origin}/.well-known/oauth-protected-resource{path}"` to `401` challenges (RFC 9728). |
36
36
  | `requiredScopes` | | Scopes every request must carry (all); `authioBearer` answers `403 insufficient_scope`. |
37
- | `clientEncryptionKey`, `clientEncryptionKeyPath` | | Decrypts access tokens Authio encrypts to this key (RSA-OAEP, RSA-OAEP-256) and validates them locally. RSA1_5 is refused (`legacy_algorithm`), even with `allowLegacyEncryptionAlgorithms`. |
37
+ | `clientEncryptionKey`, `clientEncryptionKeyPath` | | Decrypts access tokens Authio encrypts to this key and validates them locally. Without it nothing is decrypted. RSA1_5 is refused (`legacy_algorithm`), even with `allowLegacyEncryptionAlgorithms`. |
38
+ | `encryptionAlgorithms`, `encryptionMethods` | `RSA-OAEP`, `RSA-OAEP-256`; the CBC-HS methods | The JWE `alg` and `enc` accepted, as registered in Authio (AES-GCM may be listed; RSA1_5 cannot: Node cannot decrypt it). |
38
39
  | `introspection` | `none` | `none`: local JWT validation. `activeCheck`: then confirmed active by introspection (cached per `jti`), so a revoked token, an ended session or a suspension is refused before the token expires. `introspectionOnly`: introspection alone, for opaque tokens (or encrypted ones without the key). |
39
- | `client` | | The API's confidential client, needed by introspection: an `AuthioClient` or `{ clientId, clientSecret }` (or a private key). It must be the resource server's introspection client. |
40
+ | `client` | | The API's confidential client, needed by introspection: an `AuthioClient` or `{ clientId, clientSecret }` (or a private key), with `tokenEndpointAuthMethod` as the client is registered. It must be the resource server's introspection client. |
40
41
  | `introspectionCacheDuration` | 30 s | Never past the token's expiry; only active answers are kept. |
41
42
  | `clockSkew` | 30 s | |
42
- | `validAlgorithms` | RS*, PS*, ES* | |
43
+ | `accessTokenSignedResponseAlg` | RS*, PS*, ES* (never HS*) | The algorithm (or several) access tokens are signed with, as the client is registered in Authio. HS256/384/512 are verified with the `clientSecret` of `client: { clientId, clientSecret }` (at least 32, 48 or 64 bytes). |
43
44
  | `rejectNonAccessTokens` | `true` | Refuses a token whose `typ` is not `Bearer` (an ID token, for one). |
44
45
  | `roleClaims` | `["role"]` | Claims holding flat role names (a Role claim mapper). |
45
46
  | `groupClaims` | `["groups"]` | Claims holding group names (a Group claim mapper). |
@@ -60,6 +61,26 @@ const path = protectedResourceMetadataPath(verifier.resource!); // /.well-known/
60
61
  const document = createProtectedResourceMetadata({ issuer, resource: verifier.resource!, scopesSupported: ["orders:read"] });
61
62
  ```
62
63
 
64
+ ### Access tokens signed with the client secret (HS*)
65
+
66
+ Authio signs a client's access tokens with the algorithm of its access token setting. When that is HS256, HS384 or
67
+ HS512, the signature is made with the client secret: only who holds the secret validates the token locally, so that is
68
+ the client's own API.
69
+
70
+ ```ts
71
+ const verifier = createAuthioVerifier({
72
+ issuer,
73
+ audiences: "storefront",
74
+ accessTokenSignedResponseAlg: "HS256",
75
+ client: { clientId: "storefront", clientSecret: process.env.AUTHIO_CLIENT_SECRET! }, // 32+ bytes for HS256
76
+ });
77
+ ```
78
+
79
+ Third-party APIs never get the secret: they validate such tokens with introspection (`introspection:
80
+ "introspectionOnly"` and their own introspection client). Without `accessTokenSignedResponseAlg`, an HS* token is
81
+ refused (`invalid_algorithm`) with a description naming both ways. `authioBearer` and the MCP packages take the same
82
+ option.
83
+
63
84
  ## The caller
64
85
 
65
86
  `AuthioUser` reads every form of permission, the same way from a JWT or an introspection answer:
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var client = require('@hyper.authio/client');
4
+ var jose = require('jose');
4
5
 
5
6
  // src/user.ts
6
7
  var accessTokenTypes = /* @__PURE__ */ new Set(["bearer", "api_key"]);
@@ -553,7 +554,8 @@ var AuthioBearerErrorDescriptions = {
553
554
  Unconfirmed: "Authio could not be reached to confirm that the token is active.",
554
555
  Empty: "The token is empty.",
555
556
  Encrypted: "The token is encrypted; set clientEncryptionKey to decrypt it, or validate it with introspection ('introspectionOnly').",
556
- SigningKeysUnavailable: "Authio could not be reached to read its signing keys."
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')."
557
559
  };
558
560
  var maxCachedAnswers = 1e4;
559
561
  function createAuthioVerifier(options) {
@@ -573,7 +575,9 @@ function createAuthioVerifier(options) {
573
575
  const resourceMetadataUrl = resolveResourceMetadata(options.resourceMetadata, resource);
574
576
  const cacheDurationMs = client.seconds(options.introspectionCacheDuration, 30, "introspectionCacheDuration");
575
577
  const clockSkew = client.seconds(options.clockSkew, 30, "clockSkew") / 1e3;
576
- const algorithms = options.validAlgorithms && options.validAlgorithms.length > 0 ? [...options.validAlgorithms] : void 0;
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;
577
581
  const rejectNonAccessTokens = options.rejectNonAccessTokens ?? true;
578
582
  const claimSettings = { roleClaims: options.roleClaims, groupClaims: options.groupClaims };
579
583
  const cache = /* @__PURE__ */ new Map();
@@ -581,7 +585,9 @@ function createAuthioVerifier(options) {
581
585
  const encryptionKey = options.clientEncryptionKey ?? (options.clientEncryptionKeyPath ? client.encryptionKeyFromFile(options.clientEncryptionKeyPath) : void 0);
582
586
  const decrypt = encryptionKey === void 0 ? void 0 : client.createAuthioTokenDecrypter(encryptionKey, {
583
587
  setting: options.clientEncryptionKey === void 0 ? "clientEncryptionKeyPath" : "clientEncryptionKey",
584
- allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms
588
+ allowLegacyEncryptionAlgorithms: options.allowLegacyEncryptionAlgorithms,
589
+ encryptionAlgorithms: options.encryptionAlgorithms,
590
+ encryptionMethods: options.encryptionMethods
585
591
  });
586
592
  const requiredScopes = Object.freeze([...new Set((options.requiredScopes ?? []).filter((scope) => scope?.trim()))]);
587
593
  const introspect = async (token, key, signal) => {
@@ -611,7 +617,7 @@ function createAuthioVerifier(options) {
611
617
  }
612
618
  let keySet;
613
619
  try {
614
- keySet = await signingKeys(signal);
620
+ keySet = client.withClientSecret(await signingKeys(signal), clientSecret);
615
621
  } catch (error) {
616
622
  throw new AuthioBearerError(AuthioBearerErrorDescriptions.SigningKeysUnavailable, { cause: error, reason: "signing_keys_unavailable" });
617
623
  }
@@ -625,6 +631,8 @@ function createAuthioVerifier(options) {
625
631
  now: connection.clock.now()
626
632
  }));
627
633
  } catch (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 });
628
636
  if (error instanceof client.AuthioTokenValidationError) throw new AuthioBearerError(error.message, { cause: error, reason: error.reason });
629
637
  throw error;
630
638
  }
@@ -706,6 +714,35 @@ function resolveResourceMetadata(setting, resource) {
706
714
  throw new client.AuthioConfigurationError("resourceMetadata: true needs the API's resource identifier: set options.resource, or pass the metadata URL.");
707
715
  return protectedResourceMetadataUrl(resource);
708
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
+ }
709
746
  function resolveClient(options) {
710
747
  const given = options.client;
711
748
  if (!given) return void 0;