Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ yarn-error.log
*.pub
Makefile
.vscode/*
.claude/*
CLAUDE.md
.github/copilot-instructions.md
.github/chatmodes/*
.github/prompts/*
Expand Down
2 changes: 1 addition & 1 deletion docs/excluding-from-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
42 changes: 36 additions & 6 deletions src/tools/auth0/handlers/clientGrants.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -97,6 +97,20 @@ 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
if (shouldExcludeThirdPartyClients(this.config)) {
const clients = await paginate<Client>(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;
}

Expand Down Expand Up @@ -125,24 +139,40 @@ 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');

// 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 (shouldExcludeThirdPartyClients(this.config)) {
filtered = filtered.filter((item) => !thirdPartyClientIds.has(item.client_id));
}

return filtered;
};

const changes: CalculatedChanges = {
Expand Down
13 changes: 3 additions & 10 deletions src/tools/auth0/handlers/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<Client>(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();
Expand Down
13 changes: 13 additions & 0 deletions src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
112 changes: 112 additions & 0 deletions test/tools/auth0/handlers/clientGrants.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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,
};
});
});