From c01ebc931c9e3745b4a505b9be22aed97222870a Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 23 Jul 2026 10:36:09 +0530 Subject: [PATCH 1/5] feat: feat: add token_vault_privileged_access support for clients --- docs/resource-specific-documentation.md | 31 ++++++++++++ examples/yaml/tenant.yaml | 16 +++++++ src/tools/auth0/handlers/clients.ts | 40 ++++++++++++++++ test/tools/auth0/handlers/clients.tests.js | 56 ++++++++++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 7d2353647..d5a9bbfb5 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -1698,3 +1698,34 @@ Contents of `My API Client.json` (deploy-time, with pem): ``` > **Note:** The `pem` field must be supplied manually from your key generation step. Never commit private keys — only the public key PEM goes in the config. + +## Token Vault Privileged Access + +> **Early Access:** Enforcement of `ip_allowlist` and `grants` requires the `token_vault_subject_type_jwt_ea_rollout` feature flag to be enabled on the tenant. When the flag is off the fields are accepted but not stored or returned. + +The Deploy CLI supports the `token_vault_privileged_access` property on clients, which hardens a privileged Token Vault worker by restricting the caller IPs, connections, and scopes it may use at runtime. + +| Field | Type | Description | +| -------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `ip_allowlist` | array of strings | IPv4/IPv6 addresses or CIDR ranges permitted to call token exchange on behalf of this client. | +| `grants` | array of objects | Connection/scope pin objects. Each has a `connection` (name) and `scopes` (array). Max 5 connections; max 20 scopes total. | + +```yaml +clients: + - name: My Token Vault Privileged App + app_type: non_interactive + token_vault_privileged_access: + ip_allowlist: + - "192.168.1.0/24" + - "10.0.0.1" + grants: + - connection: google-oauth2 + scopes: + - "https://www.googleapis.com/auth/calendar.readonly" + - connection: slack + scopes: + - "chat:write" + - "channels:read" +``` + +> **Note:** The `credentials` sub-object of `token_vault_privileged_access` is **not** managed by the Deploy CLI. On read, Auth0 returns it as credential references (`id`) rather than names or key material, and the Deploy CLI never syncs IDs. Manage the privileged client's credentials directly on the tenant. diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index be38e2c6e..dad95e677 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -78,6 +78,22 @@ clients: allow_any_profile_of_type: - "custom_authentication" - "on_behalf_of_token_exchange" + - + name: "My Token Vault Privileged App" + app_type: "non_interactive" + # Early Access: requires the token_vault_subject_type_jwt_ea_rollout feature flag. + token_vault_privileged_access: + ip_allowlist: + - "192.168.1.0/24" + - "10.0.0.1" + grants: + - connection: "google-oauth2" + scopes: + - "https://www.googleapis.com/auth/calendar.readonly" + - connection: "slack" + scopes: + - "chat:write" + - "channels:read" - name: "My CIMD Client" external_client_id: "https://mcpserver.example.com/.well-known/client.json" diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index f8f6c9289..643b8b8c8 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -66,6 +66,45 @@ const myOrganizationConfigurationSchema = { required: ['allowed_strategies', 'connection_deletion_behavior'], }; +const tokenVaultPrivilegedAccessSchema = { + type: ['object', 'null'], + description: + 'Settings for Token Vault Privileged Access, hardening a privileged client by restricting the caller IPs, connections, and scopes it may use at runtime. Early Access, gated by the token_vault_subject_type_jwt_ea_rollout feature flag.', + properties: { + ip_allowlist: { + type: 'array', + description: + 'IPv4/IPv6 addresses or CIDR ranges permitted to call token exchange on behalf of this privileged client. When the EA flag is on, this is required (non-empty) on create if token_vault_privileged_access is being set.', + items: { + type: 'string', + }, + }, + grants: { + type: 'array', + description: + 'Connection/scope pin objects restricting which federated connections and OAuth scopes the privileged client may use at runtime. Maximum 5 connections; maximum 20 scopes in total across all connections.', + items: { + type: 'object', + properties: { + connection: { + type: 'string', + description: + 'The connection name (e.g. google-oauth2). Validated at runtime; the connection need not exist at configuration time.', + }, + scopes: { + type: 'array', + description: 'The OAuth scopes permitted for that connection.', + items: { + type: 'string', + }, + }, + }, + required: ['connection', 'scopes'], + }, + }, + }, +}; + export const schema = { type: 'array', items: { @@ -352,6 +391,7 @@ export const schema = { }, }, }, + token_vault_privileged_access: tokenVaultPrivilegedAccessSchema, third_party_security_mode: { type: 'string', enum: ['strict', 'permissive'], diff --git a/test/tools/auth0/handlers/clients.tests.js b/test/tools/auth0/handlers/clients.tests.js index 265c5f6f8..07d74dbc0 100644 --- a/test/tools/auth0/handlers/clients.tests.js +++ b/test/tools/auth0/handlers/clients.tests.js @@ -491,6 +491,62 @@ describe('#clients handler', () => { expect(wasCreateCalled).to.be.true; }); + it('should allow valid token_vault_privileged_access property in client', async () => { + const clientWithTokenVault = { + name: 'clientWithTokenVault', + token_vault_privileged_access: { + ip_allowlist: ['192.168.1.0/24', '10.0.0.1'], + grants: [ + { + connection: 'google-oauth2', + scopes: ['https://www.googleapis.com/auth/calendar.readonly'], + }, + { + connection: 'slack', + scopes: ['chat:write', 'channels:read'], + }, + ], + }, + }; + let wasCreateCalled = false; + const auth0 = { + clients: { + create: function (data) { + wasCreateCalled = true; + expect(data).to.be.an('object'); + expect(data.name).to.equal('clientWithTokenVault'); + expect(data.token_vault_privileged_access).to.deep.equal({ + ip_allowlist: ['192.168.1.0/24', '10.0.0.1'], + grants: [ + { + connection: 'google-oauth2', + scopes: ['https://www.googleapis.com/auth/calendar.readonly'], + }, + { + connection: 'slack', + scopes: ['chat:write', 'channels:read'], + }, + ], + }); + return Promise.resolve({ data }); + }, + update: () => Promise.resolve({ data: [] }), + delete: () => Promise.resolve({ data: [] }), + list: (params) => mockPagedData(params, 'clients', []), + }, + connectionProfiles: { list: (params) => mockPagedData(params, 'connectionProfiles', []) }, + userAttributeProfiles: { + list: (params) => mockPagedData(params, 'userAttributeProfiles', []), + }, + pool, + }; + const handler = new clients.default({ client: pageClient(auth0), config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + await stageFn.apply(handler, [{ clients: [clientWithTokenVault] }]); + // eslint-disable-next-line no-unused-expressions + expect(wasCreateCalled).to.be.true; + }); + it('should allow valid session_transfer delegation property in client', async () => { const clientWithDelegation = { name: 'clientWithDelegation', From 4d624b3d69c050791185a89348d7d12d6c16bcfd Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Thu, 23 Jul 2026 11:52:41 +0530 Subject: [PATCH 2/5] format: fix prettier --- docs/resource-specific-documentation.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index d5a9bbfb5..bcb3c7b2b 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -1705,10 +1705,10 @@ Contents of `My API Client.json` (deploy-time, with pem): The Deploy CLI supports the `token_vault_privileged_access` property on clients, which hardens a privileged Token Vault worker by restricting the caller IPs, connections, and scopes it may use at runtime. -| Field | Type | Description | -| -------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `ip_allowlist` | array of strings | IPv4/IPv6 addresses or CIDR ranges permitted to call token exchange on behalf of this client. | -| `grants` | array of objects | Connection/scope pin objects. Each has a `connection` (name) and `scopes` (array). Max 5 connections; max 20 scopes total. | +| Field | Type | Description | +| -------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `ip_allowlist` | array of strings | IPv4/IPv6 addresses or CIDR ranges permitted to call token exchange on behalf of this client. | +| `grants` | array of objects | Connection/scope pin objects. Each has a `connection` (name) and `scopes` (array). Max 5 connections; max 20 scopes total. | ```yaml clients: @@ -1716,16 +1716,16 @@ clients: app_type: non_interactive token_vault_privileged_access: ip_allowlist: - - "192.168.1.0/24" - - "10.0.0.1" + - '192.168.1.0/24' + - '10.0.0.1' grants: - connection: google-oauth2 scopes: - - "https://www.googleapis.com/auth/calendar.readonly" + - 'https://www.googleapis.com/auth/calendar.readonly' - connection: slack scopes: - - "chat:write" - - "channels:read" + - 'chat:write' + - 'channels:read' ``` > **Note:** The `credentials` sub-object of `token_vault_privileged_access` is **not** managed by the Deploy CLI. On read, Auth0 returns it as credential references (`id`) rather than names or key material, and the Deploy CLI never syncs IDs. Manage the privileged client's credentials directly on the tenant. From ade30a2db13c77a5e12c73094a460f7907775767 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Tue, 28 Jul 2026 13:02:24 +0530 Subject: [PATCH 3/5] fix: strip token_vault_privileged_access.credentials on export and deploy --- docs/resource-specific-documentation.md | 2 +- src/tools/auth0/handlers/clients.ts | 31 ++++++- test/tools/auth0/handlers/clients.tests.js | 99 ++++++++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index bcb3c7b2b..70c542c6d 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -1728,4 +1728,4 @@ clients: - 'channels:read' ``` -> **Note:** The `credentials` sub-object of `token_vault_privileged_access` is **not** managed by the Deploy CLI. On read, Auth0 returns it as credential references (`id`) rather than names or key material, and the Deploy CLI never syncs IDs. Manage the privileged client's credentials directly on the tenant. +> **Note:** The `credentials` sub-object of `token_vault_privileged_access` is **not** managed by the Deploy CLI. On read, Auth0 returns it as tenant-specific credential references (`id`) rather than names or key material, and the Deploy CLI never syncs IDs. It is therefore stripped on export (never written to disk) and on deploy (never sent on create/update), so an export→deploy round-trip will never re-send stale credential IDs. Only `ip_allowlist` and `grants` are managed. Manage the privileged client's credentials directly on the tenant. diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index 643b8b8c8..a8d09d1fd 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -560,6 +560,25 @@ const createClientSanitizer = (clients: Client[]): ClientSanitizerChain => { }; }; +/** + * Remove `credentials` from `token_vault_privileged_access`. + * + * Auth0 returns these as tenant-specific credential references (`{ id }`) — never + * names or key material — so they are neither portable nor manageable by the Deploy + * CLI. On export they must not be written to disk; on write they must not be re-sent. + * `ip_allowlist` and `grants` are preserved. Mutates and returns the same client. + */ +const stripTokenVaultCredentials = (client: Client): Client => { + const tvpa = (client as { token_vault_privileged_access?: { credentials?: unknown } }) + .token_vault_privileged_access; + + if (tvpa && 'credentials' in tvpa) { + delete tvpa.credentials; + } + + return client; +}; + export default class ClientHandler extends DefaultAPIHandler { existing: Client[]; @@ -702,6 +721,11 @@ export default class ClientHandler extends DefaultAPIHandler { } delete item.client_authentication_methods; + // token_vault_privileged_access.credentials are tenant-specific ids managed + // outside the Deploy CLI. Strip them so stale ids are never sent on create/update, + // while preserving ip_allowlist and grants. + stripTokenVaultCredentials(item); + return item; }); }; @@ -727,7 +751,12 @@ export default class ClientHandler extends DefaultAPIHandler { ...(shouldExcludeThirdPartyClients(this.config) && { is_first_party: true }), }); - const sanitized = createClientSanitizer(clients).sanitizeCrossOriginAuth(false).get(); + const sanitized = createClientSanitizer(clients) + .sanitizeCrossOriginAuth(false) + .get() + // token_vault_privileged_access.credentials are returned as tenant-specific + // ids — strip them so they are never exported to disk. + .map(stripTokenVaultCredentials); // Enrich credential stubs with name and credential_type for export. // Auth0 does not return pem on read — it is never exported. diff --git a/test/tools/auth0/handlers/clients.tests.js b/test/tools/auth0/handlers/clients.tests.js index 07d74dbc0..fe2043180 100644 --- a/test/tools/auth0/handlers/clients.tests.js +++ b/test/tools/auth0/handlers/clients.tests.js @@ -1894,6 +1894,41 @@ describe('#clients handler', () => { expect(cred.kid).to.equal(undefined); expect(cred.alg).to.equal(undefined); }); + + it('should strip token_vault_privileged_access.credentials on export while keeping ip_allowlist and grants', async () => { + const auth0 = { + clients: { + create: () => Promise.resolve({ data: {} }), + update: () => Promise.resolve({ data: {} }), + delete: () => Promise.resolve({ data: {} }), + list: (params) => + mockPagedData(params, 'clients', [ + { + client_id: 'client1', + name: 'Privileged App', + token_vault_privileged_access: { + credentials: [{ id: 'cred_abc' }], + ip_allowlist: ['10.0.0.1'], + grants: [{ connection: 'google-oauth2', scopes: ['openid'] }], + }, + }, + ]), + }, + connectionProfiles: { list: (params) => mockPagedData(params, 'connectionProfiles', []) }, + userAttributeProfiles: { + list: (params) => mockPagedData(params, 'userAttributeProfiles', []), + }, + pool, + }; + + const handler = new clients.default({ client: pageClient(auth0), config }); + const result = await handler.getType(); + + const tvpa = result[0].token_vault_privileged_access; + expect(tvpa).to.not.have.property('credentials'); + expect(tvpa.ip_allowlist).to.deep.equal(['10.0.0.1']); + expect(tvpa.grants).to.deep.equal([{ connection: 'google-oauth2', scopes: ['openid'] }]); + }); }); describe('#clients pem stripping in processChanges', () => { @@ -1975,6 +2010,70 @@ describe('#clients handler', () => { expect(updatePayloads['client1']).to.not.have.property('client_authentication_methods'); expect(updatePayloads['client2']).to.not.have.property('client_authentication_methods'); }); + + it('should strip token_vault_privileged_access.credentials from create/update while keeping ip_allowlist and grants', async () => { + const createPayloads = []; + const updatePayloads = {}; + const auth0 = { + clients: { + create: (data) => { + createPayloads.push(data); + return Promise.resolve({ data }); + }, + update: (clientId, data) => { + updatePayloads[clientId] = data; + return Promise.resolve({ data }); + }, + delete: () => Promise.resolve({ data: {} }), + list: (params) => + mockPagedData(params, 'clients', [ + { + client_id: 'client1', + name: 'Existing Privileged App', + }, + ]), + }, + connectionProfiles: { list: (params) => mockPagedData(params, 'connectionProfiles', []) }, + userAttributeProfiles: { + list: (params) => mockPagedData(params, 'userAttributeProfiles', []), + }, + pool, + }; + + const handler = new clients.default({ client: pageClient(auth0), config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + const tvpa = { + credentials: [{ id: 'cred_stale' }], + ip_allowlist: ['10.0.0.1'], + grants: [{ connection: 'google-oauth2', scopes: ['openid'] }], + }; + + await stageFn.apply(handler, [ + { + clients: [ + // Existing client -> update + { + client_id: 'client1', + name: 'Existing Privileged App', + token_vault_privileged_access: { ...tvpa }, + }, + // New client -> create + { name: 'New Privileged App', token_vault_privileged_access: { ...tvpa } }, + ], + }, + ]); + + const updated = updatePayloads['client1'].token_vault_privileged_access; + expect(updated).to.not.have.property('credentials'); + expect(updated.ip_allowlist).to.deep.equal(['10.0.0.1']); + expect(updated.grants).to.deep.equal([{ connection: 'google-oauth2', scopes: ['openid'] }]); + + const created = createPayloads[0].token_vault_privileged_access; + expect(created).to.not.have.property('credentials'); + expect(created.ip_allowlist).to.deep.equal(['10.0.0.1']); + expect(created.grants).to.deep.equal([{ connection: 'google-oauth2', scopes: ['openid'] }]); + }); }); describe('#clients validate', () => { From ec97b679d111b8bb7a52ef7920956e2a90bec6f1 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Tue, 28 Jul 2026 16:20:50 +0530 Subject: [PATCH 4/5] fix: make token_vault_privileged_access export-only to avoid re-sending stale credential IDs --- docs/resource-specific-documentation.md | 8 +- src/tools/auth0/handlers/clients.ts | 40 +++++++--- test/tools/auth0/handlers/clients.tests.js | 88 +++++++--------------- 3 files changed, 61 insertions(+), 75 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 70c542c6d..db0270eee 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -1703,13 +1703,17 @@ Contents of `My API Client.json` (deploy-time, with pem): > **Early Access:** Enforcement of `ip_allowlist` and `grants` requires the `token_vault_subject_type_jwt_ea_rollout` feature flag to be enabled on the tenant. When the flag is off the fields are accepted but not stored or returned. -The Deploy CLI supports the `token_vault_privileged_access` property on clients, which hardens a privileged Token Vault worker by restricting the caller IPs, connections, and scopes it may use at runtime. +The Deploy CLI exports the `token_vault_privileged_access` property on clients, which hardens a privileged Token Vault worker by restricting the caller IPs, connections, and scopes it may use at runtime. + +> **Export-only field:** `token_vault_privileged_access` is exported for visibility but **is not deployed** by the Deploy CLI — it is stripped from create/update payloads. The Management API requires a `credentials` array (tenant-specific credential `id` references) whenever the object is sent, and those ids are never persisted by the CLI because they are not portable across tenants. Sending the object without them fails validation, and sending exported ids would re-send stale references on a cross-tenant deploy. Manage `token_vault_privileged_access` directly on the tenant. | Field | Type | Description | | -------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | | `ip_allowlist` | array of strings | IPv4/IPv6 addresses or CIDR ranges permitted to call token exchange on behalf of this client. | | `grants` | array of objects | Connection/scope pin objects. Each has a `connection` (name) and `scopes` (array). Max 5 connections; max 20 scopes total. | +Exported shape (`credentials` is stripped; `ip_allowlist` and `grants` are kept for visibility): + ```yaml clients: - name: My Token Vault Privileged App @@ -1727,5 +1731,3 @@ clients: - 'chat:write' - 'channels:read' ``` - -> **Note:** The `credentials` sub-object of `token_vault_privileged_access` is **not** managed by the Deploy CLI. On read, Auth0 returns it as tenant-specific credential references (`id`) rather than names or key material, and the Deploy CLI never syncs IDs. It is therefore stripped on export (never written to disk) and on deploy (never sent on create/update), so an export→deploy round-trip will never re-send stale credential IDs. Only `ip_allowlist` and `grants` are managed. Manage the privileged client's credentials directly on the tenant. diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index a8d09d1fd..0777fda08 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -561,14 +561,14 @@ const createClientSanitizer = (clients: Client[]): ClientSanitizerChain => { }; /** - * Remove `credentials` from `token_vault_privileged_access`. + * On export, remove `credentials` from `token_vault_privileged_access`. * * Auth0 returns these as tenant-specific credential references (`{ id }`) — never - * names or key material — so they are neither portable nor manageable by the Deploy - * CLI. On export they must not be written to disk; on write they must not be re-sent. - * `ip_allowlist` and `grants` are preserved. Mutates and returns the same client. + * names or key material — so they are not portable across tenants and must never be + * written to disk. `ip_allowlist` and `grants` are kept for visibility. Mutates and + * returns the same client. */ -const stripTokenVaultCredentials = (client: Client): Client => { +const stripTokenVaultCredentialsOnExport = (client: Client): Client => { const tvpa = (client as { token_vault_privileged_access?: { credentials?: unknown } }) .token_vault_privileged_access; @@ -579,6 +579,26 @@ const stripTokenVaultCredentials = (client: Client): Client => { return client; }; +/** + * On write (create/update), remove the entire `token_vault_privileged_access` object. + * + * The Management API requires `credentials` whenever `token_vault_privileged_access` + * is present in the payload, but those credentials are tenant-specific ids that the + * Deploy CLI intentionally never persists (see stripTokenVaultCredentialsOnExport). + * Sending the object without `credentials` fails validation ("Missing required + * property: credentials"); sending it with exported ids would re-send stale ids on a + * cross-tenant deploy. The Deploy CLI therefore does not manage this field — it is + * export-only. Manage `token_vault_privileged_access` directly on the tenant. + * Mutates and returns the same client. + */ +const stripTokenVaultPrivilegedAccessOnWrite = (client: Client): Client => { + if ('token_vault_privileged_access' in client) { + delete (client as { token_vault_privileged_access?: unknown }).token_vault_privileged_access; + } + + return client; +}; + export default class ClientHandler extends DefaultAPIHandler { existing: Client[]; @@ -721,10 +741,10 @@ export default class ClientHandler extends DefaultAPIHandler { } delete item.client_authentication_methods; - // token_vault_privileged_access.credentials are tenant-specific ids managed - // outside the Deploy CLI. Strip them so stale ids are never sent on create/update, - // while preserving ip_allowlist and grants. - stripTokenVaultCredentials(item); + // token_vault_privileged_access requires tenant-specific credential ids that + // the Deploy CLI never persists. Strip the whole object on write so we neither + // re-send stale ids nor fail validation for the missing credentials property. + stripTokenVaultPrivilegedAccessOnWrite(item); return item; }); @@ -756,7 +776,7 @@ export default class ClientHandler extends DefaultAPIHandler { .get() // token_vault_privileged_access.credentials are returned as tenant-specific // ids — strip them so they are never exported to disk. - .map(stripTokenVaultCredentials); + .map(stripTokenVaultCredentialsOnExport); // Enrich credential stubs with name and credential_type for export. // Auth0 does not return pem on read — it is never exported. diff --git a/test/tools/auth0/handlers/clients.tests.js b/test/tools/auth0/handlers/clients.tests.js index fe2043180..5f6698a6b 100644 --- a/test/tools/auth0/handlers/clients.tests.js +++ b/test/tools/auth0/handlers/clients.tests.js @@ -491,60 +491,28 @@ describe('#clients handler', () => { expect(wasCreateCalled).to.be.true; }); - it('should allow valid token_vault_privileged_access property in client', async () => { - const clientWithTokenVault = { - name: 'clientWithTokenVault', - token_vault_privileged_access: { - ip_allowlist: ['192.168.1.0/24', '10.0.0.1'], - grants: [ - { - connection: 'google-oauth2', - scopes: ['https://www.googleapis.com/auth/calendar.readonly'], - }, - { - connection: 'slack', - scopes: ['chat:write', 'channels:read'], - }, - ], - }, - }; - let wasCreateCalled = false; - const auth0 = { - clients: { - create: function (data) { - wasCreateCalled = true; - expect(data).to.be.an('object'); - expect(data.name).to.equal('clientWithTokenVault'); - expect(data.token_vault_privileged_access).to.deep.equal({ - ip_allowlist: ['192.168.1.0/24', '10.0.0.1'], - grants: [ - { - connection: 'google-oauth2', - scopes: ['https://www.googleapis.com/auth/calendar.readonly'], - }, - { - connection: 'slack', - scopes: ['chat:write', 'channels:read'], - }, - ], - }); - return Promise.resolve({ data }); + it('should pass schema validation for a valid token_vault_privileged_access property', () => { + const ajv = new Ajv({ useDefaults: true, nullable: true }); + const valid = ajv.validate(clients.schema, [ + { + name: 'clientWithTokenVault', + token_vault_privileged_access: { + ip_allowlist: ['192.168.1.0/24', '10.0.0.1'], + grants: [ + { + connection: 'google-oauth2', + scopes: ['https://www.googleapis.com/auth/calendar.readonly'], + }, + { + connection: 'slack', + scopes: ['chat:write', 'channels:read'], + }, + ], }, - update: () => Promise.resolve({ data: [] }), - delete: () => Promise.resolve({ data: [] }), - list: (params) => mockPagedData(params, 'clients', []), - }, - connectionProfiles: { list: (params) => mockPagedData(params, 'connectionProfiles', []) }, - userAttributeProfiles: { - list: (params) => mockPagedData(params, 'userAttributeProfiles', []), }, - pool, - }; - const handler = new clients.default({ client: pageClient(auth0), config }); - const stageFn = Object.getPrototypeOf(handler).processChanges; - await stageFn.apply(handler, [{ clients: [clientWithTokenVault] }]); - // eslint-disable-next-line no-unused-expressions - expect(wasCreateCalled).to.be.true; + ]); + expect(valid).to.equal(true); + expect(ajv.errors).to.be.null; }); it('should allow valid session_transfer delegation property in client', async () => { @@ -2011,7 +1979,7 @@ describe('#clients handler', () => { expect(updatePayloads['client2']).to.not.have.property('client_authentication_methods'); }); - it('should strip token_vault_privileged_access.credentials from create/update while keeping ip_allowlist and grants', async () => { + it('should strip the entire token_vault_privileged_access object from create/update', async () => { const createPayloads = []; const updatePayloads = {}; const auth0 = { @@ -2064,15 +2032,11 @@ describe('#clients handler', () => { }, ]); - const updated = updatePayloads['client1'].token_vault_privileged_access; - expect(updated).to.not.have.property('credentials'); - expect(updated.ip_allowlist).to.deep.equal(['10.0.0.1']); - expect(updated.grants).to.deep.equal([{ connection: 'google-oauth2', scopes: ['openid'] }]); - - const created = createPayloads[0].token_vault_privileged_access; - expect(created).to.not.have.property('credentials'); - expect(created.ip_allowlist).to.deep.equal(['10.0.0.1']); - expect(created.grants).to.deep.equal([{ connection: 'google-oauth2', scopes: ['openid'] }]); + // The Management API requires credentials whenever token_vault_privileged_access + // is sent, but those are non-portable tenant-specific ids the CLI never persists. + // The whole object is therefore stripped on write — the field is export-only. + expect(updatePayloads['client1']).to.not.have.property('token_vault_privileged_access'); + expect(createPayloads[0]).to.not.have.property('token_vault_privileged_access'); }); }); From a36428485e45fbdb7835ae1464ac7b846e0e4bd0 Mon Sep 17 00:00:00 2001 From: Harshith Rai Date: Tue, 28 Jul 2026 17:17:25 +0530 Subject: [PATCH 5/5] fix: correct token_vault_privileged_access docs to reflect export-only behavior --- docs/resource-specific-documentation.md | 2 +- examples/yaml/tenant.yaml | 4 +++- src/tools/auth0/handlers/clients.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/resource-specific-documentation.md b/docs/resource-specific-documentation.md index 7b2e0e28d..f43ecdb07 100644 --- a/docs/resource-specific-documentation.md +++ b/docs/resource-specific-documentation.md @@ -1762,7 +1762,7 @@ Contents of `My API Client.json` (deploy-time, with pem): ## Token Vault Privileged Access -> **Early Access:** Enforcement of `ip_allowlist` and `grants` requires the `token_vault_subject_type_jwt_ea_rollout` feature flag to be enabled on the tenant. When the flag is off the fields are accepted but not stored or returned. +> **Early Access:** `token_vault_privileged_access` requires the `token_vault_subject_type_jwt_ea_rollout` feature flag to be enabled on the tenant, and writes additionally require the `create:client_token_vault_privileged_access` / `update:client_token_vault_privileged_access` scopes. This field is export-only in the Deploy CLI (see below), so these requirements affect only manual configuration on the tenant, not the CLI. The Deploy CLI exports the `token_vault_privileged_access` property on clients, which hardens a privileged Token Vault worker by restricting the caller IPs, connections, and scopes it may use at runtime. diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index dad95e677..d89a7234b 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -81,7 +81,9 @@ clients: - name: "My Token Vault Privileged App" app_type: "non_interactive" - # Early Access: requires the token_vault_subject_type_jwt_ea_rollout feature flag. + # Export-only: token_vault_privileged_access is exported for visibility but is NOT + # deployed by the CLI (it is stripped from import). Manage it directly on the tenant. + # See docs/resource-specific-documentation.md#token-vault-privileged-access. token_vault_privileged_access: ip_allowlist: - "192.168.1.0/24" diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index 4f37e620c..aff148490 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -69,7 +69,7 @@ const myOrganizationConfigurationSchema = { const tokenVaultPrivilegedAccessSchema = { type: ['object', 'null'], description: - 'Settings for Token Vault Privileged Access, hardening a privileged client by restricting the caller IPs, connections, and scopes it may use at runtime. Early Access, gated by the token_vault_subject_type_jwt_ea_rollout feature flag.', + 'Settings for Token Vault Privileged Access, hardening a privileged client by restricting the caller IPs, connections, and scopes it may use at runtime. Early Access, gated by the token_vault_subject_type_jwt_ea_rollout feature flag. Export-only: this object is exported for visibility but is not deployed by the Deploy CLI (it is stripped from create/update payloads because the required credentials are tenant-specific ids). Manage it directly on the tenant.', properties: { ip_allowlist: { type: 'array',