@cauthyhq/node 1.0.0 → 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 — security release
4
+
5
+ Cauthy now marks the tokens a machine holds, and names the client a person's token was issued through. This release makes `verifyToken` use those marks. Refusing a machine's token where a person is expected is a fix, not a break, so it ships as a minor version: upgrade.
6
+
7
+ ### Security
8
+
9
+ - **A client's own token no longer passes as a person's.** A `client_credentials` token carries `token_use: "client"` and the client's public `client_id`. `verifyToken`, `createTokenVerifier`, `auth.verifyToken` and `auth.tokenVerifier` now refuse it with the reason `client_token` (rule R13 of the spec's `verification/rules.md`), by the claim's presence, whatever its value. In 1.0 such a token was accepted as a person whose `userId` was the client's id.
10
+ - **By contract, a person's token issued through another client is refused** with `wrong_client` (rule R15): `auth.verifyToken` and `auth.tokenVerifier` check the token's `azp` against the link's sign-in client. A token with no `azp` passes. A call that names another `clientId` in contract mode is a `CauthyConfigError`.
11
+ - `token_use` and `azp`, when present, must be strings (`invalid_claim` otherwise, rule R8).
12
+
13
+ ### Backends that serve machines
14
+
15
+ If an endpoint is called by machines with a `client_credentials` token, pass `allowClientTokens: true` to the verifier that guards it:
16
+
17
+ ```ts
18
+ const verifyMachine = auth.tokenVerifier({ allowClientTokens: true })
19
+ const who = await verifyMachine(request)
20
+ who.clientId // the client's public id; who.userId is its internal id
21
+ ```
22
+
23
+ Without it those calls get `client_token` (answer 401) after the upgrade.
24
+
25
+ ### Added
26
+
27
+ - `TokenIdentity.authorizedParty`: the token's `azp`, the client a person's token was issued through; absent when none was named.
28
+ - `TokenIdentity.clientId`: the token's `client_id` (a client's own token, with `allowClientTokens`; an AI agent's).
29
+ - `VerifyTokenOptions.allowClientTokens` (default `false`) and `VerifyTokenOptions.clientId` (default none; by contract, the link's sign-in client).
30
+ - `TokenRefusal` gains `client_token` and `wrong_client`.
31
+
32
+ ### Notes
33
+
34
+ - Cauthy now answers its JWKS with `max-age=300` and publishes a key at least that long before it signs with it. Nothing changes here: the keys are kept for the answer's `max-age` and fetched again on an unknown `kid`.
35
+ - A session that began through a client renews only through it. The SDK already renews with the `clientId` it signed in with, and sends that `clientId` on its own screens' calls: keep one `clientId` per deployment.
36
+
37
+ ### Development
38
+
39
+ - vitest 4.1.12, fastify 5.12.8 (tests), and postcss 8.5.32 and esbuild 0.28.2 by override: every open advisory was in development dependencies only. `npm audit` reports none.
40
+
41
+ ## 1.0.0
42
+
43
+ First release: the server half of `cauthy-auth` 0.42 (see the README, "Moving from cauthy-auth").
package/README.md CHANGED
@@ -91,6 +91,7 @@ import { auth } from './auth'
91
91
  // issuer the instance's own (CAUTHY_ISSUER, or the contract)
92
92
  // tenantId by contract, the link's tenant_id; otherwise the `tenantId` option or CAUTHY_TENANT_ID
93
93
  // audience the `audience` option or CAUTHY_AUDIENCE: your Cauthy's JWT_AUDIENCE (Cauthy has no default for it)
94
+ // clientId by contract, the link's sign-in client: a person's token issued through another is refused (wrong_client)
94
95
  const verify = auth.tokenVerifier() // throws CauthyConfigError now if one is missing
95
96
  const who = await verify(request) // or: await auth.verifyToken(request)
96
97
  ```
@@ -106,6 +107,8 @@ const verify = createTokenVerifier({ issuer: 'https://id.example.com', audience:
106
107
 
107
108
  - **The audience and the workspace are required.** Without them a token issued for another application or workspace would be accepted.
108
109
  - A system's own token (`sys` claim, whatever its value) and a guest's token (`anon: true`) are refused where a person is expected. Pass `allowSystem` or `allowAnonymous` for an endpoint that takes them.
110
+ - A client's own token (a `client_credentials` grant: `token_use: "client"`, whatever the value) is refused with `client_token` where a person is expected. A backend that serves machines too passes `allowClientTokens: true`; the identity then carries `clientId`, the client's public id (its `userId` is the client's internal id).
111
+ - `authorizedParty` is the token's `azp`: the client a person's token was issued through, absent when none was named. With `clientId` (by contract, always the link's sign-in client), a token whose `azp` names another client is refused with `wrong_client`; a token with no `azp` passes.
109
112
  - Clock tolerance is 30 seconds (`leeway`, 0 to 60). A token over 8192 bytes, with a repeated JSON name, a fractional date or a wrongly typed claim is refused.
110
113
  - Keys are fetched from `<issuer>/.well-known/jwks.json` with no redirect, within 5 seconds, at most 64 KiB, and kept for the answer's `max-age`.
111
114
 
@@ -171,6 +174,7 @@ It refuses to start when any `CAUTHY_*` variable of the standard mode is also se
171
174
  CI runs the auth-bff conformance suite at level `strict` against `examples/acme-crm` on an in-memory Cauthy, and the spec's token vectors against `verifyToken`:
172
175
 
173
176
  SPEC_DIR=../cauthy-sdk-spec LEVEL=strict npm run conformance
177
+ SPEC_DIR=../cauthy-sdk-spec LEVEL=switches npm run conformance # also the dashboard switches, on the in-memory Cauthy
174
178
  SPEC_DIR=../cauthy-sdk-spec npm run vectors
175
179
 
176
180
  ## Security
@@ -832,13 +832,20 @@ interface TokenIdentity {
832
832
  system?: string;
833
833
  /** Set when the token is a guest's (only with `allowAnonymous`). */
834
834
  anonymous?: true;
835
+ /** The client a person's token was issued through (`azp`); absent when none was named. */
836
+ authorizedParty?: string;
837
+ /**
838
+ * The token's `client_id`: on a client's own token (only with
839
+ * `allowClientTokens`), the client's public id; on an AI agent's, the agent's.
840
+ */
841
+ clientId?: string;
835
842
  /** UTC, ISO 8601. */
836
843
  expiresAt: string;
837
844
  /** The token as it arrived, to pass on. */
838
845
  token: string;
839
846
  }
840
847
  /** Why a token was refused: the closed list of verification/rules.md, and `no_token` when none was given. */
841
- type TokenRefusal = 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key' | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid' | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'anonymous_token';
848
+ type TokenRefusal = 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key' | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid' | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'client_token' | 'anonymous_token' | 'wrong_client';
842
849
  /**
843
850
  * Cauthy's keys could not be fetched (or are not a key set): the token may be
844
851
  * good, and the application cannot tell. Answer 503, not 401. It is not a
@@ -880,6 +887,20 @@ interface VerifyTokenOptions {
880
887
  allowSystem?: boolean;
881
888
  /** Accept a guest's token (`anon: true`). Default: false (rule R14). */
882
889
  allowAnonymous?: boolean;
890
+ /**
891
+ * Accept a client's own token (a `token_use` claim, from a
892
+ * `client_credentials` grant) too: for a backend that serves machines as
893
+ * well as people. Default: false, so a machine never passes as a person
894
+ * (rule R13). The identity then carries `clientId`.
895
+ */
896
+ allowClientTokens?: boolean;
897
+ /**
898
+ * The client this backend signs people in through. When set, a person's
899
+ * token issued through another client (an `azp` naming another) is refused
900
+ * with `wrong_client` (rule R15); a token with no `azp` passes. In contract
901
+ * mode it is the link's sign-in client. Default: none, and `azp` is not checked.
902
+ */
903
+ clientId?: string;
883
904
  }
884
905
  /**
885
906
  * Cauthy's published keys (K1 to K5 of verification/rules.md): fetched with a
@@ -990,6 +1011,8 @@ interface Cauthy {
990
1011
  * an API of this application: `verifyToken` with this instance's issuer, its
991
1012
  * workspace (by contract, the link's `tenant_id`; otherwise `tenantId` or
992
1013
  * `CAUTHY_TENANT_ID`) and its audience (`audience` or `CAUTHY_AUDIENCE`).
1014
+ * By contract it also refuses a person's token issued through another
1015
+ * client than the link's (`wrong_client`).
993
1016
  */
994
1017
  verifyToken(from: string | Request | Headers, options?: Partial<VerifyTokenOptions>): Promise<TokenIdentity>;
995
1018
  /** The same, built once: throws `CauthyConfigError` now if the audience or the workspace is missing. */
@@ -832,13 +832,20 @@ interface TokenIdentity {
832
832
  system?: string;
833
833
  /** Set when the token is a guest's (only with `allowAnonymous`). */
834
834
  anonymous?: true;
835
+ /** The client a person's token was issued through (`azp`); absent when none was named. */
836
+ authorizedParty?: string;
837
+ /**
838
+ * The token's `client_id`: on a client's own token (only with
839
+ * `allowClientTokens`), the client's public id; on an AI agent's, the agent's.
840
+ */
841
+ clientId?: string;
835
842
  /** UTC, ISO 8601. */
836
843
  expiresAt: string;
837
844
  /** The token as it arrived, to pass on. */
838
845
  token: string;
839
846
  }
840
847
  /** Why a token was refused: the closed list of verification/rules.md, and `no_token` when none was given. */
841
- type TokenRefusal = 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key' | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid' | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'anonymous_token';
848
+ type TokenRefusal = 'no_token' | 'too_large' | 'malformed' | 'alg_not_allowed' | 'unsupported_header' | 'missing_kid' | 'unknown_kid' | 'weak_key' | 'bad_signature' | 'missing_claim' | 'invalid_claim' | 'wrong_issuer' | 'wrong_audience' | 'expired' | 'not_yet_valid' | 'issued_in_future' | 'wrong_tenant' | 'system_token' | 'client_token' | 'anonymous_token' | 'wrong_client';
842
849
  /**
843
850
  * Cauthy's keys could not be fetched (or are not a key set): the token may be
844
851
  * good, and the application cannot tell. Answer 503, not 401. It is not a
@@ -880,6 +887,20 @@ interface VerifyTokenOptions {
880
887
  allowSystem?: boolean;
881
888
  /** Accept a guest's token (`anon: true`). Default: false (rule R14). */
882
889
  allowAnonymous?: boolean;
890
+ /**
891
+ * Accept a client's own token (a `token_use` claim, from a
892
+ * `client_credentials` grant) too: for a backend that serves machines as
893
+ * well as people. Default: false, so a machine never passes as a person
894
+ * (rule R13). The identity then carries `clientId`.
895
+ */
896
+ allowClientTokens?: boolean;
897
+ /**
898
+ * The client this backend signs people in through. When set, a person's
899
+ * token issued through another client (an `azp` naming another) is refused
900
+ * with `wrong_client` (rule R15); a token with no `azp` passes. In contract
901
+ * mode it is the link's sign-in client. Default: none, and `azp` is not checked.
902
+ */
903
+ clientId?: string;
883
904
  }
884
905
  /**
885
906
  * Cauthy's published keys (K1 to K5 of verification/rules.md): fetched with a
@@ -990,6 +1011,8 @@ interface Cauthy {
990
1011
  * an API of this application: `verifyToken` with this instance's issuer, its
991
1012
  * workspace (by contract, the link's `tenant_id`; otherwise `tenantId` or
992
1013
  * `CAUTHY_TENANT_ID`) and its audience (`audience` or `CAUTHY_AUDIENCE`).
1014
+ * By contract it also refuses a person's token issued through another
1015
+ * client than the link's (`wrong_client`).
993
1016
  */
994
1017
  verifyToken(from: string | Request | Headers, options?: Partial<VerifyTokenOptions>): Promise<TokenIdentity>;
995
1018
  /** The same, built once: throws `CauthyConfigError` now if the audience or the workspace is missing. */
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { S as Session, C as Cauthy } from './cauthy-ClP5orP4.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-Bn-i7cKD.mjs';
3
3
  import './keys-D5k0omTb.mjs';
4
4
  import 'jose';
5
5
  import './theme.mjs';
package/dist/express.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { S as Session, C as Cauthy } from './cauthy-B6Sm3T8M.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-Wy-YDGrM.js';
3
3
  import './keys-D5k0omTb.js';
4
4
  import 'jose';
5
5
  import './theme.js';
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { S as Session, C as Cauthy } from './cauthy-ClP5orP4.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-Bn-i7cKD.mjs';
3
3
  import './keys-D5k0omTb.mjs';
4
4
  import 'jose';
5
5
  import './theme.mjs';
package/dist/fastify.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FastifyReply, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { S as Session, C as Cauthy } from './cauthy-B6Sm3T8M.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-Wy-YDGrM.js';
3
3
  import './keys-D5k0omTb.js';
4
4
  import 'jose';
5
5
  import './theme.js';
package/dist/hono.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
- import { S as Session, C as Cauthy } from './cauthy-ClP5orP4.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-Bn-i7cKD.mjs';
3
3
  import './keys-D5k0omTb.mjs';
4
4
  import 'jose';
5
5
  import './theme.mjs';
package/dist/hono.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
- import { S as Session, C as Cauthy } from './cauthy-B6Sm3T8M.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-Wy-YDGrM.js';
3
3
  import './keys-D5k0omTb.js';
4
4
  import 'jose';
5
5
  import './theme.js';
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AttributeType, a as CatalogResource, C as Cauthy, b as CauthyApiError, c as CauthyConfigError, d as CauthyEvent, e as CauthyOptions, f as CauthyPact, g as CauthySystem, h as CauthyTokenError, i as CauthyUnavailableError, j as CauthyWebhookError, k as CheckOf, l as CheckQuestion, m as CheckResult, n as CheckRule, o as ConditionalPermission, p as ContractOptions, J as JsonValue, M as MountTarget, N as NewMember, O as Organization, q as OrganizationMember, r as OrganizationRef, P as PactItem, s as PactStatus, t as PermissionOf, u as Permitted, v as Policy, w as PolicyDefinition, x as PolicyPermission, y as PolicyRole, z as PublicSession, B as PushPolicyOptions, R as ReplayStore, D as Resolved, E as ResolvedContract, F as RoleOf, G as STANDARD_VARIABLES, S as Session, H as SessionOrganization, I as SessionStatus, K as SessionUser, L as SystemKey, Q as SystemOptions, T as SystemPolicy, U as TokenIdentity, V as TokenRefusal, W as VerifyTokenOptions, X as VerifyWebhookOptions, Y as can, Z as createCauthy, _ as createSystem, $ as createTokenVerifier, a0 as definePolicy, a1 as failClosed, a2 as memoryReplayStore, a3 as policySource, a4 as pullCatalog, a5 as pullPolicy, a6 as pushCatalog, a7 as pushPolicy, a8 as readContract, a9 as remoteKeys, aa as verifyToken, ab as verifyWebhook } from './cauthy-ClP5orP4.mjs';
1
+ export { A as AttributeType, a as CatalogResource, C as Cauthy, b as CauthyApiError, c as CauthyConfigError, d as CauthyEvent, e as CauthyOptions, f as CauthyPact, g as CauthySystem, h as CauthyTokenError, i as CauthyUnavailableError, j as CauthyWebhookError, k as CheckOf, l as CheckQuestion, m as CheckResult, n as CheckRule, o as ConditionalPermission, p as ContractOptions, J as JsonValue, M as MountTarget, N as NewMember, O as Organization, q as OrganizationMember, r as OrganizationRef, P as PactItem, s as PactStatus, t as PermissionOf, u as Permitted, v as Policy, w as PolicyDefinition, x as PolicyPermission, y as PolicyRole, z as PublicSession, B as PushPolicyOptions, R as ReplayStore, D as Resolved, E as ResolvedContract, F as RoleOf, G as STANDARD_VARIABLES, S as Session, H as SessionOrganization, I as SessionStatus, K as SessionUser, L as SystemKey, Q as SystemOptions, T as SystemPolicy, U as TokenIdentity, V as TokenRefusal, W as VerifyTokenOptions, X as VerifyWebhookOptions, Y as can, Z as createCauthy, _ as createSystem, $ as createTokenVerifier, a0 as definePolicy, a1 as failClosed, a2 as memoryReplayStore, a3 as policySource, a4 as pullCatalog, a5 as pullPolicy, a6 as pushCatalog, a7 as pushPolicy, a8 as readContract, a9 as remoteKeys, aa as verifyToken, ab as verifyWebhook } from './cauthy-Bn-i7cKD.mjs';
2
2
  export { K as KeyStore, P as PactKeys, S as StoredKey } from './keys-D5k0omTb.mjs';
3
3
  export { CauthyTheme, ImportedTheme, PushThemeOptions, StackmythThemeOptions, StackmythTokens, ThemeFont, ThemeTexts, pushTheme, themeFromStackmyth, tokensFromStackmyth } from './theme.mjs';
4
4
  import 'jose';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AttributeType, a as CatalogResource, C as Cauthy, b as CauthyApiError, c as CauthyConfigError, d as CauthyEvent, e as CauthyOptions, f as CauthyPact, g as CauthySystem, h as CauthyTokenError, i as CauthyUnavailableError, j as CauthyWebhookError, k as CheckOf, l as CheckQuestion, m as CheckResult, n as CheckRule, o as ConditionalPermission, p as ContractOptions, J as JsonValue, M as MountTarget, N as NewMember, O as Organization, q as OrganizationMember, r as OrganizationRef, P as PactItem, s as PactStatus, t as PermissionOf, u as Permitted, v as Policy, w as PolicyDefinition, x as PolicyPermission, y as PolicyRole, z as PublicSession, B as PushPolicyOptions, R as ReplayStore, D as Resolved, E as ResolvedContract, F as RoleOf, G as STANDARD_VARIABLES, S as Session, H as SessionOrganization, I as SessionStatus, K as SessionUser, L as SystemKey, Q as SystemOptions, T as SystemPolicy, U as TokenIdentity, V as TokenRefusal, W as VerifyTokenOptions, X as VerifyWebhookOptions, Y as can, Z as createCauthy, _ as createSystem, $ as createTokenVerifier, a0 as definePolicy, a1 as failClosed, a2 as memoryReplayStore, a3 as policySource, a4 as pullCatalog, a5 as pullPolicy, a6 as pushCatalog, a7 as pushPolicy, a8 as readContract, a9 as remoteKeys, aa as verifyToken, ab as verifyWebhook } from './cauthy-B6Sm3T8M.js';
1
+ export { A as AttributeType, a as CatalogResource, C as Cauthy, b as CauthyApiError, c as CauthyConfigError, d as CauthyEvent, e as CauthyOptions, f as CauthyPact, g as CauthySystem, h as CauthyTokenError, i as CauthyUnavailableError, j as CauthyWebhookError, k as CheckOf, l as CheckQuestion, m as CheckResult, n as CheckRule, o as ConditionalPermission, p as ContractOptions, J as JsonValue, M as MountTarget, N as NewMember, O as Organization, q as OrganizationMember, r as OrganizationRef, P as PactItem, s as PactStatus, t as PermissionOf, u as Permitted, v as Policy, w as PolicyDefinition, x as PolicyPermission, y as PolicyRole, z as PublicSession, B as PushPolicyOptions, R as ReplayStore, D as Resolved, E as ResolvedContract, F as RoleOf, G as STANDARD_VARIABLES, S as Session, H as SessionOrganization, I as SessionStatus, K as SessionUser, L as SystemKey, Q as SystemOptions, T as SystemPolicy, U as TokenIdentity, V as TokenRefusal, W as VerifyTokenOptions, X as VerifyWebhookOptions, Y as can, Z as createCauthy, _ as createSystem, $ as createTokenVerifier, a0 as definePolicy, a1 as failClosed, a2 as memoryReplayStore, a3 as policySource, a4 as pullCatalog, a5 as pullPolicy, a6 as pushCatalog, a7 as pushPolicy, a8 as readContract, a9 as remoteKeys, aa as verifyToken, ab as verifyWebhook } from './cauthy-Wy-YDGrM.js';
2
2
  export { K as KeyStore, P as PactKeys, S as StoredKey } from './keys-D5k0omTb.js';
3
3
  export { CauthyTheme, ImportedTheme, PushThemeOptions, StackmythThemeOptions, StackmythTokens, ThemeFont, ThemeTexts, pushTheme, themeFromStackmyth, tokensFromStackmyth } from './theme.js';
4
4
  import 'jose';
package/dist/index.js CHANGED
@@ -1808,6 +1808,7 @@ function createTokenVerifier(options = {}) {
1808
1808
  if (claims.roles !== void 0 && claims.roles !== null && !(Array.isArray(claims.roles) && claims.roles.every((r) => typeof r === "string"))) bad("roles");
1809
1809
  if (claims.email !== void 0 && typeof claims.email !== "string") bad("email");
1810
1810
  for (const name of ["email_verified", "anon"]) if (claims[name] !== void 0 && typeof claims[name] !== "boolean") bad(name);
1811
+ for (const name of ["token_use", "azp"]) if (claims[name] !== void 0 && typeof claims[name] !== "string") bad(name);
1811
1812
  if (!issuers.includes(claims.iss)) throw new CauthyTokenError("wrong_issuer");
1812
1813
  if (!(Array.isArray(aud) ? aud.includes(audience) : aud === audience)) throw new CauthyTokenError("wrong_audience");
1813
1814
  const now = (options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3;
@@ -1816,7 +1817,9 @@ function createTokenVerifier(options = {}) {
1816
1817
  if (now < claims.iat - leeway) throw new CauthyTokenError("issued_in_future");
1817
1818
  if (claims.tid !== tenant) throw new CauthyTokenError("wrong_tenant");
1818
1819
  if ("sys" in claims && !options.allowSystem) throw new CauthyTokenError("system_token");
1820
+ if ("token_use" in claims && !options.allowClientTokens) throw new CauthyTokenError("client_token");
1819
1821
  if (claims.anon === true && !options.allowAnonymous) throw new CauthyTokenError("anonymous_token");
1822
+ if (options.clientId && typeof claims.azp === "string" && claims.azp !== "" && claims.azp !== options.clientId) throw new CauthyTokenError("wrong_client");
1820
1823
  return {
1821
1824
  userId: claims.sub,
1822
1825
  tenantId: claims.tid,
@@ -1825,6 +1828,8 @@ function createTokenVerifier(options = {}) {
1825
1828
  roles: Array.isArray(claims.roles) ? claims.roles : [],
1826
1829
  ...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
1827
1830
  ...claims.anon === true ? { anonymous: true } : {},
1831
+ ...typeof claims.azp === "string" && claims.azp !== "" ? { authorizedParty: claims.azp } : {},
1832
+ ...typeof claims.client_id === "string" && claims.client_id !== "" ? { clientId: claims.client_id } : {},
1828
1833
  expiresAt: new Date(claims.exp * 1e3).toISOString(),
1829
1834
  token
1830
1835
  };
@@ -3324,13 +3329,19 @@ function createCauthy(given = {}) {
3324
3329
  return { response: redirect(to) };
3325
3330
  }
3326
3331
  const replay = given.webhookReplay ?? memoryReplayStore();
3327
- const tokenVerifier = (o = {}) => createTokenVerifier({
3328
- issuer,
3329
- tenantId: contract?.tenantId ?? given.tenantId,
3330
- audience: given.audience,
3331
- fetch: given.fetch,
3332
- ...o
3333
- });
3332
+ const tokenVerifier = (o = {}) => {
3333
+ if (contract && o.clientId !== void 0 && o.clientId !== contract.clientId) {
3334
+ throw new CauthyConfigError(`clientId is the contract's (${contract.clientId}): remove it from the call`);
3335
+ }
3336
+ return createTokenVerifier({
3337
+ issuer,
3338
+ tenantId: contract?.tenantId ?? given.tenantId,
3339
+ audience: given.audience,
3340
+ fetch: given.fetch,
3341
+ ...contract ? { clientId: contract.clientId } : {},
3342
+ ...o
3343
+ });
3344
+ };
3334
3345
  const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now, replay });
3335
3346
  const pact = contract && ownKeys ? {
3336
3347
  contract,