@hyper.authio/server 0.8.0 → 0.8.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/dist/index.d.cts CHANGED
@@ -301,13 +301,26 @@ declare class AuthioBearerError extends AuthioError {
301
301
  get wwwAuthenticate(): string;
302
302
  }
303
303
  /**
304
- * The caller is known but not allowed: answer `403`, with `wwwAuthenticate` when the refusal has a bearer error.
304
+ * A rule refused a caller whose token is valid. A refusal asking for a stronger or more recent sign-in
305
+ * (`insufficient_user_authentication`) answers `401`, as RFC 9470 §3 has it, naming the protected resource metadata
306
+ * (`resource_metadata`, RFC 9728 §5.1) when the API publishes it; every other refusal answers `403`, with
307
+ * `wwwAuthenticate` when it is about scopes (`insufficient_scope`, RFC 6750 §3.1).
305
308
  */
306
309
  declare class AuthioForbiddenError extends AuthioError {
307
- readonly status = 403;
310
+ /** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
311
+ readonly status: 401 | 403;
308
312
  readonly failure: AuthioRuleFailure;
309
- constructor(failure: AuthioRuleFailure);
310
- /** The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. */
313
+ /** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
314
+ readonly resourceMetadata: string | undefined;
315
+ constructor(failure: AuthioRuleFailure, options?: {
316
+ resourceMetadata?: string;
317
+ });
318
+ /** The same refusal, pointing at the API's protected resource metadata. */
319
+ withResourceMetadata(resourceMetadata: string | undefined): AuthioForbiddenError;
320
+ /**
321
+ * The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
322
+ * carries `resource_metadata` when `resourceMetadata` is set.
323
+ */
311
324
  get wwwAuthenticate(): string | undefined;
312
325
  }
313
326
  /**
@@ -316,7 +329,8 @@ declare class AuthioForbiddenError extends AuthioError {
316
329
  */
317
330
  declare function toForbiddenChallenge(failure: AuthioRuleFailure): string | undefined;
318
331
  /**
319
- * Problem details (RFC 9457) for a `401` or `403`.
332
+ * Problem details (RFC 9457) for a `401` or `403`, with the refusal's `status`: a step-up refusal is `401`
333
+ * Unauthorized and carries `error`, `acr_values` and `max_age`.
320
334
  */
321
335
  declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenError): Record<string, unknown>;
322
336
 
package/dist/index.d.ts CHANGED
@@ -301,13 +301,26 @@ declare class AuthioBearerError extends AuthioError {
301
301
  get wwwAuthenticate(): string;
302
302
  }
303
303
  /**
304
- * The caller is known but not allowed: answer `403`, with `wwwAuthenticate` when the refusal has a bearer error.
304
+ * A rule refused a caller whose token is valid. A refusal asking for a stronger or more recent sign-in
305
+ * (`insufficient_user_authentication`) answers `401`, as RFC 9470 §3 has it, naming the protected resource metadata
306
+ * (`resource_metadata`, RFC 9728 §5.1) when the API publishes it; every other refusal answers `403`, with
307
+ * `wwwAuthenticate` when it is about scopes (`insufficient_scope`, RFC 6750 §3.1).
305
308
  */
306
309
  declare class AuthioForbiddenError extends AuthioError {
307
- readonly status = 403;
310
+ /** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
311
+ readonly status: 401 | 403;
308
312
  readonly failure: AuthioRuleFailure;
309
- constructor(failure: AuthioRuleFailure);
310
- /** The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. */
313
+ /** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
314
+ readonly resourceMetadata: string | undefined;
315
+ constructor(failure: AuthioRuleFailure, options?: {
316
+ resourceMetadata?: string;
317
+ });
318
+ /** The same refusal, pointing at the API's protected resource metadata. */
319
+ withResourceMetadata(resourceMetadata: string | undefined): AuthioForbiddenError;
320
+ /**
321
+ * The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
322
+ * carries `resource_metadata` when `resourceMetadata` is set.
323
+ */
311
324
  get wwwAuthenticate(): string | undefined;
312
325
  }
313
326
  /**
@@ -316,7 +329,8 @@ declare class AuthioForbiddenError extends AuthioError {
316
329
  */
317
330
  declare function toForbiddenChallenge(failure: AuthioRuleFailure): string | undefined;
318
331
  /**
319
- * Problem details (RFC 9457) for a `401` or `403`.
332
+ * Problem details (RFC 9457) for a `401` or `403`, with the refusal's `status`: a step-up refusal is `401`
333
+ * Unauthorized and carries `error`, `acr_values` and `max_age`.
320
334
  */
321
335
  declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenError): Record<string, unknown>;
