From 5509ff24e4fa3e6bbed80265aee0ab2a5e95091c Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Fri, 10 Mar 2023 15:13:45 +0000 Subject: [PATCH 1/3] Always honor auth0Logout config on v1 --- src/auth0-session/client.ts | 10 +++-- src/auth0-session/config.ts | 2 +- src/auth0-session/get-config.ts | 2 +- src/config.ts | 2 +- tests/auth0-session/client.test.ts | 60 ++++++++++++++++++++++++++++++ tests/auth0-session/config.test.ts | 38 +++++++++---------- tests/handlers/logout.test.ts | 32 +++++++++++++--- 7 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/auth0-session/client.ts b/src/auth0-session/client.ts index 7658503d6..0b8db8355 100644 --- a/src/auth0-session/client.ts +++ b/src/auth0-session/client.ts @@ -113,8 +113,12 @@ export default function get(config: Config, { name, version }: Telemetry): Clien applyHttpOptionsCustom(client); 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 parsedUrl = url.parse(urlJoin(issuer.metadata.issuer, '/v2/logout')); @@ -125,7 +129,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 a6d49c109..cbe5d8582 100644 --- a/src/auth0-session/config.ts +++ b/src/auth0-session/config.ts @@ -20,7 +20,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 691f030be..0be195da9 100644 --- a/src/auth0-session/get-config.ts +++ b/src/auth0-session/get-config.ts @@ -61,7 +61,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 ed7f14cdc..0a00c3dd0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,7 +23,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. diff --git a/tests/auth0-session/client.test.ts b/tests/auth0-session/client.test.ts index e5b73c4aa..8c745c4ad 100644 --- a/tests/auth0-session/client.test.ts +++ b/tests/auth0-session/client.test.ts @@ -100,6 +100,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'); + }); + + 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?returnTo=&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?returnTo=&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?returnTo=&client_id=__test_client_id__'); + }); + + it('should not 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'); + }); + + 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 710dc57f0..c02caebd9 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', () => { @@ -229,7 +225,7 @@ describe('Config', () => { ...defaultConfig, session: { rolling: true, - rollingDuration: (false as unknown) as undefined // testing invalid configuration + rollingDuration: false as unknown as undefined // testing invalid configuration } }) ).toThrow('"session.rollingDuration" must be provided an integer value when "session.rolling" is true'); @@ -251,7 +247,7 @@ describe('Config', () => { expect(() => getConfig({ ...defaultConfig, - secret: ({ key: '__test_session_secret__' } as unknown) as string // testing invalid configuration + secret: { key: '__test_session_secret__' } as unknown as string // testing invalid configuration }) ).toThrow('"secret" must be one of [string, binary, array]'); }); @@ -262,7 +258,7 @@ describe('Config', () => { ...defaultConfig, session: { cookie: { - httpOnly: ('__invalid_httponly__' as unknown) as boolean // testing invalid configuration + httpOnly: '__invalid_httponly__' as unknown as boolean // testing invalid configuration } } }) @@ -276,7 +272,7 @@ describe('Config', () => { secret: '__test_session_secret__', session: { cookie: { - secure: ('__invalid_secure__' as unknown) as boolean // testing invalid configuration + secure: '__invalid_secure__' as unknown as boolean // testing invalid configuration } } }) @@ -290,7 +286,7 @@ describe('Config', () => { secret: '__test_session_secret__', session: { cookie: { - sameSite: ('__invalid_samesite__' as unknown) as any // testing invalid configuration + sameSite: '__invalid_samesite__' as unknown as any // testing invalid configuration } } }) @@ -304,7 +300,7 @@ describe('Config', () => { secret: '__test_session_secret__', session: { cookie: { - domain: (false as unknown) as string // testing invalid configuration + domain: false as unknown as string // testing invalid configuration } } }) @@ -399,7 +395,7 @@ describe('Config', () => { }); it('should not allow empty scope', () => { - expect(() => validateAuthorizationParams({ scope: (null as unknown) as undefined })).toThrowError( + expect(() => validateAuthorizationParams({ scope: null as unknown as undefined })).toThrowError( new TypeError('"authorizationParams.scope" must be a string') ); expect(() => validateAuthorizationParams({ scope: '' })).toThrowError( @@ -420,10 +416,10 @@ describe('Config', () => { }); it('should not allow empty response_type', () => { - expect(() => validateAuthorizationParams({ response_type: (null as unknown) as undefined })).toThrowError( + expect(() => validateAuthorizationParams({ response_type: null as unknown as undefined })).toThrowError( new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]') ); - expect(() => validateAuthorizationParams({ response_type: ('' as unknown) as undefined })).toThrowError( + expect(() => validateAuthorizationParams({ response_type: '' as unknown as undefined })).toThrowError( new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]') ); }); @@ -452,16 +448,16 @@ describe('Config', () => { }); it('should not allow empty response_mode', () => { - expect(() => validateAuthorizationParams({ response_mode: (null as unknown) as undefined })).toThrowError( + expect(() => validateAuthorizationParams({ response_mode: null as unknown as undefined })).toThrowError( new TypeError('"authorizationParams.response_mode" must be [form_post]') ); - expect(() => validateAuthorizationParams({ response_mode: ('' as unknown) as undefined })).toThrowError( + expect(() => validateAuthorizationParams({ response_mode: '' as unknown as undefined })).toThrowError( new TypeError('"authorizationParams.response_mode" must be [form_post]') ); expect(() => validateAuthorizationParams({ response_type: 'code', - response_mode: ('' as unknown) as undefined + response_mode: '' as unknown as undefined }) ).toThrowError(new TypeError('"authorizationParams.response_mode" must be one of [query, form_post]')); }); diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts index 111dfc63e..7bef31e03 100644 --- a/tests/handlers/logout.test.ts +++ b/tests/handlers/logout.test.ts @@ -16,7 +16,7 @@ jest.mock('../../src/utils/assert', () => ({ describe('logout handler', () => { afterEach(teardown); - test('should redirect to the identity provider', async () => { + test('should redirect to the auth0', async () => { const baseUrl = await setup(withoutApi); const cookieJar = await login(baseUrl); @@ -59,10 +59,13 @@ describe('logout handler', () => { }); }); - test('should use end_session_endpoint if available', async () => { - const baseUrl = await setup(withoutApi, { - discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' } - }); + 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 { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, { @@ -79,6 +82,25 @@ describe('logout handler', () => { }); }); + 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' } + }); + const cookieJar = await login(baseUrl); + const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, { + redirect: 'manual', + headers: { + cookie: cookieJar.getCookieStringSync(baseUrl) + } + }); + + expect(status).toBe(302); + expect(parseUrl(headers.get('location') as string)).toMatchObject({ + host: 'acme.auth0.local', + pathname: '/v2/logout' + }); + }); + test('should delete the session', async () => { const baseUrl = await setup(withoutApi, { discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' } From 517764a85f3f037b7bfe97d663319201324fb9f6 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Fri, 10 Mar 2023 12:24:42 -0300 Subject: [PATCH 2/3] Update tests/handlers/logout.test.ts --- tests/handlers/logout.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/handlers/logout.test.ts b/tests/handlers/logout.test.ts index 7bef31e03..bfd83443e 100644 --- a/tests/handlers/logout.test.ts +++ b/tests/handlers/logout.test.ts @@ -16,7 +16,7 @@ jest.mock('../../src/utils/assert', () => ({ describe('logout handler', () => { afterEach(teardown); - test('should redirect to the auth0', async () => { + test('should redirect to auth0', async () => { const baseUrl = await setup(withoutApi); const cookieJar = await login(baseUrl); From 0c7993a03ab2f6e3cbbd5d949d633526fe15820a Mon Sep 17 00:00:00 2001 From: Adam Mcgrath Date: Fri, 10 Mar 2023 16:07:13 +0000 Subject: [PATCH 3/3] Update tests/auth0-session/client.test.ts --- tests/auth0-session/client.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/auth0-session/client.test.ts b/tests/auth0-session/client.test.ts index 8c745c4ad..e66fe74f7 100644 --- a/tests/auth0-session/client.test.ts +++ b/tests/auth0-session/client.test.ts @@ -131,7 +131,7 @@ describe('clientFactory', function () { expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/v2/logout?returnTo=&client_id=__test_client_id__'); }); - it('should not use discovered logout endpoint if domain is auth0.com but configured with auth0logout false', async function () { + 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, {