diff --git a/src/__tests__/core/web_api/__snapshots__/p2_api.test.js.snap b/src/__tests__/core/web_api/__snapshots__/p2_api.test.js.snap index e8b4fb5bb..ffbd61a1f 100644 --- a/src/__tests__/core/web_api/__snapshots__/p2_api.test.js.snap +++ b/src/__tests__/core/web_api/__snapshots__/p2_api.test.js.snap @@ -200,42 +200,6 @@ exports[`Auth0APIClient logIn with social/enterprise (without username and email } `; -exports[`Auth0APIClient parseHash should pass __enableIdPInitiatedLogin when options._enableIdPInitiatedLogin===true 1`] = ` -[ - { - "__enableIdPInitiatedLogin": true, - "hash": "hash", - "nonce": undefined, - "state": undefined, - }, - "cb", -] -`; - -exports[`Auth0APIClient parseHash should pass __enableIdPInitiatedLogin when options._enableImpersonation===true 1`] = ` -[ - { - "__enableIdPInitiatedLogin": true, - "hash": "hash", - "nonce": undefined, - "state": undefined, - }, - "cb", -] -`; - -exports[`Auth0APIClient parseHash should pass __enableIdPInitiatedLogin:false when options._enableImpersonation and options._enableIdPInitiatedLogin are not present 1`] = ` -[ - { - "__enableIdPInitiatedLogin": false, - "hash": "hash", - "nonce": undefined, - "state": undefined, - }, - "cb", -] -`; - exports[`Auth0APIClient passwordlessStart should call client.passwordlessStart 1`] = ` [ { diff --git a/src/__tests__/core/web_api/p2_api.test.js b/src/__tests__/core/web_api/p2_api.test.js index 84e9af951..91e549aa0 100644 --- a/src/__tests__/core/web_api/p2_api.test.js +++ b/src/__tests__/core/web_api/p2_api.test.js @@ -25,6 +25,7 @@ const getClient = (options = {}) => { }; const getAuth0ClientMock = () => require('auth0-js'); +const getParseHashMock = () => getAuth0ClientMock().WebAuth.mock.instances[0].parseHash.mock; const assertCallWithCallback = (mock, callbackFunction) => { expect(mock.calls.length).toBe(1); expect(mock.calls[0][0]).toMatchSnapshot(); @@ -570,33 +571,172 @@ describe('Auth0APIClient', () => { expect(mock.calls[0]).toMatchSnapshot(); }); describe('parseHash', () => { - it('should pass __enableIdPInitiatedLogin:false when options._enableImpersonation and options._enableIdPInitiatedLogin are not present', () => { + const assertParseHashOptions = ( + options, + { hash = 'hash', nonce = undefined, state = undefined } = {} + ) => { + expect(options).toEqual({ + hash, + nonce, + state + }); + expect(options).not.toHaveProperty('__enableIdPInitiatedLogin'); + }; + + const getOnlyParseHashCall = () => { + const parseHashMock = getParseHashMock(); + expect(parseHashMock.calls.length).toBe(1); + return parseHashMock.calls[0]; + }; + + it('should omit __enableIdPInitiatedLogin when private options are not present', () => { const client = getClient({}); client.parseHash('hash', 'cb'); - const mock = getAuth0ClientMock(); - const parseHashMock = mock.WebAuth.mock.instances[0].parseHash.mock; - expect(parseHashMock.calls.length).toBe(1); - expect(parseHashMock.calls[0]).toMatchSnapshot(); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); }); - it('should pass __enableIdPInitiatedLogin when options._enableImpersonation===true', () => { + it('should omit __enableIdPInitiatedLogin when options._enableImpersonation===true', () => { const client = getClient({ _enableImpersonation: true }); client.parseHash('hash', 'cb'); - const mock = getAuth0ClientMock(); - const parseHashMock = mock.WebAuth.mock.instances[0].parseHash.mock; - expect(parseHashMock.calls.length).toBe(1); - expect(parseHashMock.calls[0]).toMatchSnapshot(); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); }); - it('should pass __enableIdPInitiatedLogin when options._enableIdPInitiatedLogin===true', () => { + it('should omit __enableIdPInitiatedLogin when options._enableIdPInitiatedLogin===true', () => { const client = getClient({ _enableIdPInitiatedLogin: true }); client.parseHash('hash', 'cb'); - const mock = getAuth0ClientMock(); - const parseHashMock = mock.WebAuth.mock.instances[0].parseHash.mock; - expect(parseHashMock.calls.length).toBe(1); - expect(parseHashMock.calls[0]).toMatchSnapshot(); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); + }); + it('should omit __enableIdPInitiatedLogin when both private options are true', () => { + const client = getClient({ + _enableImpersonation: true, + _enableIdPInitiatedLogin: true + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); + }); + it('should omit __enableIdPInitiatedLogin when private options are truthy non-booleans', () => { + const client = getClient({ + _enableImpersonation: 'true', + _enableIdPInitiatedLogin: 1 + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); + }); + it('should not forward a root __enableIdPInitiatedLogin constructor option', () => { + const client = getClient({ + __enableIdPInitiatedLogin: true + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); + }); + it('should not forward a params.__enableIdPInitiatedLogin constructor option', () => { + const client = getClient({ + params: { + __enableIdPInitiatedLogin: true + } + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions); + expect(cb).toBe('cb'); + }); + it('should pass an empty string hash when parseHash is called without a hash', () => { + const client = getClient({}); + client.parseHash(undefined, 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { hash: '' }); + expect(cb).toBe('cb'); + }); + it('should pass unsolicited token hashes without the IdP initiated bypass flag', () => { + const hash = + '#id_token=ATTACKER_ID_TOKEN&access_token=ATTACKER_ACCESS_TOKEN&token_type=Bearer'; + const client = getClient({ + _enableImpersonation: true, + _enableIdPInitiatedLogin: true + }); + client.parseHash(hash, 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { hash }); + expect(cb).toBe('cb'); + }); + it('should pass OAuth error hashes without the IdP initiated bypass flag', () => { + const hash = '#error=access_denied&error_description=Unauthorized'; + const client = getClient({ + _enableIdPInitiatedLogin: true + }); + client.parseHash(hash, 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { hash }); + expect(cb).toBe('cb'); + }); + it('should pass hashes with no auth result without the IdP initiated bypass flag', () => { + const hash = '#foo=bar'; + const client = getClient({ + _enableImpersonation: true + }); + client.parseHash(hash, 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { hash }); + expect(cb).toBe('cb'); + }); + it('should pass configured nonce and state', () => { + const client = getClient({ + nonce: 'nonce', + state: 'state' + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { + nonce: 'nonce', + state: 'state' + }); + expect(cb).toBe('cb'); + }); + it('should pass configured params.nonce and params.state', () => { + const client = getClient({ + params: { + nonce: 'params-nonce', + state: 'params-state' + } + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { + nonce: 'params-nonce', + state: 'params-state' + }); + expect(cb).toBe('cb'); + }); + it('should prefer params.nonce and params.state over root nonce and state', () => { + const client = getClient({ + nonce: 'root-nonce', + state: 'root-state', + params: { + nonce: 'params-nonce', + state: 'params-state' + } + }); + client.parseHash('hash', 'cb'); + const [parseOptions, cb] = getOnlyParseHashCall(); + assertParseHashOptions(parseOptions, { + nonce: 'params-nonce', + state: 'params-state' + }); + expect(cb).toBe('cb'); }); }); }); diff --git a/src/core/web_api/p2_api.js b/src/core/web_api/p2_api.js index 2c9f3e7cc..4f34864bc 100644 --- a/src/core/web_api/p2_api.js +++ b/src/core/web_api/p2_api.js @@ -21,7 +21,6 @@ class Auth0APIClient { this.authOpt = null; this.domain = domain; this.isUniversalLogin = window.location.host === domain; - this._enableIdPInitiatedLogin = !!(opts._enableIdPInitiatedLogin || opts._enableImpersonation); const telemetry = this.getTelemetryInfo(opts._telemetryInfo); var state = opts.state; @@ -174,7 +173,6 @@ class Auth0APIClient { parseHash(hash = '', cb) { return this.client.parseHash( { - __enableIdPInitiatedLogin: this._enableIdPInitiatedLogin, hash, nonce: this.authOpt.nonce, state: this.authOpt.state diff --git a/types/auth0-lock-tests.ts b/types/auth0-lock-tests.ts index 7bca887e2..a05fa303b 100644 --- a/types/auth0-lock-tests.ts +++ b/types/auth0-lock-tests.ts @@ -160,8 +160,6 @@ const allOptions: Auth0LockConstructorOptions = { theme: { primaryColor: "#ea5323" }, useCustomPasswordlessConnection: false, usernameStyle: "username", - _enableImpersonation: false, - _enableIdPInitiatedLogin: false, _sendTelemetry: true, _telemetryInfo: { name: "my-sdk", version: "1.0.0", env: { "auth0.js": "9.0.0" } }, __useTenantInfo: false, @@ -169,6 +167,12 @@ const allOptions: Auth0LockConstructorOptions = { new Auth0Lock(CLIENT_ID, DOMAIN, allOptions); +// Private state-validation bypass options must not be public constructor options. +// @ts-expect-error +new Auth0Lock(CLIENT_ID, DOMAIN, { _enableImpersonation: true }); +// @ts-expect-error +new Auth0Lock(CLIENT_ID, DOMAIN, { _enableIdPInitiatedLogin: true }); + // Passwordless new Auth0LockPasswordless(CLIENT_ID, DOMAIN); diff --git a/types/index.d.ts b/types/index.d.ts index fee119361..483ec4762 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -178,8 +178,6 @@ interface Auth0LockConstructorOptions { theme?: Auth0LockThemeOptions | undefined; useCustomPasswordlessConnection?: boolean | undefined; usernameStyle?: "email" | "username" | undefined; - _enableImpersonation?: boolean | undefined; - _enableIdPInitiatedLogin?: boolean | undefined; _sendTelemetry?: boolean | undefined; _telemetryInfo?: { name?: string | undefined; version?: string | undefined; env?: Record | undefined } | undefined; __useTenantInfo?: boolean | undefined;