Skip to content
Merged
6 changes: 6 additions & 0 deletions examples/directory/clients/My Native app.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@
"token_endpoint_auth_method": "none",
"custom_login_page_on": true,
"oidc_logout": {
"backchannel_logout_urls": [
"https://example.com/logout"
],
"backchannel_logout_initiators": {
"mode": "custom",
"selected_initiators": [
"rp-logout",
"idp-logout"
]
},
"backchannel_logout_session_metadata": {
"include": true
}
}
}
10 changes: 10 additions & 0 deletions examples/yaml/tenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ clients:
native_social_login:
google:
enabled: true
oidc_logout:
backchannel_logout_urls:
- "https://example.com/logout"
backchannel_logout_initiators:
mode: "custom"
selected_initiators:
- "rp-logout"
- "idp-logout"
backchannel_logout_session_metadata:
include: true
# Add other client settings https://auth0.com/docs/api/management/v2#!/Clients/post_clients
-
name: "My Resource Server Client"
Expand Down
186 changes: 143 additions & 43 deletions src/tools/auth0/handlers/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,151 @@ export const schema = {
},
},
},
oidc_logout: {
type: ['object', 'null'],
description: 'Configuration for OIDC backchannel logout',
properties: {
backchannel_logout_urls: {
type: 'array',
description:
'Comma-separated list of URLs that are valid to call back from Auth0 for OIDC backchannel logout. Currently only one URL is allowed.',
items: {
type: 'string',
},
},
backchannel_logout_initiators: {
type: 'object',
description: 'Configuration for OIDC backchannel logout initiators',
properties: {
mode: {
type: 'string',
schemaName: 'ClientOIDCBackchannelLogoutInitiatorsModeEnum',
enum: ['custom', 'all'],
description:
'The `mode` property determines the configuration method for enabling initiators. `custom` enables only the initiators listed in the selected_initiators array, `all` enables all current and future initiators.',
},
selected_initiators: {
type: 'array',
items: {
type: 'string',
enum: [
'rp-logout',
'idp-logout',
'password-changed',
'session-expired',
'session-revoked',
'account-deleted',
'email-identifier-changed',
'mfa-phone-unenrolled',
'account-deactivated',
],
description:
'The `selected_initiators` property contains the list of initiators to be enabled for the given application.',
},
},
},
},
backchannel_logout_session_metadata: {
type: ['object', 'null'],
description:
'Controls whether session metadata is included in the logout token. Default value is null.',
properties: {
include: {
type: 'boolean',
description:
'The `include` property determines whether session metadata is included in the logout token.',
},
},
},
},
},
},
required: ['name'],
},
};

export type Client = Management.Client;

type ClientSanitizerChain = {
sanitizeOidcLogout(): ClientSanitizerChain;
sanitizeCrossOriginAuth(): ClientSanitizerChain;
get(): Client[];
};

const createClientSanitizer = (clients: Client[]): ClientSanitizerChain => {
let sanitized = clients;

return {
sanitizeCrossOriginAuth() {
const deprecatedClients: string[] = [];

sanitized = sanitized.map((client) => {
let updated: Client = { ...client };

if (has(updated, 'cross_origin_auth')) {
const clientName = client.name || client.client_id || 'unknown client';
deprecatedClients.push(clientName);

if (!has(updated, 'cross_origin_authentication')) {
updated.cross_origin_authentication = updated.cross_origin_auth;
}

updated = omit(updated, 'cross_origin_auth') as Client;
}

return updated;
});

if (deprecatedClients.length > 0) {
log.warn(
"The 'cross_origin_auth' parameter is deprecated in clients and scheduled for removal in future releases.\n" +
`Use 'cross_origin_authentication' going forward. Clients using the deprecated setting: [${deprecatedClients.join(
', '
)}]`
);
}

return this;
},

sanitizeOidcLogout() {
const deprecatedClients: string[] = [];

sanitized = sanitized.map((client) => {
let updated: Client = { ...client };

if (has(updated, 'oidc_backchannel_logout')) {
const clientName = client.name || client.client_id || 'unknown client';
deprecatedClients.push(clientName);

if (!has(updated, 'oidc_logout')) {
updated.oidc_logout = updated.oidc_backchannel_logout;
}

updated = omit(updated, 'oidc_backchannel_logout') as Client;
}

return updated;
});

if (deprecatedClients.length > 0) {
log.warn(
"The 'oidc_backchannel_logout' parameter is deprecated in clients and scheduled for removal in future releases.\n" +
`Use 'oidc_logout' going forward. Clients using the deprecated setting: [${deprecatedClients.join(
', '
)}]`
);
}

return this;
},

get: () => {
return sanitized;
},
};
};