322
336
 
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, AuthioTokenTypes } from '@hyper.authio/client';
2
2
  import { decodeProtectedHeader } from 'jose';
3
3
 
4
4
  // src/user.ts
@@ -469,16 +469,30 @@ var AuthioBearerError = class _AuthioBearerError extends AuthioError {
469
469
  return parameters.length === 0 ? "Bearer" : `Bearer ${parameters.join(", ")}`;
470
470
  }
471
471
  };
472
- var AuthioForbiddenError = class extends AuthioError {
473
- status = 403;
472
+ var AuthioForbiddenError = class _AuthioForbiddenError extends AuthioError {
473
+ /** `401` for a step-up (`insufficient_user_authentication`), `403` otherwise. */
474
+ status;
474
475
  failure;
475
- constructor(failure) {
476
+ /** The API's protected resource metadata URL (RFC 9728), sent as `resource_metadata` with a step-up `401`. */
477
+ resourceMetadata;
478
+ constructor(failure, options = {}) {
476
479
  super(failure.message);
477
480
  this.failure = failure;
481
+ this.status = failure.error === "insufficient_user_authentication" ? 401 : 403;
482
+ this.resourceMetadata = options.resourceMetadata;
478
483
  }
479
- /** The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. */
484
+ /** The same refusal, pointing at the API's protected resource metadata. */
485
+ withResourceMetadata(resourceMetadata) {
486
+ if (!resourceMetadata || resourceMetadata === this.resourceMetadata) return this;
487
+ return new _AuthioForbiddenError(this.failure, { resourceMetadata });
488
+ }
489
+ /**
490
+ * The `WWW-Authenticate` value, or `undefined` when the refusal is not about scopes or the sign-in. A step-up `401`
491
+ * carries `resource_metadata` when `resourceMetadata` is set.
492
+ */
480
493
  get wwwAuthenticate() {
481
- return toForbiddenChallenge(this.failure);
494
+ const challenge = toForbiddenChallenge(this.failure);
495
+ return challenge && this.status === 401 && this.resourceMetadata ? `${challenge}, resource_metadata="${escapeQuoted(this.resourceMetadata)}"` : challenge;
482
496
  }
483
497
  };
484
498
  function toForbiddenChallenge(failure) {
@@ -492,15 +506,15 @@ function toForbiddenChallenge(failure) {
492
506
  function toProblemDetails(error) {
493
507
  if (error instanceof AuthioBearerError)
494
508
  return {
495
- type: "https://tools.ietf.org/html/rfc9110#section-15.5.2",
509
+ type: unauthorizedType,
496
510
  title: "Unauthorized",
497
511
  status: 401,
498
512
  detail: "A valid Authio access token is required."
499
513
  };
500
514
  return {
501
- type: "https://tools.ietf.org/html/rfc9110#section-15.5.4",
502
- title: "Forbidden",
503
- status: 403,
515
+ type: error.status === 401 ? unauthorizedType : "https://tools.ietf.org/html/rfc9110#section-15.5.4",
516
+ title: error.status === 401 ? "Unauthorized" : "Forbidden",
517
+ status: error.status,
504
518
  detail: error.failure.message || "The caller is not allowed to access this resource.",
505
519
  ...error.failure.error ? { error: error.failure.error } : {},
506
520
  ...error.failure.scope ? { scope: error.failure.scope } : {},
@@ -508,6 +522,7 @@ function toProblemDetails(error) {
508
522
  ...error.failure.maxAge !== void 0 ? { max_age: error.failure.maxAge } : {}
509
523
  };
510
524
  }
525
+ var unauthorizedType = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
511
526
  function toHeaderText(text) {
512
527
  return [...text].map((character) => character >= " " && character <= "~" && character !== '"' && character !== "\\" ? character : "?").join("");
513
528
  }
@@ -903,13 +918,16 @@ function createOnBehalfOf(options) {
903
918
  };
904
919
  const token = await getToken(subject, request.signal);
905
920
  const response = await attempt(token);
906
- if (response.status !== 401) return response;
921
+ if (response.status !== 401 || isStepUpChallenge(response)) return response;
907
922
  await invalidate(subject, token.accessToken);
908
923
  return attempt(await getToken(subject, request.signal));
909
924
  };
910
925
  }
911
926
  };
912
927
  }
928
+ function isStepUpChallenge(response) {
929
+ return parseAuthioBearerChallenge(response.headers.get("www-authenticate"))?.error === AuthioOAuthErrors.InsufficientUserAuthentication;
930
+ }
913
931
  function tokenOf(subject) {
914
932
  if (!subject) return void 0;
915
933
  return typeof subject === "string" ? subject : subject.token;