@hyper.authio/server 0.9.0 → 0.10.1

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
@@ -36,7 +36,7 @@ try {
36
36
  | `requiredScopes` | | Scopes every request must carry (all); `authioBearer` answers `403 insufficient_scope`. |
37
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
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). |
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
+ | `introspection` | `none` | `none`: local JWT validation; the caller is read from the token alone. `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; the caller is read from the token, plus the claims of the answer the token does not carry. `introspectionOnly`: introspection alone, for opaque (`Reference`) tokens (or encrypted ones without the key); the caller is read from the answer. A `Lightweight` access token needs `activeCheck` or `introspectionOnly` (see below). |
40
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. |
41
41
  | `introspectionCacheDuration` | 30 s | Never past the token's expiry; only active answers are kept. |
42
42
  | `clockSkew` | 30 s | |
@@ -51,6 +51,31 @@ A refusal is an `AuthioBearerError` whose `wwwAuthenticate` is `Bearer` when the
51
51
  `not_access_token`, `reference_token`, `decryption_failed`, …, and `inactive` / `unconfirmed` for introspection). A
52
52
  token addressed elsewhere is described with the audience the API accepts; introspection failures fail closed.
53
53
 
54
+ ### Lightweight and reference access tokens
55
+
56
+ Authio hands out a client's access tokens in the form its access token format setting names:
57
+
58
+ | Format | The token | Validate it with |
59
+ | --- | --- | --- |
60
+ | `Jwt` | A self-contained JWT with every claim. | Any mode. |
61
+ | `Lightweight` | A JWT with only the identity and session claims (`iss`, `sub`, `aud`, `exp`, `nbf`, `iat`, `jti`, `typ`, `client_id`, `azp`, `scope`, `sid`, `auth_time`, `acr`, `amr`, `idp`, `act`, `delegation_id`, `session_client`, `org_id`, `org_alias`). The `*_resource_access` roles and permissions, `subscription`, the profile and the email are only in the introspection answer. | `activeCheck` or `introspectionOnly`, which read the caller with the answer's claims, so `roles`, `permissions`, `kind` and `email` are complete. With `none` the caller has only what the token carries: no role or permission, and a consumer reads as a `client`. |
62
+ | `Reference` | An opaque handle, no JWT (refused locally as `reference_token`). | `introspectionOnly`. |
63
+
64
+ ```ts
65
+ const verifier = createAuthioVerifier({
66
+ issuer,
67
+ resource: "https://orders.example.com",
68
+ introspection: "activeCheck", // the client's access token format is Lightweight
69
+ client: { clientId: "orders-api", clientSecret: process.env.AUTHIO_CLIENT_SECRET! },
70
+ });
71
+ ```
72
+
73
+ Nothing tells a lightweight token apart from a full one that carries no role (a person without roles has no
74
+ `*_resource_access` either), so the verifier never guesses: the mode decides. The introspection answer is cached per
75
+ `jti` for `introspectionCacheDuration`, so `activeCheck` asks Authio once per token and cache period. The answer only
76
+ adds claims: one the token carries keeps the token's value, which its signature vouches for, whatever the answer says
77
+ (it names `client_id` for `azp`).
78
+
54
79
  ### Protected resource metadata (RFC 9728)
55
80
 
56
81
  ```ts
@@ -83,7 +108,7 @@ option.
83
108
 
84
109
  ## The caller
85
110
 
86
- `AuthioUser` reads every form of permission, the same way from a JWT or an introspection answer:
111
+ `AuthioUser` reads every form of permission, the same way from a JWT, an introspection answer or both (`activeCheck`):
87
112
 
88
113
  ```ts
89
114
  user.subject; user.clientId; user.scopes;
package/dist/index.cjs CHANGED
@@ -89,7 +89,10 @@ var AuthioUser = class _AuthioUser {
89
89
  credentialType;
90
90
  /** `credential_id`. */
91
91
  credentialId;
92
- /** Every claim, as the token or the introspection answer carried it. */
92
+ /**
93
+ * Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
94
+ * answer's the token does not carry).
95
+ */
93
96
  claims;
94
97
  flatRoles;
95
98
  /**
@@ -657,8 +660,9 @@ function createAuthioVerifier(options) {
657
660
  const type = typeof payload.typ === "string" ? payload.typ : void 0;
658
661
  if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
659
662
  throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
660
- if (introspection === "activeCheck") await confirmActive(token, payload, signal);
661
- return new AuthioUser(payload, claimSettings, token);
663
+ if (introspection !== "activeCheck") return new AuthioUser(payload, claimSettings, token);
664
+ const answer = await confirmActive(token, payload, signal);
665
+ return new AuthioUser({ ...claimsOfIntrospection(answer), ...payload }, claimSettings, token);
662
666
  };
663
667
  const confirmActive = async (token, payload, signal) => {
664
668
  const key = payload.jti ? `jti:${payload.jti}` : await keyForToken(token);
@@ -670,6 +674,7 @@ function createAuthioVerifier(options) {
670
674
  throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
671
675
  }
672
676
  if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
677
+ return result;
673
678
  };
674
679
  const verifyByIntrospection = async (token, signal) => {
675
680
  let result;