@hyper.authio/server 0.8.0 → 0.9.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 +23 -5
- package/dist/index.cjs +36 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -11
- package/dist/index.d.ts +30 -11
- package/dist/index.js +37 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ try {
|
|
|
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
|
|
|
@@ -120,11 +120,28 @@ const refusal = authorize(user, requirePermission("orders", "write"), anyOf(requ
|
|
|
120
120
|
|
|
121
121
|
if (refusal) {
|
|
122
122
|
const error = new AuthioForbiddenError(refusal);
|
|
123
|
-
//
|
|
124
|
-
//
|
|
123
|
+
// error.status: 401 when the refusal asks for a stronger or more recent sign-in (insufficient_user_authentication,
|
|
124
|
+
// RFC 9470 §3), 403 otherwise; error.wwwAuthenticate for scope (insufficient_scope) and sign-in refusals;
|
|
125
|
+
// toProblemDetails(error) for an RFC 9457 body with the same status.
|
|
125
126
|
}
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
| Refusal | Status | `WWW-Authenticate` |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| No token | `401` | `Bearer` (no `error`, RFC 6750 §3.1) |
|
|
132
|
+
| Token not valid | `401` | `Bearer error="invalid_token", error_description="…"` |
|
|
133
|
+
| Missing scope | `403` | `Bearer error="insufficient_scope", error_description="…", scope="…"` |
|
|
134
|
+
| Sign-in too weak or too old (step-up) | `401` | `Bearer error="insufficient_user_authentication", error_description="…", acr_values="…", max_age=…` |
|
|
135
|
+
| Missing role, permission, group, claim, organization, client or caller kind | `403` | none |
|
|
136
|
+
|
|
137
|
+
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
|
|
138
|
+
or permission, which no stronger sign-in fixes, answers `403` (with `insufficient_scope` for a scope) and no step-up.
|
|
139
|
+
|
|
140
|
+
`resource_metadata` (RFC 9728 §5.1) is added to every `401` when the verifier publishes the protected resource
|
|
141
|
+
metadata (`resourceMetadata`): the verifier's own refusals carry it, and `new AuthioForbiddenError(refusal, {
|
|
142
|
+
resourceMetadata: verifier.resourceMetadataUrl })` (or `withResourceMetadata`) adds it to a step-up; the guards of
|
|
143
|
+
`@hyper.authio/express` do it for you. The table is `conformance/challenges.json`, which every Authio library runs.
|
|
144
|
+
|
|
128
145
|
Rules: `requireScope`, `requireRole`, `requirePermission`, `requireGroup`, `requireClaim`, `requireOrganization`,
|
|
129
146
|
`requireAuthenticationLevel`, `requireAuthenticationMethod`, `requireRecentAuthentication`, `requireCallerKind`,
|
|
130
147
|
`requireClient`, combined with `allOf` and `anyOf`. `authorize` checks every rule and joins their messages.
|
|
@@ -132,8 +149,9 @@ Rules: `requireScope`, `requireRole`, `requirePermission`, `requireGroup`, `requ
|
|
|
132
149
|
**Step-up (RFC 9470)**: `requireAuthenticationLevel(2, { maxAge: 300 })`, `requireAuthenticationMethod("otp")` and
|
|
133
150
|
`requireRecentAuthentication(300)` refuse with `insufficient_user_authentication`, `acrValues` (the level asked for;
|
|
134
151
|
`2` for `mfa`/`otp`/`mca`, `3` for `swk`/`hwk`) and `maxAge`, so the challenge reads
|
|
135
|
-
`Bearer error="insufficient_user_authentication", error_description="…", acr_values="2", max_age=300
|
|
136
|
-
answers it with `createStepUpAuthorizationUrl` from
|
|
152
|
+
`Bearer error="insufficient_user_authentication", error_description="…", acr_values="2", max_age=300`, answered with
|
|
153
|
+
`401` (RFC 9470 §3; `403` before 0.8.1). A client answers it with `createStepUpAuthorizationUrl` from
|
|
154
|
+
`@hyper.authio/client`.
|
|
137
155
|
|
|
138
156
|
## Webhooks
|
|
139
157
|
|
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;
|
|
@@ -104,7 +104,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
104
104
|
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
105
105
|
this.claims = Object.freeze({ ...claims });
|
|
106
106
|
this.subject = text(client.AuthioClaimTypes.Subject) ?? "";
|
|
107
|
-
this.clientId = text(client.AuthioClaimTypes.
|
|
107
|
+
this.clientId = text(client.AuthioClaimTypes.ClientId) ?? text(client.AuthioClaimTypes.AuthorizedParty);
|
|
108
108
|
this.sessionId = text(client.AuthioClaimTypes.SessionId);
|
|
109
109
|
this.tokenId = text(client.AuthioClaimTypes.JwtId);
|
|
110
110
|
this.audiences = Object.freeze(distinct(valuesOf(claims[client.AuthioClaimTypes.Audience])));
|
|
@@ -277,8 +277,9 @@ function distinctIgnoringCase(values) {
|
|
|
277
277
|
function authorize(user, ...rules) {
|
|
278
278
|
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
279
279
|
if (failures.length === 0) return void 0;
|
|
280
|
-
const
|
|
281
|
-
const
|
|
280
|
+
const others = failures.filter((failure) => failure.error !== "insufficient_user_authentication");
|
|
281
|
+
const stepUps = others.length === 0 ? failures : [];
|
|
282
|
+
const bearer = stepUps[0] ?? others.find((failure) => failure.error);
|
|
282
283
|
const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
|
|
283
284
|
const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
|
|
284
285
|
return {
|
|
@@ -471,16 +472,30 @@ var AuthioBearerError = class _AuthioBearerError extends client.AuthioError {
|
|
|
471
472
|
return parameters.length === 0 ? "Bearer" : `Bearer ${parameters.join(", ")}`;
|
|
472
473
|
}
|
|
473
474
|
};
|
|
474
|
-
var AuthioForbiddenError = class extends client.AuthioError {
|
|
475
|
-
|
|
475
|
+
var AuthioForbiddenError = class _AuthioForbiddenError extends client.AuthioError {
|
|
476
|
+
/** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
|
|
477
|
+
status;
|
|
476
478
|
failure;
|
|
477
|
-
|
|
479
|
+
/** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
|
|
480
|
+
resourceMetadata;
|
|
481
|
+
constructor(failure, options = {}) {
|
|
478
482
|
super(failure.message);
|
|
479
483
|
this.failure = failure;
|
|
484
|
+
this.status = failure.error === "insufficient_user_authentication" ? 401 : 403;
|
|
485
|
+
this.resourceMetadata = options.resourceMetadata;
|
|
480
486
|
}
|
|
481
|
-
/** The
|
|
487
|
+
/** The same refusal, pointing at the API's protected resource metadata. */
|
|
488
|
+
withResourceMetadata(resourceMetadata) {
|
|
489
|
+
if (!resourceMetadata || resourceMetadata === this.resourceMetadata) return this;
|
|
490
|
+
return new _AuthioForbiddenError(this.failure, { resourceMetadata });
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
|
|
494
|
+
* carries `resource_metadata` when `resourceMetadata` is set.
|
|
495
|
+
*/
|
|
482
496
|
get wwwAuthenticate() {
|
|
483
|
-
|
|
497
|
+
const challenge = toForbiddenChallenge(this.failure);
|
|
498
|
+
return challenge && this.status === 401 && this.resourceMetadata ? `${challenge}, resource_metadata="${escapeQuoted(this.resourceMetadata)}"` : challenge;
|
|
484
499
|
}
|
|
485
500
|
};
|
|
486
501
|
function toForbiddenChallenge(failure) {
|
|
@@ -494,15 +509,15 @@ function toForbiddenChallenge(failure) {
|
|
|
494
509
|
function toProblemDetails(error) {
|
|
495
510
|
if (error instanceof AuthioBearerError)
|
|
496
511
|
return {
|
|
497
|
-
type:
|
|
512
|
+
type: unauthorizedType,
|
|
498
513
|
title: "Unauthorized",
|
|
499
514
|
status: 401,
|
|
500
515
|
detail: "A valid Authio access token is required."
|
|
501
516
|
};
|
|
502
517
|
return {
|
|
503
|
-
type: "https://tools.ietf.org/html/rfc9110#section-15.5.4",
|
|
504
|
-
title: "Forbidden",
|
|
505
|
-
status:
|
|
518
|
+
type: error.status === 401 ? unauthorizedType : "https://tools.ietf.org/html/rfc9110#section-15.5.4",
|
|
519
|
+
title: error.status === 401 ? "Unauthorized" : "Forbidden",
|
|
520
|
+
status: error.status,
|
|
506
521
|
detail: error.failure.message || "The caller is not allowed to access this resource.",
|
|
507
522
|
...error.failure.error ? { error: error.failure.error } : {},
|
|
508
523
|
...error.failure.scope ? { scope: error.failure.scope } : {},
|
|
@@ -510,6 +525,7 @@ function toProblemDetails(error) {
|
|
|
510
525
|
...error.failure.maxAge !== void 0 ? { max_age: error.failure.maxAge } : {}
|
|
511
526
|
};
|
|
512
527
|
}
|
|
528
|
+
var unauthorizedType = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
|
|
513
529
|
function toHeaderText(text) {
|
|
514
530
|
return [...text].map((character) => character >= " " && character <= "~" && character !== '"' && character !== "\\" ? character : "?").join("");
|
|
515
531
|
}
|
|
@@ -623,13 +639,15 @@ function createAuthioVerifier(options) {
|
|
|
623
639
|
}
|
|
624
640
|
let payload;
|
|
625
641
|
try {
|
|
626
|
-
|
|
642
|
+
let header;
|
|
643
|
+
({ payload, header } = await client.verifyAuthioJwt(signed, keySet, {
|
|
627
644
|
issuer: connection.issuer,
|
|
628
645
|
audiences,
|
|
629
646
|
algorithms,
|
|
630
647
|
clockSkew,
|
|
631
648
|
now: connection.clock.now()
|
|
632
649
|
}));
|
|
650
|
+
client.requireAccessTokenType(header);
|
|
633
651
|
} catch (error) {
|
|
634
652
|
if (error instanceof client.AuthioTokenValidationError && error.reason === "invalid_algorithm" && symmetric.length === 0 && isSymmetricToken(signed))
|
|
635
653
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Symmetric, { cause: error, reason: error.reason });
|
|
@@ -905,13 +923,16 @@ function createOnBehalfOf(options) {
|
|
|
905
923
|
};
|
|
906
924
|
const token = await getToken(subject, request.signal);
|
|
907
925
|
const response = await attempt(token);
|
|
908
|
-
if (response.status !== 401) return response;
|
|
926
|
+
if (response.status !== 401 || isStepUpChallenge(response)) return response;
|
|
909
927
|
await invalidate(subject, token.accessToken);
|
|
910
928
|
return attempt(await getToken(subject, request.signal));
|
|
911
929
|
};
|
|
912
930
|
}
|
|
913
931
|
};
|
|
914
932
|
}
|
|
933
|
+
function isStepUpChallenge(response) {
|
|
934
|
+
return client.parseAuthioBearerChallenge(response.headers.get("www-authenticate"))?.error === client.AuthioOAuthErrors.InsufficientUserAuthentication;
|
|
935
|
+
}
|
|
915
936
|
function tokenOf(subject) {
|
|
916
937
|
if (!subject) return void 0;
|
|
917
938
|
return typeof subject === "string" ? subject : subject.token;
|