diff --git a/examples/directory/clients/My Native app.json b/examples/directory/clients/My Native app.json index b18167f8..e1c9de94 100644 --- a/examples/directory/clients/My Native app.json +++ b/examples/directory/clients/My Native app.json @@ -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 } } } diff --git a/examples/yaml/tenant.yaml b/examples/yaml/tenant.yaml index d74af510..f61ee265 100644 --- a/examples/yaml/tenant.yaml +++ b/examples/yaml/tenant.yaml @@ -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" diff --git a/src/tools/auth0/handlers/clients.ts b/src/tools/auth0/handlers/clients.ts index 6ad39375..f9ac359e 100644 --- a/src/tools/auth0/handlers/clients.ts +++ b/src/tools/auth0/handlers/clients.ts @@ -277,6 +277,64 @@ 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'], }, diff --git a/test/context/directory/clients.test.js b/test/context/directory/clients.test.js index 4cc32285..2a9cb1bb 100644 --- a/test/context/directory/clients.test.js +++ b/test/context/directory/clients.test.js @@ -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, + }, + }, + }); + }); }); diff --git a/test/context/yaml/clients.test.js b/test/context/yaml/clients.test.js index b42ec82f..230ec82a 100644 --- a/test/context/yaml/clients.test.js +++ b/test/context/yaml/clients.test.js @@ -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); + }); }); diff --git a/test/tools/auth0/handlers/clients.tests.js b/test/tools/auth0/handlers/clients.tests.js index 044477df..ec3ec2f8 100644 --- a/test/tools/auth0/handlers/clients.tests.js +++ b/test/tools/auth0/handlers/clients.tests.js @@ -1199,5 +1199,122 @@ describe('#clients handler', () => { expect(newOnlyClient).to.not.have.property('cross_origin_auth'); expect(newOnlyClient.cross_origin_authentication).to.equal(false); }); + + it('should create client with oidc_logout configuration', async () => { + const clientWithOidcLogout = { + name: 'My Client with OIDC Logout', + 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, + }, + }, + }; + + const auth0 = { + clients: { + create: function (data) { + (() => expect(this).to.not.be.undefined)(); + expect(data).to.be.an('object'); + expect(data.name).to.equal('My Client with OIDC Logout'); + expect(data.oidc_logout).to.deep.equal({ + backchannel_logout_urls: ['https://example.com/logout'], + backchannel_logout_initiators: { + mode: 'custom', + selected_initiators: ['rp-logout', 'idp-logout'], + }, + backchannel_logout_session_metadata: { + include: true, + }, + }); + 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: [clientWithOidcLogout] }]); + }); + + it('should update client with oidc_logout configuration', async () => { + const auth0 = { + clients: { + create: () => Promise.resolve({ data: [] }), + update: function (client_id, data) { + (() => expect(this).to.not.be.undefined)(); + expect(client_id).to.equal('client1'); + expect(data.oidc_logout).to.deep.equal({ + backchannel_logout_urls: ['https://new-example.com/logout'], + backchannel_logout_initiators: { + mode: 'all', + selected_initiators: [], + }, + backchannel_logout_session_metadata: { + include: false, + }, + }); + return Promise.resolve({ data }); + }, + delete: () => Promise.resolve({ data: [] }), + list: (params) => + mockPagedData(params, 'clients', [ + { + client_id: 'client1', + name: 'My Client', + oidc_logout: { + backchannel_logout_urls: ['https://example.com/logout'], + backchannel_logout_initiators: { + mode: 'custom', + selected_initiators: ['rp-logout'], + }, + }, + }, + ]), + }, + 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: [ + { + name: 'My Client', + oidc_logout: { + backchannel_logout_urls: ['https://new-example.com/logout'], + backchannel_logout_initiators: { + mode: 'all', + selected_initiators: [], + }, + backchannel_logout_session_metadata: { + include: false, + }, + }, + }, + ], + }, + ]); + }); }); });