@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/dist/index.d.cts
CHANGED
|
@@ -75,7 +75,7 @@ declare class AuthioUser {
|
|
|
75
75
|
readonly actorChain: readonly string[];
|
|
76
76
|
/** `auth_time`, in seconds since the epoch. */
|
|
77
77
|
readonly authTime: number | undefined;
|
|
78
|
-
/** The client the token was issued to (`
|
|
78
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
79
79
|
readonly clientId: string | undefined;
|
|
80
80
|
/** `sid`. */
|
|
81
81
|
readonly sessionId: string | undefined;
|
|
@@ -203,9 +203,11 @@ interface AuthioRule {
|
|
|
203
203
|
check(user: AuthioUser): AuthioRuleFailure | undefined;
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
|
-
* Checks every rule. On refusal the message joins every rule's
|
|
207
|
-
*
|
|
208
|
-
*
|
|
206
|
+
* Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
|
|
207
|
+
* permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
|
|
208
|
+
* refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
|
|
209
|
+
* Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
|
|
210
|
+
* with the strongest `acr_values` and the shortest `max_age` they ask for.
|
|
209
211
|
*/
|
|
210
212
|
declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
|
|
211
213
|
/**
|
|
@@ -264,7 +266,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
|
|
|
264
266
|
*/
|
|
265
267
|
declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
|
|
266
268
|
/**
|
|
267
|
-
* Requires the token to have been issued to one of the clients (`
|
|
269
|
+
* Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
|
|
268
270
|
*/
|
|
269
271
|
declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
|
|
270
272
|
/**
|
|
@@ -301,13 +303,26 @@ declare class AuthioBearerError extends AuthioError {
|
|
|
301
303
|
get wwwAuthenticate(): string;
|
|
302
304
|
}
|
|
303
305
|
/**
|
|
304
|
-
*
|
|
306
|
+
* A rule refused a caller whose token is valid. A refusal asking for a stronger or more recent sign-in
|
|
307
|
+
* (`insufficient_user_authentication`) answers `401`, as RFC 9470 §3 has it, naming the protected resource metadata
|
|
308
|
+
* (`resource_metadata`, RFC 9728 §5.1) when the API publishes it; every other refusal answers `403`, with
|
|
309
|
+
* `wwwAuthenticate` when it is about scopes (`insufficient_scope`, RFC 6750 §3.1).
|
|
305
310
|
*/
|
|
306
311
|
declare class AuthioForbiddenError extends AuthioError {
|
|
307
|
-
|
|
312
|
+
/** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
|
|
313
|
+
readonly status: 401 | 403;
|
|
308
314
|
readonly failure: AuthioRuleFailure;
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
/** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
|
|
316
|
+
readonly resourceMetadata: string | undefined;
|
|
317
|
+
constructor(failure: AuthioRuleFailure, options?: {
|
|
318
|
+
resourceMetadata?: string;
|
|
319
|
+
});
|
|
320
|
+
/** The same refusal, pointing at the API's protected resource metadata. */
|
|
321
|
+
withResourceMetadata(resourceMetadata: string | undefined): AuthioForbiddenError;
|
|
322
|
+
/**
|
|
323
|
+
* The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
|
|
324
|
+
* carries `resource_metadata` when `resourceMetadata` is set.
|
|
325
|
+
*/
|
|
311
326
|
get wwwAuthenticate(): string | undefined;
|
|
312
327
|
}
|
|
313
328
|
/**
|
|
@@ -316,7 +331,8 @@ declare class AuthioForbiddenError extends AuthioError {
|
|
|
316
331
|
*/
|
|
317
332
|
declare function toForbiddenChallenge(failure: AuthioRuleFailure): string | undefined;
|
|
318
333
|
/**
|
|
319
|
-
* Problem details (RFC 9457) for a `401` or `403
|
|
334
|
+
* Problem details (RFC 9457) for a `401` or `403`, with the refusal's `status`: a step-up refusal is `401`
|
|
335
|
+
* Unauthorized and carries `error`, `acr_values` and `max_age`.
|
|
320
336
|
*/
|
|
321
337
|
declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenError): Record<string, unknown>;
|
|
322
338
|
|
|
@@ -367,7 +383,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
|
|
|
367
383
|
* realm's keys are accepted, and never HS*.
|
|
368
384
|
*/
|
|
369
385
|
accessTokenSignedResponseAlg?: string | readonly string[];
|
|
370
|
-
/**
|
|
386
|
+
/**
|
|
387
|
+
* Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
|
|
388
|
+
* whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
|
|
389
|
+
*/
|
|
371
390
|
rejectNonAccessTokens?: boolean;
|
|
372
391
|
/**
|
|
373
392
|
* The private half of the encryption key Authio encrypts this API's access tokens to (the resource server's or its
|
package/dist/index.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ declare class AuthioUser {
|
|
|
75
75
|
readonly actorChain: readonly string[];
|
|
76
76
|
/** `auth_time`, in seconds since the epoch. */
|
|
77
77
|
readonly authTime: number | undefined;
|
|
78
|
-
/** The client the token was issued to (`
|
|
78
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
79
79
|
readonly clientId: string | undefined;
|
|
80
80
|
/** `sid`. */
|
|
81
81
|
readonly sessionId: string | undefined;
|
|
@@ -203,9 +203,11 @@ interface AuthioRule {
|
|
|
203
203
|
check(user: AuthioUser): AuthioRuleFailure | undefined;
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
|
-
* Checks every rule. On refusal the message joins every rule's
|
|
207
|
-
*
|
|
208
|
-
*
|
|
206
|
+
* Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
|
|
207
|
+
* permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
|
|
208
|
+
* refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
|
|
209
|
+
* Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
|
|
210
|
+
* with the strongest `acr_values` and the shortest `max_age` they ask for.
|
|
209
211
|
*/
|
|
210
212
|
declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
|
|
211
213
|
/**
|
|
@@ -264,7 +266,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
|
|
|
264
266
|
*/
|
|
265
267
|
declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
|
|
266
268
|
/**
|
|
267
|
-
* Requires the token to have been issued to one of the clients (`
|
|
269
|
+
* Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
|
|
268
270
|
*/
|
|
269
271
|
declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
|
|
270
272
|
/**
|
|
@@ -301,13 +303,26 @@ declare class AuthioBearerError extends AuthioError {
|
|
|
301
303
|
get wwwAuthenticate(): string;
|
|
302
304
|
}
|
|
303
305
|
/**
|
|
304
|
-
*
|
|
306
|
+
* A rule refused a caller whose token is valid. A refusal asking for a stronger or more recent sign-in
|
|
307
|
+
* (`insufficient_user_authentication`) answers `401`, as RFC 9470 §3 has it, naming the protected resource metadata
|
|
308
|
+
* (`resource_metadata`, RFC 9728 §5.1) when the API publishes it; every other refusal answers `403`, with
|
|
309
|
+
* `wwwAuthenticate` when it is about scopes (`insufficient_scope`, RFC 6750 §3.1).
|
|
305
310
|
*/
|
|
306
311
|
declare class AuthioForbiddenError extends AuthioError {
|
|
307
|
-
|
|
312
|
+
/** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
|
|
313
|
+
readonly status: 401 | 403;
|
|
308
314
|
readonly failure: AuthioRuleFailure;
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
/** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
|
|
316
|
+
readonly resourceMetadata: string | undefined;
|
|
317
|
+
constructor(failure: AuthioRuleFailure, options?: {
|
|
318
|
+
resourceMetadata?: string;
|
|
319
|
+
});
|
|
320
|
+
/** The same refusal, pointing at the API's protected resource metadata. */
|
|
321
|
+
withResourceMetadata(resourceMetadata: string | undefined): AuthioForbiddenError;
|
|
322
|
+
/**
|
|
323
|
+
* The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
|
|
324
|
+
* carries `resource_metadata` when `resourceMetadata` is set.
|
|
325
|
+
*/
|
|
311
326
|
get wwwAuthenticate(): string | undefined;
|
|
312
327
|
}
|
|
313
328
|
/**
|
|
@@ -316,7 +331,8 @@ declare class AuthioForbiddenError extends AuthioError {
|
|
|
316
331
|
*/
|
|
317
332
|
declare function toForbiddenChallenge(failure: AuthioRuleFailure): string | undefined;
|
|
318
333
|
/**
|
|
319
|
-
* Problem details (RFC 9457) for a `401` or `403
|
|
334
|
+
* Problem details (RFC 9457) for a `401` or `403`, with the refusal's `status`: a step-up refusal is `401`
|
|
335
|
+
* Unauthorized and carries `error`, `acr_values` and `max_age`.
|
|
320
336
|
*/
|
|
321
337
|
declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenError): Record<string, unknown>;
|
|
322
338
|
|
|
@@ -367,7 +383,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
|
|
|
367
383
|
* realm's keys are accepted, and never HS*.
|
|
368
384
|
*/
|
|
369
385
|
accessTokenSignedResponseAlg?: string | readonly string[];
|
|
370
|
-
/**
|
|
386
|
+
/**
|
|
387
|
+
* Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
|
|
388
|
+
* whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
|
|
389
|
+
*/
|
|
371
390
|
rejectNonAccessTokens?: boolean;
|
|
372
391
|
/**
|
|
373
392
|
* The private half of the encryption key Authio encrypts this API's access tokens to (the resource server's or its
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, AuthioHmacAlgorithms, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, AuthioAsymmetricAlgorithms, requireHmacSecretLength, createAuthioClient, toScopeString, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, withClientSecret, verifyAuthioJwt, AuthioTokenTypes } from '@hyper.authio/client';
|
|
1
|
+
import { systemClock, AuthioClaimTypes, parseResourceAccess, AuthioError, AuthioDefaults, resolveIssuer, AuthioConfigurationError, resolveConnection, createAuthioDiscoveryClient, seconds, AuthioHmacAlgorithms, createAuthioSigningKeys, encryptionKeyFromFile, createAuthioTokenDecrypter, AuthioAsymmetricAlgorithms, requireHmacSecretLength, createAuthioClient, toScopeString, parseAuthioBearerChallenge, AuthioOAuthErrors, AuthioWebhookHeaders, verifyWebhookSignature, tryParseWebhookEvent, toClientCredentialsToken, isAuthioTokenEncrypted, AuthioTokenValidationError, withClientSecret, verifyAuthioJwt, requireAccessTokenType, AuthioTokenTypes } from '@hyper.authio/client';
|
|
2
2
|
import { decodeProtectedHeader } from 'jose';
|
|
3
3
|
|
|
4
4
|
// src/user.ts
|
|
@@ -41,7 +41,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
41
41
|
actorChain;
|
|
42
42
|
/** `auth_time`, in seconds since the epoch. */
|
|
43
43
|
authTime;
|
|
44
|
-
/** The client the token was issued to (`
|
|
44
|
+
/** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
|
|
45
45
|
clientId;
|
|
46
46
|
/** `sid`. */
|
|
47
47
|
sessionId;
|
|
@@ -102,7 +102,7 @@ var AuthioUser = class _AuthioUser {
|
|
|
102
102
|
const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
|
|
103
103
|
this.claims = Object.freeze({ ...claims });
|
|
104
104
|
this.subject = text(AuthioClaimTypes.Subject) ?? "";
|
|
105
|
-
this.clientId = text(AuthioClaimTypes.
|
|
105
|
+
this.clientId = text(AuthioClaimTypes.ClientId) ?? text(AuthioClaimTypes.AuthorizedParty);
|
|
106
106
|
this.sessionId = text(AuthioClaimTypes.SessionId);
|
|
107
107
|
this.tokenId = text(AuthioClaimTypes.JwtId);
|
|
108
108
|
this.audiences = Object.freeze(distinct(valuesOf(claims[AuthioClaimTypes.Audience])));
|
|
@@ -275,8 +275,9 @@ function distinctIgnoringCase(values) {
|
|
|
275
275
|
function authorize(user, ...rules) {
|
|
276
276
|
const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
|
|
277
277
|
if (failures.length === 0) return void 0;
|
|
278
|
-
const
|
|
279
|
-
const
|
|
278
|
+
const others = failures.filter((failure) => failure.error !== "insufficient_user_authentication");
|
|
279
|
+
const stepUps = others.length === 0 ? failures : [];
|
|
280
|
+
const bearer = stepUps[0] ?? others.find((failure) => failure.error);
|
|
280
281
|
const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
|
|
281
282
|
const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
|
|
282
283
|
return {
|
|
@@ -469,16 +470,30 @@ var AuthioBearerError = class _AuthioBearerError extends AuthioError {
|
|
|
469
470
|
return parameters.length === 0 ? "Bearer" : `Bearer ${parameters.join(", ")}`;
|
|
470
471
|
}
|
|
471
472
|
};
|
|
472
|
-
var AuthioForbiddenError = class extends AuthioError {
|
|
473
|
-
|
|
473
|
+
var AuthioForbiddenError = class _AuthioForbiddenError extends AuthioError {
|
|
474
|
+
/** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
|
|
475
|
+
status;
|
|
474
476
|
failure;
|
|
475
|
-
|
|
477
|
+
/** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
|
|
478
|
+
resourceMetadata;
|
|
479
|
+
constructor(failure, options = {}) {
|
|
476
480
|
super(failure.message);
|
|
477
481
|
this.failure = failure;
|
|
482
|
+
this.status = failure.error === "insufficient_user_authentication" ? 401 : 403;
|
|
483
|
+
this.resourceMetadata = options.resourceMetadata;
|
|
478
484
|
}
|
|
479
|
-
/** The
|
|
485
|
+
/** The same refusal, pointing at the API's protected resource metadata. */
|
|
486
|
+
withResourceMetadata(resourceMetadata) {
|
|
487
|
+
if (!resourceMetadata || resourceMetadata === this.resourceMetadata) return this;
|
|
488
|
+
return new _AuthioForbiddenError(this.failure, { resourceMetadata });
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
|
|
492
|
+
* carries `resource_metadata` when `resourceMetadata` is set.
|
|
493
|
+
*/
|
|
480
494
|
get wwwAuthenticate() {
|
|
481
|
-
|
|
495
|
+
const challenge = toForbiddenChallenge(this.failure);
|
|
496
|
+
return challenge && this.status === 401 && this.resourceMetadata ? `${challenge}, resource_metadata="${escapeQuoted(this.resourceMetadata)}"` : challenge;
|
|
482
497
|
}
|
|
483
498
|
};
|
|
484
499
|
function toForbiddenChallenge(failure) {
|
|
@@ -492,15 +507,15 @@ function toForbiddenChallenge(failure) {
|
|
|
492
507
|
function toProblemDetails(error) {
|
|
493
508
|
if (error instanceof AuthioBearerError)
|
|
494
509
|
return {
|
|
495
|
-
type:
|
|
510
|
+
type: unauthorizedType,
|
|
496
511
|
title: "Unauthorized",
|
|
497
512
|
status: 401,
|
|
498
513
|
detail: "A valid Authio access token is required."
|
|
499
514
|
};
|
|
500
515
|
return {
|
|
501
|
-
type: "https://tools.ietf.org/html/rfc9110#section-15.5.4",
|
|
502
|
-
title: "Forbidden",
|
|
503
|
-
status:
|
|
516
|
+
type: error.status === 401 ? unauthorizedType : "https://tools.ietf.org/html/rfc9110#section-15.5.4",
|
|
517
|
+
title: error.status === 401 ? "Unauthorized" : "Forbidden",
|
|
518
|
+
status: error.status,
|
|
504
519
|
detail: error.failure.message || "The caller is not allowed to access this resource.",
|
|
505
520
|
...error.failure.error ? { error: error.failure.error } : {},
|
|
506
521
|
...error.failure.scope ? { scope: error.failure.scope } : {},
|
|
@@ -508,6 +523,7 @@ function toProblemDetails(error) {
|
|
|
508
523
|
...error.failure.maxAge !== void 0 ? { max_age: error.failure.maxAge } : {}
|
|
509
524
|
};
|
|
510
525
|
}
|
|
526
|
+
var unauthorizedType = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
|
|
511
527
|
function toHeaderText(text) {
|
|
512
528
|
return [...text].map((character) => character >= " " && character <= "~" && character !== '"' && character !== "\\" ? character : "?").join("");
|
|
513
529
|
}
|
|
@@ -621,13 +637,15 @@ function createAuthioVerifier(options) {
|
|
|
621
637
|
}
|
|
622
638
|
let payload;
|
|
623
639
|
try {
|
|
624
|
-
|
|
640
|
+
let header;
|
|
641
|
+
({ payload, header } = await verifyAuthioJwt(signed, keySet, {
|
|
625
642
|
issuer: connection.issuer,
|
|
626
643
|
audiences,
|
|
627
644
|
algorithms,
|
|
628
645
|
clockSkew,
|
|
629
646
|
now: connection.clock.now()
|
|
630
647
|
}));
|
|
648
|
+
requireAccessTokenType(header);
|
|
631
649
|
} catch (error) {
|
|
632
650
|
if (error instanceof AuthioTokenValidationError && error.reason === "invalid_algorithm" && symmetric.length === 0 && isSymmetricToken(signed))
|
|
633
651
|
throw new AuthioBearerError(AuthioBearerErrorDescriptions.Symmetric, { cause: error, reason: error.reason });
|
|
@@ -903,13 +921,16 @@ function createOnBehalfOf(options) {
|
|
|
903
921
|
};
|
|
904
922
|
const token = await getToken(subject, request.signal);
|
|
905
923
|
const response = await attempt(token);
|
|
906
|
-
if (response.status !== 401) return response;
|
|
924
|
+
if (response.status !== 401 || isStepUpChallenge(response)) return response;
|
|
907
925
|
await invalidate(subject, token.accessToken);
|
|
908
926
|
return attempt(await getToken(subject, request.signal));
|
|
909
927
|
};
|
|
910
928
|
}
|
|
911
929
|
};
|
|
912
930
|
}
|
|
931
|
+
function isStepUpChallenge(response) {
|
|
932
|
+
return parseAuthioBearerChallenge(response.headers.get("www-authenticate"))?.error === AuthioOAuthErrors.InsufficientUserAuthentication;
|
|
933
|
+
}
|
|
913
934
|
function tokenOf(subject) {
|
|
914
935
|
if (!subject) return void 0;
|
|
915
936
|
return typeof subject === "string" ? subject : subject.token;
|