export default class ClientHandler extends DefaultAPIHandler {
existing: Client[];

Expand Down Expand Up @@ -347,7 +485,10 @@ export default class ClientHandler extends DefaultAPIHandler {

// Sanitize client fields
const sanitizeClientFields = (list: Client[]): Client[] => {
const sanitizedClients = this.sanitizeCrossOriginAuth(list);
const sanitizedClients = createClientSanitizer(list)
.sanitizeCrossOriginAuth()
.sanitizeOidcLogout()
.get();

return sanitizedClients.map((item: Client) => {
if (item.app_type === 'resource_server') {
Expand Down Expand Up @@ -377,45 +518,6 @@ export default class ClientHandler extends DefaultAPIHandler {
});
}

/**
* @description
* Sanitize the deprecated field `cross_origin_auth` to `cross_origin_authentication`
*
* @param {Client[]} clients - The client array to sanitize.
* @returns {Client[]} The sanitized array of clients.
*/
private sanitizeCrossOriginAuth(clients: Client[]): Client[] {
const deprecatedClients: string[] = [];

const updatedClients = clients.map((client) => {
let updated: Client = { ...client };

if (has(updated, 'cross_origin_auth')) {
const clientName = client.name || client.client_id || 'unknown client';
deprecatedClients.push(clientName);

if (!has(updated, 'cross_origin_authentication')) {
updated.cross_origin_authentication = updated.cross_origin_auth;
}

updated = omit(updated, 'cross_origin_auth') as Client;
}

return updated;
});

if (deprecatedClients.length > 0) {
log.warn(
"The 'cross_origin_auth' parameter is deprecated in clients and scheduled for removal in future releases.\n" +
`Use 'cross_origin_authentication' going forward. Clients using the deprecated setting: [${deprecatedClients.join(
', '
)}]`
);
}

return updatedClients;
}

async getType() {
if (this.existing) return this.existing;

Expand All @@ -429,9 +531,7 @@ export default class ClientHandler extends DefaultAPIHandler {
...(excludeThirdPartyClients && { is_first_party: true }),
});

const sanitizedClients = this.sanitizeCrossOriginAuth(clients);

this.existing = sanitizedClients;
this.existing = createClientSanitizer(clients).sanitizeCrossOriginAuth().get();
return this.existing;
}

Expand Down
79 changes: 79 additions & 0 deletions test/context/directory/clients.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,83 @@ describe('#directory context clients', () => {
organization_require_behavior: 'no_prompt',
});
});

it('should process clients with oidc_logout', async () => {
const files = {
[constants.CLIENTS_DIRECTORY]: {
'oidcLogoutClient.json':
'{ "app_type": "regular_web", "name": "oidcLogoutClient", "oidc_logout": { "backchannel_logout_urls": ["https://example.com/logout"], "backchannel_logout_initiators": { "mode": "custom", "selected_initiators": ["rp-logout", "idp-logout"] }, "backchannel_logout_session_metadata": { "include": true } } }',
'simpleClient.json': '{ "app_type": "spa", "name": "simpleClient" }',
},
};

const repoDir = path.join(testDataDir, 'directory', 'clientsWithOidcLogout');
createDir(repoDir, files);

const config = {
AUTH0_INPUT_FILE: repoDir,
};
const context = new Context(config, mockMgmtClient());
await context.loadAssetsFromLocal();

const target = [
{
app_type: 'regular_web',
name: 'oidcLogoutClient',
oidc_logout: {
backchannel_logout_urls: ['https://example.com/logout'],
backchannel_logout_initiators: {
mode: 'custom',
selected_initiators: ['rp-logout', 'idp-logout'],
},
backchannel_logout_session_metadata: {
include: true,
},
},
},
{ app_type: 'spa', name: 'simpleClient' },
];
expect(context.assets.clients).to.deep.equal(target);
});

it('should dump clients with oidc_logout', async () => {
const dir = path.join(testDataDir, 'directory', 'clientsOidcLogoutDump');
cleanThenMkdir(dir);
const context = new Context({ AUTH0_INPUT_FILE: dir }, mockMgmtClient());

context.assets.clients = [
{
name: 'oidcLogoutClient',
app_type: 'regular_web',
oidc_logout: {
backchannel_logout_urls: ['https://example.com/logout'],
backchannel_logout_initiators: {
mode: 'custom',
selected_initiators: ['rp-logout', 'idp-logout'],
},
backchannel_logout_session_metadata: {
include: true,
},
},
},
];

await handler.dump(context);

const dumpedClient = loadJSON(path.join(dir, 'clients', 'oidcLogoutClient.json'));
expect(dumpedClient).to.deep.equal({
name: 'oidcLogoutClient',
app_type: 'regular_web',
oidc_logout: {
backchannel_logout_urls: ['https://example.com/logout'],
backchannel_logout_initiators: {
mode: 'custom',
selected_initiators: ['rp-logout', 'idp-logout'],
},
backchannel_logout_session_metadata: {
include: true,
},
},
});
});
});
54 changes: 54 additions & 0 deletions test/context/yaml/clients.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,58 @@ describe('#YAML context clients', () => {

expect(context.assets.clients).to.deep.equal(target);
});

it('should process clients with oidc_logout', async () => {
const dir = path.join(testDataDir, 'yaml', 'clientsWithOidcLogout');
cleanThenMkdir(dir);

const yaml = `
clients:
-
name: "oidcLogoutClient"
app_type: "regular_web"
oidc_logout:
backchannel_logout_urls: ['https://example.com/logout']
backchannel_logout_initiators:
mode: 'custom'
selected_initiators: ['rp-logout', 'idp-logout']
backchannel_logout_session_metadata:
include: true
-
name: "simpleClient"
app_type: "spa"
`;

const target = [
{
name: 'oidcLogoutClient',
app_type: 'regular_web',
oidc_logout: {
backchannel_logout_urls: ['https://example.com/logout'],
backchannel_logout_initiators: {
mode: 'custom',
selected_initiators: ['rp-logout', 'idp-logout'],
},
backchannel_logout_session_metadata: {
include: true,
},
},
},
{
name: 'simpleClient',
app_type: 'spa',
},
];

const yamlFile = path.join(dir, 'clients.yaml');
fs.writeFileSync(yamlFile, yaml);

const config = {
AUTH0_INPUT_FILE: yamlFile,
};
const context = new Context(config, mockMgmtClient());
await context.loadAssetsFromLocal();

expect(context.assets.clients).to.deep.equal(target);
});
});
Loading