Skip to content

Commit

Permalink
[IAMRISK-3539] Use signup classic endpoint for captcha (#2587)
Browse files Browse the repository at this point in the history
### Changes
* Uses Signup Captcha enforcement Policy for Signup instead of
Login/Default enforcement policy (new capability)
 * Uses reset_password enforcement Policy for reset_password (bug)
* Isolates calls for passwordless and reset password enforcement policy
to specific flows instead of on Lock load (bug)


### References

https://auth0team.atlassian.net/browse/IAMRISK-4032
https://auth0team.atlassian.net/browse/IAMRISK-4161

### Testing


https://oktawiki.atlassian.net/wiki/spaces/IAMCA/pages/3113844770/Bot+Detection+Signup+Classic+UL+Testing+Documentation

* [x] This change adds unit test coverage
* [x] This change adds integration test coverage
* [x] This change has been tested on the latest version of the
platform/language

### Checklist

* [x] I have read the [Auth0 general contribution
guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
* [x] I have read the [Auth0 Code of
Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
* [x] All code quality tools/guidelines have been run/followed
* [x] All relevant assets have been compiled
  • Loading branch information
TSLarson authored Nov 7, 2024
2 parents 1cf70cc + 05e6b67 commit 0854d9a
Show file tree
Hide file tree
Showing 16 changed files with 161 additions and 83 deletions.
22 changes: 12 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ exports[`passwordless connection initPasswordless() calls initNS with showTerms
`;

exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 1`] = `
[
undefined,
"passwordlessCaptcha",
{
"successFn": undefined,
"syncFn": [Function],
},
]
`;

exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 2`] = `
[
undefined,
"location",
Expand All @@ -100,14 +111,14 @@ exports[`passwordless connection initPasswordless() should call webAPI.getUserCo
]
`;

exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 2`] = `
exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 3`] = `
[
"id",
"cb",
]
`;

exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 3`] = `
exports[`passwordless connection initPasswordless() should call webAPI.getUserCountry when there is no default location 4`] = `
[
"model",
"en",
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/connection/passwordless/passwordless.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ describe('passwordless connection', () => {
it('should call webAPI.getUserCountry when there is no default location', () => {
initPasswordless(null, {});
const sync = require('sync');
expectMockToMatch(sync, 1);
expectMockToMatch(sync, 2);

const { syncFn, successFn } = sync.mock.calls[0][2];
const { syncFn, successFn } = sync.mock.calls[1][2];
syncFn(null, 'cb');
expectMockToMatch(require('core/web_api').getUserCountry, 1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ exports[`SignUpPane shows the Captcha pane 1`] = `
/>
<CaptchaPane
error={false}
flow="signup"
i18n={
{
"html": [Function],
Expand Down Expand Up @@ -241,6 +242,7 @@ exports[`SignUpPane shows the Captcha pane for SSO (ADFS) connections 1`] = `
/>
<CaptchaPane
error={false}
flow="signup"
i18n={
{
"html": [Function],
Expand Down
10 changes: 6 additions & 4 deletions src/__tests__/engine/classic/sign_up_pane.test.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import { expectComponent, mockComponent } from 'testUtils';
import { expectShallowComponent } from '../../testUtils';
import { Flow } from '../../../connection/captcha';

jest.mock('field/email/email_pane', () => mockComponent('email_pane'));
jest.mock('field/password/password_pane', () => mockComponent('password_pane'));
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', () => ({
Expand Down Expand Up @@ -38,6 +39,7 @@ describe('SignUpPane', () => {
str: (...keys) => keys.join(','),
html: (...keys) => keys.join(',')
},
flow: Flow.SIGNUP,
model: 'model',
emailInputPlaceholder: 'emailInputPlaceholder',
onlyEmail: true,
Expand All @@ -58,7 +60,7 @@ describe('SignUpPane', () => {
});

it('shows the Captcha pane', () => {
require('core/index').captcha.mockReturnValue({
require('core/index').signupCaptcha.mockReturnValue({
get() {
return true;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
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
12 changes: 12 additions & 0 deletions src/connection/passwordless/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { dataFns } from '../../utils/data_utils';
const { get, initNS, tget, tremove, tset } = dataFns(['passwordless']);
import webAPI from '../../core/web_api';
import sync from '../../sync';
import webApi from '../../core/web_api';
import { setPasswordlessCaptcha } from '../../core/index';

export function initPasswordless(m, opts) {
// TODO: validate opts
Expand All @@ -14,6 +16,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: setPasswordlessCaptcha
});

if (opts.defaultLocation && typeof opts.defaultLocation === 'string') {
m = initLocation(m, opts.defaultLocation.toUpperCase());
} else {
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
19 changes: 0 additions & 19 deletions src/core/remote_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
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
Loading

0 comments on commit 0854d9a

Please sign in to comment.