Repository: auth0/nextjs-auth0
Pull request #1104 (2023-03-10 12:24:49)

[SDK-3887] Always honor auth0Logout config
See https://github.com/auth0/express-openid-connect/pull/447

### Description

The OIDC RP Initiated Logout endpoint is incompatible with Auth0's proprietary logout. Make sure this SDK does not use it if `auth0Logout` is configured and an `end_session_endpoint` is Discovered in the OIDC Discovery document.

### Testing

If `auth0Logout` is true -> use v2/logout regardless of discovery
If `auth0Logout` is false -> use discovered endpoint or nothing
If `auth0Logout` is not set -> use v2/logout regardless of discovery (this is different from express as this is an auth0 SDK)

### Checklist

- [ ] I have added documentation for new/changed functionality in this PR or in auth0.com/docs
- [ ] All active GitHub checks for tests, formatting, and security are passing
- [ ] The correct base branch is being used, if not the default branch


Patch:
diff --git a/src/auth0-session/client.ts b/src/auth0-session/client.ts
index 48106049..d0ce9f32 100644
--- a/src/auth0-session/client.ts
+++ b/src/auth0-session/client.ts
@@ -109,8 +109,12 @@ export default function get(config: Config, { name, version }: Telemetry): Clien
     );
     client[custom.clock_tolerance] = config.clockTolerance;
 
