Skip to content

Commit

Permalink
Use Signup challenge enforcement endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
TSLarson committed Oct 31, 2024
1 parent 59b50aa commit da99f30
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 44 deletions.
16 changes: 14 additions & 2 deletions src/connection/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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);
}
Expand All @@ -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]);
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 8 additions & 8 deletions src/connection/database/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
});
};
Expand Down Expand Up @@ -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 = (
Expand All @@ -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);
});
Expand All @@ -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);
});
}
Expand All @@ -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);
});
}
Expand Down
9 changes: 9 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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');
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/web_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/web_api/p2_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions src/engine/classic/sign_up_pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) ? (
<CaptchaPane i18n={i18n} lock={model} onReload={() => swapCaptcha(l.id(model), Flow.DEFAULT, false)} />
<CaptchaPane i18n={i18n} lock={model} flow={Flow.SIGNUP} onReload={() => swapCaptcha(l.id(model), Flow.SIGNUP, false)} />
) : null;

const passwordPane = !onlyEmail && (
Expand Down
78 changes: 47 additions & 31 deletions test/captcha_signup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import en from '../src/i18n/en';

const lockOpts = {
allowedConnections: ['db'],
rememberLastLogin: false,
initialScreen: 'signUp'
rememberLastLogin: false
};

const svgCaptchaRequiredResponse1 = {
Expand Down Expand Up @@ -33,39 +32,40 @@ 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();
}, 500);
});

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) {
Expand All @@ -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 () {
Expand All @@ -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);
Expand All @@ -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 () {
Expand Down Expand Up @@ -156,12 +166,17 @@ describe('captcha on signup', function () {
});

describe('when the challenge api returns required: false', function () {
let notRequiredStub;
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 () {
Expand All @@ -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);
Expand All @@ -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();
});
});
Expand Down
21 changes: 21 additions & 0 deletions test/helper/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const stubWebApis = () => {
cb(null, ssoData);
});
stubGetChallenge();
stubGetSignupChallenge();
stubI18n();
};

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 } });
};
Expand Down Expand Up @@ -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);
});
};

0 comments on commit da99f30

Please sign in to comment.