@hyper.authio/server 0.8.1 → 0.10.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 +31 -3
- package/dist/index.cjs +16 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -13
- package/dist/index.d.ts +27 -13
- package/dist/index.js +17 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ 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 | |
|
|
43
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). |
|
|
44
|
-
| `rejectNonAccessTokens` | `true` | Refuses a token whose `typ` is not `Bearer` (an ID token, for one). |
|
|
44
|
+
| `rejectNonAccessTokens` | `true` | Refuses a token whose `typ` claim is not `Bearer` (an ID token, for one). The header `typ` must be `at+jwt` (RFC 9068 §4) regardless. |
|
|
45
45
|
| `roleClaims` | `["role"]` | Claims holding flat role names (a Role claim mapper). |
|
|
46
46
|
| `groupClaims` | `["groups"]` | Claims holding group names (a Group claim mapper). |
|
|
47
47
|
|
|
@@ -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 (Authio answers a single
|
|
77
|
+
`aud`, and `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
|
|
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;
|
|
@@ -134,6 +159,9 @@ if (refusal) {
|
|
|
134
159
|
| Sign-in too weak or too old (step-up) | `401` | `Bearer error="insufficient_user_authentication", error_description="…", acr_values="…", max_age=…` |
|
|
135
160
|
| Missing role, permission, group, claim, organization, client or caller kind | `403` | none |
|
|
136
161
|
|
|
162
|
+
When several rules fail at once, a step-up is asked for only if every failing rule is a step-up: a missing scope, role
|
|
163
|
+
or permission, which no stronger sign-in fixes, answers `403` (with `insufficient_scope` for a scope) and no step-up.
|
|
164
|
+
|
|
137
165
|
`resource_metadata` (RFC 9728 §5.1) is added to every `401` when the verifier publishes the protected resource
|
|
138
166
|
metadata (`resourceMetadata`): the verifier's own refusals carry it, and `new AuthioForbiddenError(refusal, {
|
|
139
167
|
resourceMetadata: verifier.resourceMetadataUrl })` (or `withResourceMetadata`) adds it to a step-up; the guards of
|
package/dist/index.cjs
CHANGED
|
@@ -43,7 +43,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
43
43
|
actorChain;
|
|
44
44
|
/** `auth_time`, in seconds since the epoch. */
|
|
45
45
|
authTime;
|
|
46
|
-
/** The client the token was issued to (`
|
|
46
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
47
47
|
clientId;
|
|
48
48
|
/** `sid`. */
|
|
49
49
|
sessionId;
|
|
@@ -89,7 +89,10 @@ var AuthioUser = class _AuthioUser {
|
|
|
89
89
|
credentialType;
|
|
90
90
|
/** `credential_id`. */
|
|
91
91
|
credentialId;
|
|
92
|
-
/**
|
|
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
|
/**
|
|
@@ -104,7 +107,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
104
107
|
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
105
108
|
this.claims = Object.freeze({ ...claims });
|
|
106
109
|
this.subject = text(client.AuthioClaimTypes.Subject) ?? "";
|
|
107
|
-
this.clientId = text(client.AuthioClaimTypes.
|
|
110
|
+
this.clientId = text(client.AuthioClaimTypes.ClientId) ?? text(client.AuthioClaimTypes.AuthorizedParty);
|
|
108
111
|
this.sessionId = text(client.AuthioClaimTypes.SessionId);
|
|
109
112
|
this.tokenId = text(client.AuthioClaimTypes.JwtId);
|
|
110
113
|
this.audiences = Object.freeze(distinct(valuesOf(claims[client.AuthioClaimTypes.Audience])));
|
|
@@ -277,8 +280,9 @@ function distinctIgnoringCase(values) {
|
|
|
277
280
|
function authorize(user, ...rules) {
|
|
278
281
|
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
279
282
|
if (failures.length === 0) return void 0;
|
|
280
|
-
const
|
|
281
|
-
const
|
|
283
|
+
const others = failures.filter((failure) => failure.error !== "insufficient_user_authentication");
|
|
284
|
+
const stepUps = others.length === 0 ? failures : [];
|
|
285
|
+
const bearer = stepUps[0] ?? others.find((failure) => failure.error);
|
|
282
286
|
const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
|
|
283
287
|
const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
|
|
284
288
|
return {
|
|
@@ -638,13 +642,15 @@ function createAuthioVerifier(options) {
|
|
|
638
642
|
}
|
|
639
643
|
let payload;
|
|
640
644
|
try {
|
|
641
|
-
|
|
645
|
+
let header;
|
|
646
|
+
({ payload, header } = await client.verifyAuthioJwt(signed, keySet, {
|
|
642
647
|
issuer: connection.issuer,
|
|
643
648
|
audiences,
|
|
644
649
|
algorithms,
|
|
645
650
|
clockSkew,
|
|
646
651
|
now: connection.clock.now()
|
|
647
652
|
}));
|
|
653
|
+
client.requireAccessTokenType(header);
|
|
648
654
|
} catch (error) {
|
|
649
655
|
if (error instanceof client.AuthioTokenValidationError && error.reason === "invalid_algorithm" && symmetric.length === 0 && isSymmetricToken(signed))
|
|
650
656
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Symmetric, { cause: error, reason: error.reason });
|
|
@@ -654,8 +660,9 @@ function createAuthioVerifier(options) {
|
|
|
654
660
|
const type = typeof payload.typ === "string" ? payload.typ : void 0;
|
|
655
661
|
if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
|
|
656
662
|
throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
|
|
657
|
-
if (introspection
|
|
658
|
-
|
|
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);
|
|
659
666
|
};
|
|
660
667
|
const confirmActive = async (token, payload, signal) => {
|
|
661
668
|
const key = payload.jti ? `jti:${payload.jti}` : await keyForToken(token);
|
|
@@ -667,6 +674,7 @@ function createAuthioVerifier(options) {
|
|
|
667
674
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
|
|
668
675
|
}
|
|
669
676
|
if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
|
|
677
|
+
return result;
|
|
670
678
|
};
|
|
671
679
|
const verifyByIntrospection = async (token, signal) => {
|
|
672
680
|
let result;
|