@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 +24 -3
- package/dist/index.cjs +41 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +42 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
| `
|
|
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.
|
|
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;
|