-    if (config.idpLogout && !issuer.end_session_endpoint) {
-      if (config.auth0Logout || (url.parse(issuer.metadata.issuer).hostname as string).match('\\.auth0\\.com$')) {
+    if (config.idpLogout) {
+      if (
+        config.auth0Logout ||
+        ((url.parse(issuer.metadata.issuer).hostname as string).match('\\.auth0\\.com$') &&
+          config.auth0Logout !== false)
+      ) {
         Object.defineProperty(client, 'endSessionUrl', {
           value(params: EndSessionParameters) {
             const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params;
@@ -128,7 +132,7 @@ export default function get(config: Config, { name, version }: Telemetry): Clien
             return url.format(parsedUrl);
           }
         });
-      } else {
+      } else if (!issuer.end_session_endpoint) {
         debug('the issuer does not support RP-Initiated Logout');
       }
     }
diff --git a/src/auth0-session/config.ts b/src/auth0-session/config.ts
index df45f82d..e0dd4394 100644
--- a/src/auth0-session/config.ts
+++ b/src/auth0-session/config.ts
@@ -21,7 +21,7 @@ export interface Config {
   /**
    * Boolean value to enable Auth0's logout feature.
    */
-  auth0Logout: boolean;
+  auth0Logout?: boolean;
 
   /**
    * URL parameters used when redirecting users to the authorization server to log in.
diff --git a/src/auth0-session/get-config.ts b/src/auth0-session/get-config.ts
index 488f49ac..3defc218 100644
--- a/src/auth0-session/get-config.ts
+++ b/src/auth0-session/get-config.ts
@@ -53,7 +53,7 @@ const paramsSchema = Joi.object({
   })
     .default()
     .unknown(false),
-  auth0Logout: Joi.boolean().optional().default(false),
+  auth0Logout: Joi.boolean().optional(),
   authorizationParams: Joi.object({
     response_type: Joi.string().optional().valid('id_token', 'code id_token', 'code').default('id_token'),
     scope: Joi.string()
diff --git a/src/config.ts b/src/config.ts
index 0e86aa37..8e97584b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -27,7 +27,7 @@ export interface BaseConfig {
    * Boolean value to enable Auth0's proprietary logout feature.
    * Since this SDK is for Auth0, it's set to `true`by default.
    */
-  auth0Logout: boolean;
+  auth0Logout?: boolean;
 
   /**
    *  URL parameters used when redirecting users to the authorization server to log in.


Test patch:
diff --git a/tests/auth0-session/client.test.ts b/tests/auth0-session/client.test.ts
index b35c8570..cf925fd4 100644
--- a/tests/auth0-session/client.test.ts
+++ b/tests/auth0-session/client.test.ts
@@ -105,6 +105,66 @@ describe('clientFactory', function () {
     expect(client.id_token_signed_response_alg).toEqual('RS256');
   });
 
+  it('should use discovered logout endpoint by default', async function () {
+    const client = await getClient({ ...defaultConfig, idpLogout: true });
+    expect(client.endSessionUrl({})).toEqual('https://op.example.com/session/end?client_id=__test_client_id__');
+  });
+
+  it('should use auth0 logout endpoint if configured', async function () {
+    const client = await getClient({ ...defaultConfig, idpLogout: true, auth0Logout: true });
+    expect(client.endSessionUrl({})).toEqual('https://op.example.com/v2/logout?client_id=__test_client_id__');
+  });
+
+  it('should use auth0 logout endpoint if domain is auth0.com', async function () {
+    nock('https://foo.auth0.com')
+      .get('/.well-known/openid-configuration')
+      .reply(200, { ...wellKnown, issuer: 'https://foo.auth0.com/' });
+    const client = await getClient({ ...defaultConfig, idpLogout: true, issuerBaseURL: 'https://foo.auth0.com' });
+    expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/v2/logout?client_id=__test_client_id__');
+  });
+
+  it('should use auth0 logout endpoint if domain is auth0.com and configured', async function () {
+    nock('https://foo.auth0.com')
+      .get('/.well-known/openid-configuration')
+      .reply(200, { ...wellKnown, issuer: 'https://foo.auth0.com/' });
+    const client = await getClient({
+      ...defaultConfig,
+      issuerBaseURL: 'https://foo.auth0.com',
+      idpLogout: true,
+      auth0Logout: true
+    });
+    expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/v2/logout?client_id=__test_client_id__');
+  });
+
+  it('should use discovered logout endpoint if domain is auth0.com but configured with auth0logout false', async function () {
+    nock('https://foo.auth0.com')
+      .get('/.well-known/openid-configuration')
+      .reply(200, {
+        ...wellKnown,
+        issuer: 'https://foo.auth0.com/',
+        end_session_endpoint: 'https://foo.auth0.com/oidc/logout'
+      });
+    const client = await getClient({
+      ...defaultConfig,
+      issuerBaseURL: 'https://foo.auth0.com',
+      idpLogout: true,
+      auth0Logout: false
+    });
+    expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/oidc/logout?client_id=__test_client_id__');
+  });
+
+  it('should create client with no end_session_endpoint', async function () {
+    nock('https://op2.example.com')
+      .get('/.well-known/openid-configuration')
+      .reply(200, {
+        ...wellKnown,
+        issuer: 'https://op2.example.com',
+        end_session_endpoint: undefined
+      });
+    const client = await getClient({ ...defaultConfig, issuerBaseURL: 'https://op2.example.com' });
+    expect(() => client.endSessionUrl({})).toThrowError();
+  });
+
   it('should create custom logout for auth0', async function () {
     nock('https://test.eu.auth0.com')
       .get('/.well-known/openid-configuration')
diff --git a/tests/auth0-session/config.test.ts b/tests/auth0-session/config.test.ts
index ff9a26f2..252018ee 100644
--- a/tests/auth0-session/config.test.ts
+++ b/tests/auth0-session/config.test.ts
@@ -64,12 +64,10 @@ describe('Config', () => {
     });
   });
 
-  it('auth0Logout and idpLogout should default to false', () => {
+  it('auth0Logout should default to undefined and idpLogout should default to false', () => {
     const config = getConfig(defaultConfig);
-    expect(config).toMatchObject({
-      auth0Logout: false,
-      idpLogout: false
-    });
+    expect(config.idpLogout).toBe(false);
+    expect(config.auth0Logout).toBeUndefined();
   });
 
   it('should not set auth0Logout to true when idpLogout is true', () => {
@@ -77,10 +75,8 @@ describe('Config', () => {
       ...defaultConfig,
       idpLogout: true
     });
-    expect(config).toMatchObject({
-      auth0Logout: false,
-      idpLogout: true
-    });
+    expect(config.idpLogout).toBe(true);
+    expect(config.auth0Logout).toBeUndefined();
   });
 
   it('should set default route paths', () => {
diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts
index c8e30dd5..b043e7df 100644
--- a/tests/handlers/logout.test.ts
+++ b/tests/handlers/logout.test.ts
@@ -7,7 +7,7 @@ import { setup, teardown, login } from '../fixtures/setup';
 describe('logout handler', () => {
   afterEach(teardown);
 
-  test('should redirect to the identity provider', async () => {
+  test('should redirect to auth0', async () => {
     const baseUrl = await setup(withoutApi);
     const cookieJar = await login(baseUrl);
 
@@ -30,7 +30,7 @@ describe('logout handler', () => {
     });
   });
 
-  test('should pass logout params to the identity provider', async () => {
+  test('should pass logout params to auth0', async () => {
     const baseUrl = await setup(withoutApi, { logoutOptions: { logoutParams: { foo: 'bar' } } });
     const cookieJar = await login(baseUrl);
 
@@ -74,7 +74,30 @@ describe('logout handler', () => {
     });
   });
 
-  test('should use end_session_endpoint if available', async () => {
+  test('should use end_session_endpoint when configured', async () => {
+    const baseUrl = await setup(
+      { ...withoutApi, auth0Logout: false },
+      {
+        discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
+      }
+    );
+    const cookieJar = await login(baseUrl);
+
+    const {
+      res: { statusCode, headers }
+    } = await get(baseUrl, '/api/auth/logout', {
+      cookieJar,
+      fullResponse: true
+    });
+
+    expect(statusCode).toBe(302);
+    expect(parseUrl(headers['location'])).toMatchObject({
+      host: 'my-end-session-endpoint',
+      pathname: '/logout'
+    });
+  });
+
+  test('should use auth0 logout by default even when end_session_endpoint is discovered', async () => {
     const baseUrl = await setup(withoutApi, {
       discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
     });
@@ -89,8 +112,8 @@ describe('logout handler', () => {
 
     expect(statusCode).toBe(302);
     expect(parseUrl(headers['location'])).toMatchObject({
-      host: 'my-end-session-endpoint',
-      pathname: '/logout'
+      host: 'acme.auth0.local',
+      pathname: '/v2/logout'
     });
   });