From 9705fdc3243c16aac28581a357a6e083e34bc15c Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Fri, 6 Feb 2026 15:49:04 +0530 Subject: [PATCH 1/2] feat: exclude third-party client grants based on configuration - src/tools/auth0/handlers/clientGrants.ts: add logic to filter out third-party client grants when AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS is enabled - test/tools/auth0/handlers/clientGrants.tests.js: add tests for excluding third-party client grants in getType and ensure they are not deleted --- .gitignore | 2 + src/tools/auth0/handlers/clientGrants.ts | 49 +++++++- .../auth0/handlers/clientGrants.tests.js | 112 ++++++++++++++++++ 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 92e4e55c..12c875f1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ yarn-error.log *.pub Makefile .vscode/* +.claude/* +CLAUDE.md .github/copilot-instructions.md .github/chatmodes/* .github/prompts/* diff --git a/src/tools/auth0/handlers/clientGrants.ts b/src/tools/auth0/handlers/clientGrants.ts index 235af94b..406e44f4 100644 --- a/src/tools/auth0/handlers/clientGrants.ts +++ b/src/tools/auth0/handlers/clientGrants.ts @@ -97,6 +97,24 @@ export default class ClientGrantsHandler extends DefaultHandler { this.existing = this.existing.filter((grant) => grant.client_id !== currentClient); + // Filter out third-party client grants when AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS is enabled + const excludeThirdPartyClients = + this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || + this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; + + if (excludeThirdPartyClients) { + const clients = await paginate(this.client.clients.list, { + paginate: true, + is_first_party: true, + }); + + const firstPartyClientIds = new Set(clients.map((c) => c.client_id)); + + this.existing = this.existing.filter((grant) => + firstPartyClientIds.has(grant.client_id) + ); + } + return this.existing; } @@ -125,24 +143,45 @@ export default class ClientGrantsHandler extends DefaultHandler { // Always filter out the client we are using to access Auth0 Management API const currentClient = this.config('AUTH0_CLIENT_ID'); + // Check if third-party clients should be excluded + const excludeThirdPartyClients = + this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || + this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; + + // Build a set of third-party client IDs for efficient lookup + const thirdPartyClientIds = new Set( + clients.filter((c) => c.is_first_party === false).map((c) => c.client_id) + ); + const { del, update, create, conflicts } = await this.calcChanges({ ...assets, clientGrants: formatted, }); const filterGrants = (list: ClientGrant[]) => { + let filtered = list; + + // Filter out the current client (Auth0 Management API client) + filtered = filtered.filter((item) => item.client_id !== currentClient); + + // Filter out excluded clients if (excludedClients.length) { - return list.filter( + filtered = filtered.filter( (item) => - item.client_id !== currentClient && item.client_id && ![...excludedClientsByNames, ...excludedClients].includes(item.client_id) ); } - return list - .filter((item) => item.client_id !== currentClient) - .filter((item) => item.is_system !== true); + // Filter out system grants + filtered = filtered.filter((item) => item.is_system !== true); + + // Filter out third-party client grants when flag is enabled + if (excludeThirdPartyClients) { + filtered = filtered.filter((item) => !thirdPartyClientIds.has(item.client_id)); + } + + return filtered; }; const changes: CalculatedChanges = { diff --git a/test/tools/auth0/handlers/clientGrants.tests.js b/test/tools/auth0/handlers/clientGrants.tests.js index 6b9b79d4..b191298f 100644 --- a/test/tools/auth0/handlers/clientGrants.tests.js +++ b/test/tools/auth0/handlers/clientGrants.tests.js @@ -256,6 +256,64 @@ describe('#clientGrants handler', () => { expect(data.map((g) => g.id)).to.deep.equal(['cg0', 'cg1', 'cg2', 'cg3', 'cg4']); }); + it('should exclude third-party client grants in getType when AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS is enabled', async () => { + const configWithExclude = function (key) { + return configWithExclude.data && configWithExclude.data[key]; + }; + + configWithExclude.data = { + AUTH0_CLIENT_ID: 'current_client', + AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS: true, + }; + + const clientId1 = 'first_party_client'; + const clientId2 = 'third_party_client'; + const clientGrant1 = { + audience: 'https://test.auth0.com/api/v2/', + client_id: clientId1, + id: 'cgr_first_party', + scope: ['read:logs'], + }; + const clientGrant2 = { + audience: 'https://test.auth0.com/api/v2/', + client_id: clientId2, + id: 'cgr_third_party', + scope: ['read:logs'], + }; + + const auth0 = { + clientGrants: { + list: (params) => mockPagedData(params, 'client_grants', [clientGrant1, clientGrant2]), + }, + clients: { + list: (params) => { + // When is_first_party filter is applied, only return first-party clients + if (params.is_first_party === true) { + return mockPagedData(params, 'clients', [ + { name: 'First Party App', client_id: clientId1, is_first_party: true }, + ]); + } + return mockPagedData(params, 'clients', [ + { name: 'First Party App', client_id: clientId1, is_first_party: true }, + { name: 'Third Party App', client_id: clientId2, is_first_party: false }, + ]); + }, + }, + pool, + }; + + const handler = new clientGrants.default({ + client: pageClient(auth0), + config: configWithExclude, + }); + const data = await handler.getType(); + + // Should only return the first-party client grant + expect(data).to.have.lengthOf(1); + expect(data[0].id).to.equal('cgr_first_party'); + expect(data[0].client_id).to.equal(clientId1); + }); + it('should convert client_name to client_id', async () => { const auth0 = { clientGrants: { @@ -692,4 +750,58 @@ describe('#clientGrants handler', () => { await stageFn.apply(handler, [assets]); }); + + it('should not delete client grants for third-party clients when AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS is enabled', async () => { + config.data = { + AUTH0_CLIENT_ID: 'current_client', + AUTH0_ALLOW_DELETE: true, + AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS: true, + }; + + let deletedGrantId = null; + + const auth0 = { + clientGrants: { + create: (_params) => { + return Promise.resolve({ data: [] }); + }, + update: (_params) => { + return Promise.resolve({ data: [] }); + }, + delete: function (params) { + (() => expect(this).to.not.be.undefined)(); + deletedGrantId = params; + return Promise.resolve({ data: [] }); + }, + list: (params) => + mockPagedData(params, 'client_grants', [ + { id: 'cg1', client_id: 'third_party_client', audience: 'audience1' }, + { id: 'cg2', client_id: 'first_party_client', audience: 'audience2' }, + ]), + }, + clients: { + list: (params) => + mockPagedData(params, 'clients', [ + { name: 'Third Party App', client_id: 'third_party_client', is_first_party: false }, + { name: 'My App', client_id: 'first_party_client', is_first_party: true }, + ]), + }, + pool, + }; + + const handler = new clientGrants.default({ client: pageClient(auth0), config }); + const stageFn = Object.getPrototypeOf(handler).processChanges; + + // Empty array should delete all non-excluded grants + await stageFn.apply(handler, [{ clientGrants: [] }]); + + // Should only delete the first-party client grant, not the third-party one + expect(deletedGrantId).to.equal('cg2'); + + // Reset config to default for subsequent tests + config.data = { + AUTH0_CLIENT_ID: 'client_id', + AUTH0_ALLOW_DELETE: true, + }; + }); }); From 359904f0d28ca65e34accff822f0005a4dfa8592 Mon Sep 17 00:00:00 2001 From: kushalshit27 <43465488+kushalshit27@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:26:37 +0530 Subject: [PATCH 2/2] feat: enhance third-party client exclusion functionality - src/tools/utils.ts: add shouldExcludeThirdPartyClients utility function for config-based exclusion - src/tools/auth0/handlers/clientGrants.ts: refactor to use shouldExcludeThirdPartyClients for filtering grants - src/tools/auth0/handlers/clients.ts: refactor to use shouldExcludeThirdPartyClients for client filtering - docs/excluding-from-management.md: update documentation to clarify client grants exclusion --- docs/excluding-from-management.md | 2 +- src/tools/auth0/handlers/clientGrants.ts | 15 +++------------ src/tools/auth0/handlers/clients.ts | 13 +++---------- src/tools/utils.ts | 13 +++++++++++++ 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/docs/excluding-from-management.md b/docs/excluding-from-management.md index d0614ae0..a5636c49 100644 --- a/docs/excluding-from-management.md +++ b/docs/excluding-from-management.md @@ -57,7 +57,7 @@ Some resource types support exclusions of individual resource by name. This is p ### Excluding third-party clients -You can also exclude all third-party clients at once using the `AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS` configuration option. When enabled, only first-party clients will be included in export and import operations. This is useful when you have Dynamic Client Registration (DCR) enabled and you have a lot of third-party clients in your tenant. +You can also exclude all third-party clients at once using the `AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS` configuration option. When enabled, only first-party clients and their associated client grants will be included in export and import operations. This is useful when you have Dynamic Client Registration (DCR) enabled and you have a lot of third-party clients in your tenant. ```json { diff --git a/src/tools/auth0/handlers/clientGrants.ts b/src/tools/auth0/handlers/clientGrants.ts index 406e44f4..86c9da0e 100644 --- a/src/tools/auth0/handlers/clientGrants.ts +++ b/src/tools/auth0/handlers/clientGrants.ts @@ -1,6 +1,6 @@ import { Management } from 'auth0'; import DefaultHandler, { order } from './default'; -import { convertClientNamesToIds } from '../../utils'; +import { convertClientNamesToIds, shouldExcludeThirdPartyClients } from '../../utils'; import { Assets, CalculatedChanges } from '../../../types'; import DefaultAPIHandler from './default'; import { paginate } from '../client'; @@ -98,11 +98,7 @@ export default class ClientGrantsHandler extends DefaultHandler { this.existing = this.existing.filter((grant) => grant.client_id !== currentClient); // Filter out third-party client grants when AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS is enabled - const excludeThirdPartyClients = - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; - - if (excludeThirdPartyClients) { + if (shouldExcludeThirdPartyClients(this.config)) { const clients = await paginate(this.client.clients.list, { paginate: true, is_first_party: true, @@ -143,11 +139,6 @@ export default class ClientGrantsHandler extends DefaultHandler { // Always filter out the client we are using to access Auth0 Management API const currentClient = this.config('AUTH0_CLIENT_ID'); - // Check if third-party clients should be excluded - const excludeThirdPartyClients = - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; - // Build a set of third-party client IDs for efficient lookup const thirdPartyClientIds = new Set( clients.filter((c) => c.is_first_party === false).map((c) => c.client_id) @@ -177,7 +168,7 @@ export default class ClientGrantsHandler extends DefaultHandler { filtered = filtered.filter((item) => item.is_system !== true); // Filter out third-party client grants when flag is enabled - if (excludeThirdPartyClients) { + if (shouldExcludeThirdPartyClients(this.config)) { filtered = filtered.filter((item) => !thirdPartyClientIds.has(item.client_id)); } diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index d7bcce0c..84295d24 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -6,6 +6,7 @@ import DefaultAPIHandler from './default'; import { getConnectionProfile } from './connectionProfiles'; import { getUserAttributeProfiles } from './userAttributeProfiles'; import log from '../../../logger'; +import { shouldExcludeThirdPartyClients } from '../../utils'; const multiResourceRefreshTokenPoliciesSchema = { type: ['array', 'null'], @@ -458,10 +459,6 @@ export default class ClientHandler extends DefaultAPIHandler { const excludedClients = (assets.exclude && assets.exclude.clients) || []; - const excludeThirdPartyClients = - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; - const { del, update, create, conflicts } = await this.calcChanges(assets); // Always filter out the client we are using to access Auth0 Management API @@ -480,7 +477,7 @@ export default class ClientHandler extends DefaultAPIHandler { item.client_id !== currentClient && item.name && !excludedClients.includes(item.name) && - (!excludeThirdPartyClients || item.is_first_party) + (!shouldExcludeThirdPartyClients(this.config) || item.is_first_party) ); // Sanitize client fields @@ -521,14 +518,10 @@ export default class ClientHandler extends DefaultAPIHandler { async getType() { if (this.existing) return this.existing; - const excludeThirdPartyClients = - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === 'true' || - this.config('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS') === true; - const clients = await paginate(this.client.clients.list, { paginate: true, is_global: false, - ...(excludeThirdPartyClients && { is_first_party: true }), + ...(shouldExcludeThirdPartyClients(this.config) && { is_first_party: true }), }); this.existing = createClientSanitizer(clients).sanitizeCrossOriginAuth().get(); diff --git a/src/tools/utils.ts b/src/tools/utils.ts index 964f64f7..c642d28a 100644 --- a/src/tools/utils.ts +++ b/src/tools/utils.ts @@ -329,3 +329,16 @@ export function maskSecretAtPath({ } return maskOnObj; } + +/** + * Determines whether third-party clients should be excluded based on configuration. + * Checks the AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS config value and returns true if it's + * set to boolean true or string 'true'. + * + * @param configFn - The configuration function to retrieve the config value. + * @returns True if third-party clients should be excluded, false otherwise. + */ +export const shouldExcludeThirdPartyClients = (configFn: (key: string) => any): boolean => { + const value = configFn('AUTH0_EXCLUDE_THIRD_PARTY_CLIENTS'); + return value === 'true' || value === true; +};