@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/dist/index.d.cts CHANGED
@@ -34,8 +34,9 @@ interface AuthioUserClaimSettings {
34
34
  groupClaims?: readonly string[];
35
35
  }
36
36
  /**
37
- * The caller of an API, read from a validated access token or an introspection answer: identity, every form of
38
- * permission Authio issues, and the raw claims for anything else.
37
+ * The caller of an API, read from a validated access token, an introspection answer, or both (the token's claims and
38
+ * those of the answer the token does not carry, as `activeCheck` reads it): identity, every form of permission Authio
39
+ * issues, and the raw claims for anything else.
39
40
  */
40
41
  declare class AuthioUser {
41
42
  /** `sub`: the person's id, or the credential's for a machine caller. */
@@ -75,7 +76,7 @@ declare class AuthioUser {
75
76
  readonly actorChain: readonly string[];
76
77
  /** `auth_time`, in seconds since the epoch. */
77
78
  readonly authTime: number | undefined;
78
- /** The client the token was issued to (`azp`, else `client_id`). */
79
+ /** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
79
80
  readonly clientId: string | undefined;
80
81
  /** `sid`. */
81
82
  readonly sessionId: string | undefined;
@@ -121,7 +122,10 @@ declare class AuthioUser {
121
122
  readonly credentialType: string | undefined;
122
123
  /** `credential_id`. */
123
124
  readonly credentialId: string | undefined;
124
- /** Every claim, as the token or the introspection answer carried it. */
125
+ /**
126
+ * Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
127
+ * answer's the token does not carry).
128
+ */
125
129
  readonly claims: Readonly<Record<string, unknown>>;
126
130
  /** The access token the user was read from. Not enumerable, so it stays out of logs and JSON. */
127
131
  readonly token: string | undefined;
@@ -203,9 +207,11 @@ interface AuthioRule {
203
207
  check(user: AuthioUser): AuthioRuleFailure | undefined;
204
208
  }
205
209
  /**
206
- * Checks every rule. On refusal the message joins every rule's, and the error is the one asking for a stronger
207
- * sign-in (with the strongest `acr_values` and the shortest `max_age` the failing rules ask for), else the first one
208
- * that has an error.
210
+ * Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
211
+ * permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
212
+ * refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
213
+ * Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
214
+ * with the strongest `acr_values` and the shortest `max_age` they ask for.
209
215
  */
210
216
  declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
211
217
  /**
@@ -264,7 +270,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
264
270
  */
265
271
  declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
266
272
  /**
267
- * Requires the token to have been issued to one of the clients (`azp`, else `client_id`).
273
+ * Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
268
274
  */
269
275
  declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
270
276
  /**
@@ -337,10 +343,15 @@ declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenErro
337
343
  /**
338
344
  * Whether and how Authio's introspection endpoint takes part in validating a token.
339
345
  *
340
- * - `none`: the JWT is validated locally (signature, issuer, audience, lifetime). Fastest; a revoked token stays
341
- * valid until it expires.
342
- * - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`).
343
- * - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key.
346
+ * - `none`: the JWT is validated locally (signature, issuer, audience, lifetime) and the caller is read from it alone.
347
+ * Fastest; a revoked token stays valid until it expires. A lightweight access token carries no roles, permissions,
348
+ * plan, profile or email, so it needs one of the modes below.
349
+ * - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`). The caller is read
350
+ * from the token, and the introspection answer adds the claims the token does not carry: the answer has every claim
351
+ * of the complete token, so a lightweight access token's roles and permissions are read too. A claim the token
352
+ * carries keeps the token's value, which its signature vouches for (the answer names a single `aud`).
353
+ * - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key. The
354
+ * caller is read from the answer.
344
355
  */
345
356
  type AuthioIntrospectionMode = "none" | "activeCheck" | "introspectionOnly";
346
357
  /**
@@ -381,7 +392,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
381
392
  * realm's keys are accepted, and never HS*.
382
393
  */
383
394
  accessTokenSignedResponseAlg?: string | readonly string[];
384
- /** Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. */
395
+ /**
396
+ * Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
397
+ * whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
398
+ */
385
399
  rejectNonAccessTokens?: boolean;
386
400
  /**
387
401
  * 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
@@ -34,8 +34,9 @@ interface AuthioUserClaimSettings {
34
34
  groupClaims?: readonly string[];
35
35
  }
36
36
  /**
37
- * The caller of an API, read from a validated access token or an introspection answer: identity, every form of
38
- * permission Authio issues, and the raw claims for anything else.
37
+ * The caller of an API, read from a validated access token, an introspection answer, or both (the token's claims and
38
+ * those of the answer the token does not carry, as `activeCheck` reads it): identity, every form of permission Authio
39
+ * issues, and the raw claims for anything else.
39
40
  */
40
41
  declare class AuthioUser {
41
42
  /** `sub`: the person's id, or the credential's for a machine caller. */
@@ -75,7 +76,7 @@ declare class AuthioUser {
75
76
  readonly actorChain: readonly string[];
76
77
  /** `auth_time`, in seconds since the epoch. */
77
78
  readonly authTime: number | undefined;
78
- /** The client the token was issued to (`azp`, else `client_id`). */
79
+ /** The client the token was issued to (`client_id`, RFC 9068 §2.2, else `azp`). */
79
80
  readonly clientId: string | undefined;
80
81
  /** `sid`. */
81
82
  readonly sessionId: string | undefined;
@@ -121,7 +122,10 @@ declare class AuthioUser {
121
122
  readonly credentialType: string | undefined;
122
123
  /** `credential_id`. */
123
124
  readonly credentialId: string | undefined;
124
- /** Every claim, as the token or the introspection answer carried it. */
125
+ /**
126
+ * Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
127
+ * answer's the token does not carry).
128
+ */
125
129
  readonly claims: Readonly<Record<string, unknown>>;
126
130
  /** The access token the user was read from. Not enumerable, so it stays out of logs and JSON. */
127
131
  readonly token: string | undefined;
@@ -203,9 +207,11 @@ interface AuthioRule {
203
207
  check(user: AuthioUser): AuthioRuleFailure | undefined;
204
208
  }
205
209
  /**
206
- * Checks every rule. On refusal the message joins every rule's, and the error is the one asking for a stronger
207
- * sign-in (with the strongest `acr_values` and the shortest `max_age` the failing rules ask for), else the first one
208
- * that has an error.
210
+ * Checks every rule. On refusal the message joins every rule's. When a rule other than a step-up fails (a scope, role,
211
+ * permission, group, claim, organization, client or caller kind), no stronger sign-in can satisfy the request: the
212
+ * refusal is a `403`, with the first such rule's error (`insufficient_scope`) or none, and never asks for a step-up.
213
+ * Only when every failing rule asks for a stronger or more recent sign-in is the error `insufficient_user_authentication`,
214
+ * with the strongest `acr_values` and the shortest `max_age` they ask for.
209
215
  */
210
216
  declare function authorize(user: AuthioUser, ...rules: AuthioRule[]): AuthioRuleFailure | undefined;
211
217
  /**
@@ -264,7 +270,7 @@ declare function requireRecentAuthentication(maxAge: number, options?: {
264
270
  */
265
271
  declare function requireCallerKind(kinds: AuthioCallerKind | readonly AuthioCallerKind[]): AuthioRule;
266
272
  /**
267
- * Requires the token to have been issued to one of the clients (`azp`, else `client_id`).
273
+ * Requires the token to have been issued to one of the clients (`client_id`, else `azp`).
268
274
  */
269
275
  declare function requireClient(clientIds: string | readonly string[]): AuthioRule;
270
276
  /**
@@ -337,10 +343,15 @@ declare function toProblemDetails(error: AuthioBearerError | AuthioForbiddenErro
337
343
  /**
338
344
  * Whether and how Authio's introspection endpoint takes part in validating a token.
339
345
  *
340
- * - `none`: the JWT is validated locally (signature, issuer, audience, lifetime). Fastest; a revoked token stays
341
- * valid until it expires.
342
- * - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`).
343
- * - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key.
346
+ * - `none`: the JWT is validated locally (signature, issuer, audience, lifetime) and the caller is read from it alone.
347
+ * Fastest; a revoked token stays valid until it expires. A lightweight access token carries no roles, permissions,
348
+ * plan, profile or email, so it needs one of the modes below.
349
+ * - `activeCheck`: validated locally, then confirmed active by introspection (cached per `jti`). The caller is read
350
+ * from the token, and the introspection answer adds the claims the token does not carry: the answer has every claim
351
+ * of the complete token, so a lightweight access token's roles and permissions are read too. A claim the token
352
+ * carries keeps the token's value, which its signature vouches for (the answer names a single `aud`).
353
+ * - `introspectionOnly`: introspection alone, for opaque (reference) tokens, or encrypted ones without the key. The
354
+ * caller is read from the answer.
344
355
  */
345
356
  type AuthioIntrospectionMode = "none" | "activeCheck" | "introspectionOnly";
346
357
  /**
@@ -381,7 +392,10 @@ interface AuthioVerifierOptions extends AuthioConnectionOptions, AuthioUserClaim
381
392
  * realm's keys are accepted, and never HS*.
382
393
  */
383
394
  accessTokenSignedResponseAlg?: string | readonly string[];
384
- /** Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. */
395
+ /**
396
+ * Refuses a token whose `typ` claim is present and not `Bearer` (an ID token, for one). Defaults to `true`. A JWT
397
+ * whose header `typ` is not `at+jwt` (RFC 9068 §4) is refused whatever this says.
398
+ */
385
399
  rejectNonAccessTokens?: boolean;
386
400
  /**
387
401
  * 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, parseAuthioBearerChallenge, AuthioOAuthErrors, 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 (`azp`, else `client_id`). */
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;
@@ -87,7 +87,10 @@ var AuthioUser = class _AuthioUser {
87
87
  credentialType;
88
88
  /** `credential_id`. */
89
89
  credentialId;
90
- /** Every claim, as the token or the introspection answer carried it. */
90
+ /**
91
+ * Every claim, as the token or the introspection answer carried it (with `activeCheck`, the token's, plus the
92
+ * answer's the token does not carry).
93
+ */
91
94
  claims;
92
95
  flatRoles;
93
96
  /**
@@ -102,7 +105,7 @@ var AuthioUser = class _AuthioUser {
102
105
  const text = (name) => typeof claims[name] === "string" && claims[name] ? claims[name] : void 0;
103
106
  this.claims = Object.freeze({ ...claims });
104
107
  this.subject = text(AuthioClaimTypes.Subject) ?? "";
105
- this.clientId = text(AuthioClaimTypes.AuthorizedParty) ?? text(AuthioClaimTypes.ClientId);
108
+ this.clientId = text(AuthioClaimTypes.ClientId) ?? text(AuthioClaimTypes.AuthorizedParty);
106
109
  this.sessionId = text(AuthioClaimTypes.SessionId);
107
110
  this.tokenId = text(AuthioClaimTypes.JwtId);
108
111
  this.audiences = Object.freeze(distinct(valuesOf(claims[AuthioClaimTypes.Audience])));
@@ -275,8 +278,9 @@ function distinctIgnoringCase(values) {
275
278
  function authorize(user, ...rules) {
276
279
  const failures = rules.map((rule) => rule.check(user)).filter((failure) => failure !== void 0);
277
280
  if (failures.length === 0) return void 0;
278
- const stepUps = failures.filter((failure) => failure.error === "insufficient_user_authentication");
279
- const bearer = stepUps[0] ?? failures.find((failure) => failure.error);
281
+ const others = failures.filter((failure) => failure.error !== "insufficient_user_authentication");
282
+ const stepUps = others.length === 0 ? failures : [];
283
+ const bearer = stepUps[0] ?? others.find((failure) => failure.error);
280
284
  const acrValues = strongestAcr(stepUps.map((failure) => failure.acrValues));
281
285
  const maxAges = stepUps.map((failure) => failure.maxAge).filter((value) => value !== void 0);
282
286
  return {
@@ -636,13 +640,15 @@ function createAuthioVerifier(options) {
636
640
  }
637
641
  let payload;
638
642
  try {
639
- ({ payload } = await verifyAuthioJwt(signed, keySet, {
643
+ let header;
644
+ ({ payload, header } = await verifyAuthioJwt(signed, keySet, {
640
645
  issuer: connection.issuer,
641
646
  audiences,
642
647
  algorithms,
643
648
  clockSkew,
644
649
  now: connection.clock.now()
645
650
  }));
651
+ requireAccessTokenType(header);
646
652
  } catch (error) {
647
653
  if (error instanceof AuthioTokenValidationError && error.reason === "invalid_algorithm" && symmetric.length === 0 && isSymmetricToken(signed))
648
654
  throw new AuthioBearerError(AuthioBearerErrorDescriptions.Symmetric, { cause: error, reason: error.reason });
@@ -652,8 +658,9 @@ function createAuthioVerifier(options) {
652
658
  const type = typeof payload.typ === "string" ? payload.typ : void 0;
653
659
  if (rejectNonAccessTokens && type !== void 0 && type.toLowerCase() !== "bearer")
654
660
  throw new AuthioBearerError(`Only access tokens are accepted; this token's typ is '${type}'.`, { reason: "not_access_token" });
655
- if (introspection === "activeCheck") await confirmActive(token, payload, signal);
656
- return new AuthioUser(payload, claimSettings, token);
661
+ if (introspection !== "activeCheck") return new AuthioUser(payload, claimSettings, token);
662
+ const answer = await confirmActive(token, payload, signal);
663
+ return new AuthioUser({ ...claimsOfIntrospection(answer), ...payload }, claimSettings, token);
657
664
  };
658
665
  const confirmActive = async (token, payload, signal) => {
659
666
  const key = payload.jti ? `jti:${payload.jti}` : await keyForToken(token);
@@ -665,6 +672,7 @@ function createAuthioVerifier(options) {
665
672
  throw new AuthioBearerError(AuthioBearerErrorDescriptions.Unconfirmed, { cause: error, reason: "unconfirmed" });
666
673
  }
667
674
  if (!result) throw new AuthioBearerError(AuthioBearerErrorDescriptions.Inactive, { reason: "inactive" });
675
+ return result;
668
676
  };
669
677
  const verifyByIntrospection = async (token, signal) => {
670
678
  let result;