diff --git a/package-lock.json b/package-lock.json index 84f80fff4..809b9e79e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "auth0-lock", - "version": "12.5.0", + "version": "12.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "auth0-lock", - "version": "12.5.0", + "version": "12.5.1", "license": "MIT", "dependencies": { - "auth0-js": "^9.26.0", + "auth0-js": "^9.27.0", "auth0-password-policies": "^1.0.2", "blueimp-md5": "^2.19.0", "classnames": "^2.3.2", - "dompurify": "^2.3.12", + "dompurify": "^2.5.4", "immutable": "^3.7.6", "jsonp": "^0.2.1", "password-sheriff": "^1.1.1", @@ -4789,9 +4789,10 @@ } }, "node_modules/auth0-js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.26.0.tgz", - "integrity": "sha512-uq+CTtL0p4Pm05N7vZhTHE1prkNt4RfQ4BJqwaczNC6NTG82MOhR3emYopYjw1SJ2Nem6wiV8Ix1TEAM/whaKA==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.27.0.tgz", + "integrity": "sha512-v0qcPulkG/ezsCarJO9tDi3KeYAaEhVz84goq4x2dyk7NWs9JeGnvaT3eSehN11fRkoFXaQByMntEYVD9BqxPg==", + "license": "MIT", "dependencies": { "base64-js": "^1.5.1", "idtoken-verifier": "^2.2.4", @@ -7942,9 +7943,10 @@ } }, "node_modules/dompurify": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.2.tgz", - "integrity": "sha512-5vSyvxRAb45EoWwAktUT3AYqAwXK4FL7si22Cgj46U6ICsj/YJczCN+Bk7WNABIQmpWRymGfslMhrRUZkQNnqA==" + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.4.tgz", + "integrity": "sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA==", + "license": "(MPL-2.0 OR Apache-2.0)" }, "node_modules/domutils": { "version": "3.1.0", diff --git a/package.json b/package.json index 3832d32e6..582269313 100644 --- a/package.json +++ b/package.json @@ -121,11 +121,11 @@ "webpack-dev-server": "^4.11.1" }, "dependencies": { - "auth0-js": "^9.26.0", + "auth0-js": "^9.27.0", "auth0-password-policies": "^1.0.2", "blueimp-md5": "^2.19.0", "classnames": "^2.3.2", - "dompurify": "^2.3.12", + "dompurify": "^2.5.4", "immutable": "^3.7.6", "jsonp": "^0.2.1", "password-sheriff": "^1.1.1", diff --git a/src/__tests__/engine/classic/__snapshots__/sign_up_pane.test.jsx.snap b/src/__tests__/engine/classic/__snapshots__/sign_up_pane.test.jsx.snap index 6c2c15ed9..a24a007d7 100644 --- a/src/__tests__/engine/classic/__snapshots__/sign_up_pane.test.jsx.snap +++ b/src/__tests__/engine/classic/__snapshots__/sign_up_pane.test.jsx.snap @@ -214,6 +214,7 @@ exports[`SignUpPane shows the Captcha pane 1`] = ` /> mockComponent('email_pane')); jest.mock('field/password/password_pane', () => mockComponent('password_pane')); @@ -8,7 +9,7 @@ jest.mock('field/username/username_pane', () => mockComponent('username_pane')); jest.mock('field/custom_input', () => mockComponent('custom_input')); jest.mock('core/index', () => ({ - captcha: jest.fn() + signupCaptcha: jest.fn() })); jest.mock('engine/classic', () => ({ @@ -38,6 +39,7 @@ describe('SignUpPane', () => { str: (...keys) => keys.join(','), html: (...keys) => keys.join(',') }, + flow: Flow.SIGNUP, model: 'model', emailInputPlaceholder: 'emailInputPlaceholder', onlyEmail: true, @@ -58,7 +60,7 @@ describe('SignUpPane', () => { }); it('shows the Captcha pane', () => { - require('core/index').captcha.mockReturnValue({ + require('core/index').signupCaptcha.mockReturnValue({ get() { return true; } @@ -72,7 +74,7 @@ describe('SignUpPane', () => { }); it('hides the Captcha pane for SSO connections', () => { - require('core/index').captcha.mockReturnValue({ + require('core/index').signupCaptcha.mockReturnValue({ get() { return true; } @@ -86,7 +88,7 @@ describe('SignUpPane', () => { }); it('shows the Captcha pane for SSO (ADFS) connections', () => { - require('core/index').captcha.mockReturnValue({ + require('core/index').signupCaptcha.mockReturnValue({ get() { return true; } diff --git a/src/connection/captcha.js b/src/connection/captcha.js index 3d9e74d9a..c4880f02c 100644 --- a/src/connection/captcha.js +++ b/src/connection/captcha.js @@ -6,13 +6,14 @@ import webApi from '../core/web_api'; export const Flow = Object.freeze({ DEFAULT: 'default', + SIGNUP: 'signup', PASSWORDLESS: 'passwordless', PASSWORD_RESET: 'password_reset', }); /** * Return the captcha config object based on the type of flow. - * + * * @param {Object} m model * @param {Flow} flow Which flow the captcha is being rendered in */ @@ -21,6 +22,8 @@ export function getCaptchaConfig(m, flow) { return l.passwordResetCaptcha(m); } else if (flow === Flow.PASSWORDLESS) { return l.passwordlessCaptcha(m); + } else if (flow === Flow.SIGNUP) { + return l.signupCaptcha(m); } else { return l.captcha(m); } @@ -42,7 +45,7 @@ export function showMissingCaptcha(m, id, flow = Flow.DEFAULT) { captchaConfig.get('provider') === 'hcaptcha' || captchaConfig.get('provider') === 'auth0_v2' || captchaConfig.get('provider') === 'friendly_captcha' || - captchaConfig.get('provider') === 'arkose' + captchaConfig.get('provider') === 'arkose' ) ? 'invalid_recaptcha' : 'invalid_captcha'; const errorMessage = i18n.html(m, ['error', 'login', captchaError]); @@ -110,6 +113,15 @@ export function swapCaptcha(id, flow, wasInvalid, next) { next(); } }); + } else if (flow === Flow.SIGNUP) { + return webApi.getSignupChallenge(id, (err, newCaptcha) => { + if (!err && newCaptcha) { + swap(updateEntity, 'lock', id, l.setSignupChallenge, newCaptcha, wasInvalid); + } + if (next) { + next(); + } + }); } else { return webApi.getChallenge(id, (err, newCaptcha) => { if (!err && newCaptcha) { diff --git a/src/connection/database/actions.js b/src/connection/database/actions.js index e42948e35..a7a7965ef 100644 --- a/src/connection/database/actions.js +++ b/src/connection/database/actions.js @@ -88,9 +88,9 @@ export function signUp(id) { autoLogin: shouldAutoLogin(m) }; - const isCaptchaValid = setCaptchaParams(m, params, Flow.DEFAULT, fields); + const isCaptchaValid = setCaptchaParams(m, params, Flow.SIGNUP, fields); if (!isCaptchaValid) { - return showMissingCaptcha(m, id); + return showMissingCaptcha(m, id, Flow.SIGNUP); } if (databaseConnectionRequiresUsername(m)) { @@ -131,7 +131,7 @@ export function signUp(id) { const wasInvalidCaptcha = error && error.code === 'invalid_captcha'; - swapCaptcha(id, Flow.DEFAULT, wasInvalidCaptcha, () => { + swapCaptcha(id, Flow.SIGNUP, wasInvalidCaptcha, () => { setTimeout(() => signUpError(id, error), 250); }); }; @@ -290,7 +290,7 @@ export function resetPasswordSuccess(id) { function resetPasswordError(id, error) { const m = read(getEntity, 'lock', id); let key = error.code; - + if (error.code === 'invalid_captcha') { const captchaConfig = l.passwordResetCaptcha(m); key = ( @@ -302,7 +302,7 @@ function resetPasswordError(id, error) { const errorMessage = i18n.html(m, ['error', 'forgotPassword', key]) || i18n.html(m, ['error', 'forgotPassword', 'lock.fallback']); - + swapCaptcha(id, Flow.PASSWORD_RESET, error.code === 'invalid_captcha', () => { swap(updateEntity, 'lock', id, l.setSubmitting, false, errorMessage); }); @@ -322,11 +322,11 @@ export function showLoginActivity(id, fields = ['password']) { export function showSignUpActivity(id, fields = ['password']) { const m = read(getEntity, 'lock', id); - const captchaConfig = l.captcha(m); + const captchaConfig = l.signupCaptcha(m); if (captchaConfig && captchaConfig.get('provider') === 'arkose') { swap(updateEntity, 'lock', id, setScreen, 'signUp', fields); } else { - swapCaptcha(id, 'login', false, () => { + swapCaptcha(id, Flow.SIGNUP, false, () => { swap(updateEntity, 'lock', id, setScreen, 'signUp', fields); }); } @@ -338,7 +338,7 @@ export function showResetPasswordActivity(id, fields = ['password']) { if (captchaConfig && captchaConfig.get('provider') === 'arkose') { swap(updateEntity, 'lock', id, setScreen, 'forgotPassword', fields); } else { - swapCaptcha(id, 'login', false, () => { + swapCaptcha(id, Flow.PASSWORD_RESET, false, () => { swap(updateEntity, 'lock', id, setScreen, 'forgotPassword', fields); }); } diff --git a/src/connection/passwordless/index.js b/src/connection/passwordless/index.js index 86f923f55..7d18a7a41 100644 --- a/src/connection/passwordless/index.js +++ b/src/connection/passwordless/index.js @@ -14,6 +14,16 @@ export function initPasswordless(m, opts) { const showTerms = opts.showTerms === undefined ? true : !!opts.showTerms; m = initNS(m, Map({ send, mustAcceptTerms, showTerms })); + + m = sync(m, 'passwordlessCaptcha', { + syncFn: (m, cb) => { + webAPI.getPasswordlessChallenge(m.get('id'), (err, r) => { + cb(null, r); + }); + }, + successFn: l.setPasswordlessCaptcha + }); + if (opts.defaultLocation && typeof opts.defaultLocation === 'string') { m = initLocation(m, opts.defaultLocation.toUpperCase()); } else { diff --git a/src/core/index.js b/src/core/index.js index 817cf9c91..4afd08749 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -421,6 +421,11 @@ export function setCaptcha(m, value, wasInvalid) { return set(m, 'captcha', Immutable.fromJS(value)); } +export function setSignupChallenge(m, value, wasInvalid) { + m = captchaField.reset(m, wasInvalid); + return set(m, 'signupCaptcha', Immutable.fromJS(value)); +} + export function setPasswordlessCaptcha(m, value, wasInvalid) { m = captchaField.reset(m, wasInvalid); return set(m, 'passwordlessCaptcha', Immutable.fromJS(value)); @@ -435,6 +440,10 @@ export function captcha(m) { return get(m, 'captcha'); } +export function signupCaptcha(m) { + return get(m, 'signupCaptcha'); +} + export function passwordlessCaptcha(m) { return get(m, 'passwordlessCaptcha'); } diff --git a/src/core/remote_data.js b/src/core/remote_data.js index ef81e3319..77f0e79eb 100644 --- a/src/core/remote_data.js +++ b/src/core/remote_data.js @@ -6,7 +6,7 @@ import * as l from './index'; import { isADEnabled } from '../connection/enterprise'; // shouldn't depend on this import sync, { isSuccess } from '../sync'; import webApi from './web_api'; -import { setCaptcha, setPasswordlessCaptcha, setPasswordResetCaptcha } from '../core/index'; +import { setCaptcha } from '../core/index'; export function syncRemoteData(m) { if (l.useTenantInfo(m)) { @@ -60,24 +60,5 @@ export function syncRemoteData(m) { successFn: setCaptcha }); - m = sync(m, 'passwordlessCaptcha', { - syncFn: (m, cb) => { - webApi.getPasswordlessChallenge(m.get('id'), (err, r) => { - cb(null, r); - }); - }, - successFn: setPasswordlessCaptcha - }); - - m = sync(m, 'passwordResetCaptcha', { - syncFn: (m, cb) => { - webApi.getPasswordResetChallenge(m.get('id'), (err, r) => { - cb(null, r); - }); - }, - successFn: setPasswordResetCaptcha - }); - - return m; } diff --git a/src/core/web_api.js b/src/core/web_api.js index 4000d7d7b..e1a4300dd 100644 --- a/src/core/web_api.js +++ b/src/core/web_api.js @@ -60,6 +60,10 @@ class Auth0WebAPI { return this.clients[lockID].getChallenge(callback); } + getSignupChallenge(lockID, callback) { + return this.clients[lockID].getSignupChallenge(callback); + } + getPasswordlessChallenge(lockID, callback) { return this.clients[lockID].getPasswordlessChallenge(callback); } diff --git a/src/core/web_api/p2_api.js b/src/core/web_api/p2_api.js index 8cc5f1484..7efd5a926 100644 --- a/src/core/web_api/p2_api.js +++ b/src/core/web_api/p2_api.js @@ -195,6 +195,10 @@ class Auth0APIClient { return this.client.client.getChallenge(...params); } + getSignupChallenge(...params) { + return this.client.client.dbConnection.getSignupChallenge(...params); + } + getPasswordlessChallenge(...params) { return this.client.client.passwordless.getChallenge(...params); } diff --git a/src/engine/classic/sign_up_pane.jsx b/src/engine/classic/sign_up_pane.jsx index 621afdd9e..1911778fd 100644 --- a/src/engine/classic/sign_up_pane.jsx +++ b/src/engine/classic/sign_up_pane.jsx @@ -64,10 +64,10 @@ export default class SignUpPane extends React.Component { )); const captchaPane = - l.captcha(model) && - l.captcha(model).get('required') && + l.signupCaptcha(model) && + l.signupCaptcha(model).get('required') && (isHRDDomain(model, databaseUsernameValue(model)) || !sso) ? ( - swapCaptcha(l.id(model), Flow.DEFAULT, false)} /> + swapCaptcha(l.id(model), Flow.SIGNUP, false)} /> ) : null; const passwordPane = !onlyEmail && ( diff --git a/test/captcha_signup.test.js b/test/captcha_signup.test.js index fcbcf9bac..6622fa48d 100644 --- a/test/captcha_signup.test.js +++ b/test/captcha_signup.test.js @@ -4,8 +4,7 @@ import en from '../src/i18n/en'; const lockOpts = { allowedConnections: ['db'], - rememberLastLogin: false, - initialScreen: 'signUp' + rememberLastLogin: false }; const svgCaptchaRequiredResponse1 = { @@ -33,22 +32,26 @@ describe('captcha on signup', function () { describe('svg-captcha', () => { describe('when the api returns a new challenge', function () { beforeEach(function (done) { - this.stub = h.stubGetChallenge([svgCaptchaRequiredResponse1, svgCaptchaRequiredResponse2]); - this.lock = h.displayLock('', lockOpts, done); + this.stub = h.stubGetChallenge({ required: false }); + this.stub = h.stubGetSignupChallenge([svgCaptchaRequiredResponse1, svgCaptchaRequiredResponse2]); + this.lock = h.displayLock('', lockOpts, () => { + h.clickSignUpTab(); + h.waitUntilExists(this.lock, '.auth0-lock-with-terms', () => { + done(); + }); + }); }); afterEach(function () { this.lock.hide(); }); - it('sign-up tab should be active', function (done) { - h.waitUntilExists(this.lock, '.auth0-lock-tabs-current', () => { - expect(h.isSignUpTabCurrent(this.lock)).to.be.ok(); - done(); - }); + it('sign-up tab should be active', function () { + expect(h.isSignUpTabCurrent(this.lock)).to.be.ok(); }); it('should show the captcha input', function (done) { + expect(h.isSignUpTabCurrent(this.lock)).to.be.ok(); setTimeout(() => { expect(h.qInput(this.lock, 'captcha', false)).to.be.ok(); done(); @@ -56,16 +59,13 @@ describe('captcha on signup', function () { }); it('should require another challenge when clicking the refresh button', function (done) { - h.waitUntilExists(this.lock, '.auth0-lock-captcha-refresh', () => { - h.clickRefreshCaptchaButton(this.lock); - - setTimeout(() => { - expect(h.q(this.lock, '.auth0-lock-captcha-image').style.backgroundImage).to.equal( - `url("${svgCaptchaRequiredResponse2.image}")` - ); - done(); - }, 200); - }); + h.clickRefreshCaptchaButton(this.lock); + setTimeout(() => { + expect(h.q(this.lock, '.auth0-lock-captcha-image').style.backgroundImage).to.equal( + `url("${svgCaptchaRequiredResponse2.image}")` + ); + done(); + }, 200); }); it('should submit the captcha provided by the user', function (done) { @@ -86,12 +86,16 @@ describe('captcha on signup', function () { }); }); - describe('when the challenge api returns required: false', function () { + describe('when the challenge api returns required: false for signup', function () { beforeEach(function (done) { - h.stubGetChallenge({ - required: false + h.stubGetChallenge([svgCaptchaRequiredResponse1, svgCaptchaRequiredResponse2]); + h.stubGetSignupChallenge({ required: false }); + this.lock = h.displayLock('', lockOpts, () => { + h.clickSignUpTab(); + h.waitUntilExists(this.lock, '.auth0-lock-with-terms', () => { + done(); + }); }); - this.lock = h.displayLock('', lockOpts, done); }); afterEach(function () { @@ -110,7 +114,7 @@ describe('captcha on signup', function () { }); h.waitForEmailAndPasswordInput(this.lock, () => { - h.stubGetChallenge(svgCaptchaRequiredResponse1); + h.stubGetSignupChallenge(svgCaptchaRequiredResponse1); h.fillEmailInput(this.lock, 'someone@example.com'); h.fillComplexPassword(this.lock); h.submitForm(this.lock); @@ -127,8 +131,14 @@ describe('captcha on signup', function () { describe('recaptcha', () => { describe('when the api returns a new challenge', function () { beforeEach(function (done) { - this.stub = h.stubGetChallenge([recaptchav2Response]); - this.lock = h.displayLock('', lockOpts, done); + this.stub = h.stubGetChallenge({ required: false }); + this.stub = h.stubGetSignupChallenge([recaptchav2Response]); + this.lock = h.displayLock('', lockOpts, () => { + h.clickSignUpTab(); + h.waitUntilExists(this.lock, '.auth0-lock-with-terms', () => { + done(); + }); + }); }); afterEach(function () { @@ -157,11 +167,16 @@ describe('captcha on signup', function () { describe('when the challenge api returns required: false', function () { let notRequiredStub; + let loginGetChallengeStub; beforeEach(function (done) { - notRequiredStub = h.stubGetChallenge({ - required: false + loginGetChallengeStub = h.stubGetChallenge([recaptchav2Response]); + notRequiredStub = h.stubGetSignupChallenge({ required: false }); + this.lock = h.displayLock('', lockOpts, () => { + h.clickSignUpTab(); + h.waitUntilExists(this.lock, '.auth0-lock-with-terms', () => { + done(); + }); }); - this.lock = h.displayLock('', lockOpts, done); }); afterEach(function () { @@ -181,7 +196,7 @@ describe('captcha on signup', function () { setTimeout(done, 260); }); - challengeStub = h.stubGetChallenge(recaptchav2Response); + challengeStub = h.stubGetSignupChallenge([recaptchav2Response]); h.fillEmailInput(this.lock, 'someone@example.com'); h.fillComplexPassword(this.lock); h.submitForm(this.lock); @@ -190,6 +205,7 @@ describe('captcha on signup', function () { it('should call the challenge api again and show the input', function () { expect(notRequiredStub.calledOnce).to.be.true; expect(challengeStub.calledOnce).to.be.true; + expect(loginGetChallengeStub.calledOnce).to.be.false; expect(h.q(this.lock, '.auth0-lock-recaptchav2')).to.be.ok(); }); }); diff --git a/test/helper/ui.js b/test/helper/ui.js index 14d8519be..bb226813a 100644 --- a/test/helper/ui.js +++ b/test/helper/ui.js @@ -33,6 +33,7 @@ export const stubWebApis = () => { cb(null, ssoData); }); stubGetChallenge(); + stubGetSignupChallenge(); stubI18n(); }; @@ -79,6 +80,7 @@ export const restoreWebApis = () => { webApi.signUp.restore(); } webApi.getChallenge.restore(); + webApi.getSignupChallenge.restore(); gravatarProvider.displayName.restore(); gravatarProvider.url.restore(); ClientSettings.fetchClientSettings.restore(); @@ -286,6 +288,13 @@ export const clickRefreshCaptchaButton = (lock, connection) => export const clickSocialConnectionButton = (lock, connection) => clickFn(lock, `.auth0-lock-social-button[data-provider='${connection}']`); + +export const clickSignUpTab = (lock) => { + // there is no id for the unselected tab (Login is selected by default) + const signUpTab = window.document['querySelector']('.auth0-lock-tabs > li:nth-child(2) > a'); + Simulate.click(signUpTab, {}); +} + const fillInput = (lock, name, str) => { Simulate.change(qInput(lock, name, true), { target: { value: str } }); }; @@ -469,3 +478,15 @@ export const stubGetChallenge = (result = { required: false }) => { callback(null, result); }); }; + +export const stubGetSignupChallenge = (result = { required: false }) => { + if (typeof webApi.getSignupChallenge.restore === 'function') { + webApi.getSignupChallenge.restore(); + } + return stub(webApi, 'getSignupChallenge', (lockID, callback) => { + if (Array.isArray(result)) { + return callback(null, result.shift()); + } + callback(null, result); + }); +};