Skip to content

Commit f8c87c0

Browse files
robinbijlanikaki1104Steve Hobbs
authored
IAMRISK-1790 Support captcha for Passwordless (#2222)
* initial changes for email passwordless login screen * updated tests * Fix test for passwordless/social_or_email_login_screen Fixes for a couple of issues: **Circular dependency error** Thanks to the email field, it was trying to use a function `isHRDEmailValid` from 'connection/enterprise', which isn't necessary for this test (I don't *think* you can have enterprise passwordless connections, you would just use the non-passwordless version of Lock). Mocking out this module and just returning `false` for `isHRDEmailValid` makes things simpler. **m.getIn is not a function** This is down to `social_or_email_login_screen` calling `hasSomeConnections` from 'core/index', this can simply be mocked to return `true` for this test. This function just verifies that there is a passwordless or email connection available. I also had to mock out i18n.html, as this function is called when the component renders. * add capthca pane to social or email login screen (failing tests) * Remove unneeded lines * add captcha to passwordless login screens, with unit tests passing * got rid of sso * got rid of enterprise check * deleted unncessary imports * Captcha support for Passwordless * Update passwordless snapshots * Error translations * Swap captcha if restarting passwordless * Add missing fun argument docs * Use invalid_recaptcha error key for recaptcha_enterprise * Resolve deps publicly * Fix bug to correctly reset captcha field * Remove conditional for non objects * Add missing translations Co-authored-by: kaki1104 <kaki.so2011@gmail.com> Co-authored-by: Steve Hobbs <steve.hobbs@auth0.com>
1 parent 872f7f7 commit f8c87c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+544
-86
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
"webpack-dev-server": "^2.3.0"
107107
},
108108
"dependencies": {
109-
"auth0-js": "^9.19.2",
109+
"auth0-js": "^9.20.0",
110110
"auth0-password-policies": "^1.0.2",
111111
"blueimp-md5": "^2.19.0",
112112
"classnames": "^2.3.2",

src/__tests__/connection/passwordless/passwordless.actions.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ describe('passwordless actions', () => {
2929
}));
3030
jest.mock('core/web_api', () => ({
3131
startPasswordless: jest.fn(),
32-
passwordlessVerify: jest.fn()
32+
passwordlessVerify: jest.fn(),
33+
getPasswordlessChallenge: jest.fn()
3334
}));
3435
jest.mock('core/actions', () => ({
3536
closeLock: jest.fn(),
@@ -50,7 +51,8 @@ describe('passwordless actions', () => {
5051
},
5152
emitAuthorizationErrorEvent: jest.fn(),
5253
connections: jest.fn(),
53-
useCustomPasswordlessConnection: jest.fn(() => false)
54+
useCustomPasswordlessConnection: jest.fn(() => false),
55+
passwordlessCaptcha: jest.fn()
5456
}));
5557
jest.mock('store/index', () => ({
5658
read: jest.fn(() => 'model'),
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`email passwordless renders a captcha 1`] = `
4+
<div>
5+
<div
6+
data-__type="social_buttons_pane"
7+
data-instructions="socialLoginInstructions"
8+
data-labelFn={[Function]}
9+
data-lock="model"
10+
data-signUp={true}
11+
/>
12+
<div
13+
data-__type="pane_separator"
14+
/>
15+
<p>
16+
passwordlessEmailAlternativeInstructions
17+
</p>
18+
<div
19+
data-__type="email_pane"
20+
data-i18n={
21+
Object {
22+
"html": [Function],
23+
"str": [Function],
24+
}
25+
}
26+
data-lock="model"
27+
data-placeholder="emailInputPlaceholder"
28+
data-strictValidation={false}
29+
/>
30+
<div
31+
data-__type="captcha_pane"
32+
data-i18n={
33+
Object {
34+
"html": [Function],
35+
"str": [Function],
36+
}
37+
}
38+
data-isPasswordless={true}
39+
data-lock="model"
40+
data-onReload={[Function]}
41+
/>
42+
</div>
43+
`;
44+
45+
exports[`email passwordless renders correctly 1`] = `
46+
<div>
47+
<div
48+
data-__type="social_buttons_pane"
49+
data-instructions="socialLoginInstructions"
50+
data-labelFn={[Function]}
51+
data-lock="model"
52+
data-signUp={true}
53+
/>
54+
<div
55+
data-__type="pane_separator"
56+
/>
57+
<p>
58+
passwordlessEmailAlternativeInstructions
59+
</p>
60+
<div
61+
data-__type="email_pane"
62+
data-i18n={
63+
Object {
64+
"html": [Function],
65+
"str": [Function],
66+
}
67+
}
68+
data-lock="model"
69+
data-placeholder="emailInputPlaceholder"
70+
data-strictValidation={false}
71+
/>
72+
</div>
73+
`;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`sms passwordless renders a captcha 1`] = `
4+
<div>
5+
<div
6+
data-__type="social_buttons_pane"
7+
data-instructions="socialLoginInstructions"
8+
data-labelFn={[Function]}
9+
data-lock="model"
10+
data-signUp={true}
11+
/>
12+
<div
13+
data-__type="pane_separator"
14+
/>
15+
<div
16+
data-__type="phone_number_pane"
17+
data-instructions="passwordlessSMSAlternativeInstructions"
18+
data-invalidHint="phoneNumberInputInvalidHint"
19+
data-lock="model"
20+
data-placeholder="phoneNumberInputPlaceholder"
21+
/>
22+
<div
23+
data-__type="captcha_pane"
24+
data-i18n={
25+
Object {
26+
"html": [Function],
27+
"str": [Function],
28+
}
29+
}
30+
data-isPasswordless={true}
31+
data-lock="model"
32+
data-onReload={[Function]}
33+
/>
34+
</div>
35+
`;
36+
37+
exports[`sms passwordless renders correctly 1`] = `
38+
<div>
39+
<div
40+
data-__type="social_buttons_pane"
41+
data-instructions="socialLoginInstructions"
42+
data-labelFn={[Function]}
43+
data-lock="model"
44+
data-signUp={true}
45+
/>
46+
<div
47+
data-__type="pane_separator"
48+
/>
49+
<div
50+
data-__type="phone_number_pane"
51+
data-instructions="passwordlessSMSAlternativeInstructions"
52+
data-invalidHint="phoneNumberInputInvalidHint"
53+
data-lock="model"
54+
data-placeholder="phoneNumberInputPlaceholder"
55+
/>
56+
</div>
57+
`;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
3+
import { expectComponent, mockComponent } from 'testUtils';
4+
5+
jest.mock('connection/enterprise');
6+
jest.mock('core/index');
7+
8+
jest.mock('field/social/social_buttons_pane', () => mockComponent('social_buttons_pane'));
9+
jest.mock('field/email/email_pane', () => mockComponent('email_pane'));
10+
jest.mock('field/captcha/captcha_pane', () => mockComponent('captcha_pane'));
11+
jest.mock('core/pane_separator', () => mockComponent('pane_separator'));
12+
jest.mock('connection/database/sign_up_terms', () => mockComponent('sign_up_terms'));
13+
jest.mock('connection/passwordless/index', () => ({
14+
isEmail: jest.fn()
15+
}));
16+
17+
const getComponent = () => {
18+
const SocialOrEmailScreen = require('engine/passwordless/social_or_email_login_screen').default;
19+
const screen = new SocialOrEmailScreen();
20+
return screen.render();
21+
};
22+
23+
describe('email passwordless', () => {
24+
beforeEach(() => {
25+
jest.resetModules();
26+
jest.resetAllMocks();
27+
28+
jest.mock('connection/database/index', () => ({
29+
hasScreen: () => false,
30+
databaseUsernameValue: jest.fn()
31+
}));
32+
33+
jest.mock('connection/database/actions', () => ({
34+
cancelMFALogin: jest.fn(),
35+
logIn: jest.fn()
36+
}));
37+
38+
jest.mock('core/signed_in_confirmation', () => ({
39+
renderSignedInConfirmation: jest.fn()
40+
}));
41+
42+
jest.mock('connection/enterprise', () => ({
43+
isHRDEmailValid: jest.fn(() => false),
44+
isHRDDomain: jest.fn(() => true)
45+
}));
46+
47+
jest.mock('core/index', () => ({
48+
hasSomeConnections: jest.fn(() => true),
49+
passwordlessCaptcha: jest.fn()
50+
}));
51+
});
52+
53+
const defaultProps = {
54+
i18n: {
55+
str: (...keys) => keys.join(','),
56+
html: (...keys) => keys.join(',')
57+
},
58+
model: 'model'
59+
};
60+
61+
it('renders correctly', () => {
62+
const Component = getComponent();
63+
64+
expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
65+
});
66+
67+
it('renders a captcha', () => {
68+
const Component = getComponent();
69+
70+
require('core/index').passwordlessCaptcha.mockReturnValue({
71+
get() {
72+
return true;
73+
}
74+
});
75+
76+
expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
77+
});
78+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
3+
import { expectComponent, mockComponent } from 'testUtils';
4+
5+
jest.mock('connection/enterprise');
6+
jest.mock('core/index');
7+
8+
jest.mock('field/social/social_buttons_pane', () => mockComponent('social_buttons_pane'));
9+
jest.mock('field/phone-number/phone_number_pane', () => mockComponent('phone_number_pane'));
10+
jest.mock('field/captcha/captcha_pane', () => mockComponent('captcha_pane'));
11+
jest.mock('core/pane_separator', () => mockComponent('pane_separator'));
12+
jest.mock('connection/database/sign_up_terms', () => mockComponent('sign_up_terms'));
13+
jest.mock('connection/passwordless/index', () => ({
14+
isEmail: jest.fn()
15+
}));
16+
17+
const getComponent = () => {
18+
const SocialOrPhoneNumberScreen = require('engine/passwordless/social_or_phone_number_login_screen').default;
19+
const screen = new SocialOrPhoneNumberScreen();
20+
return screen.render();
21+
};
22+
23+
describe('sms passwordless', () => {
24+
beforeEach(() => {
25+
jest.resetModules();
26+
jest.resetAllMocks();
27+
28+
jest.mock('connection/database/index', () => ({
29+
hasScreen: () => false,
30+
databaseUsernameValue: jest.fn()
31+
}));
32+
33+
jest.mock('connection/database/actions', () => ({
34+
cancelMFALogin: jest.fn(),
35+
logIn: jest.fn()
36+
}));
37+
38+
jest.mock('core/signed_in_confirmation', () => ({
39+
renderSignedInConfirmation: jest.fn()
40+
}));
41+
42+
jest.mock('connection/enterprise', () => ({
43+
isHRDEmailValid: jest.fn(() => false),
44+
isHRDDomain: jest.fn(() => true)
45+
}));
46+
47+
jest.mock('core/index', () => ({
48+
hasSomeConnections: jest.fn(() => true),
49+
passwordlessCaptcha: jest.fn()
50+
}));
51+
});
52+
53+
const defaultProps = {
54+
i18n: {
55+
str: (...keys) => keys.join(','),
56+
html: (...keys) => keys.join(',')
57+
},
58+
model: 'model'
59+
};
60+
61+
it('renders correctly', () => {
62+
const Component = getComponent();
63+
64+
expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
65+
});
66+
67+
it('renders a captcha', () => {
68+
const Component = getComponent();
69+
70+
require('core/index').passwordlessCaptcha.mockReturnValue({
71+
get() {
72+
return true;
73+
}
74+
});
75+
76+
expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
77+
});
78+
});

src/connection/captcha.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import webApi from '../core/web_api';
99
*
1010
* @param {Object} m model
1111
* @param {Number} id
12+
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
1213
*/
13-
export function showMissingCaptcha(m, id) {
14-
const captchaConfig = l.captcha(m);
14+
export function showMissingCaptcha(m, id, isPasswordless = false) {
15+
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
1516

16-
const captchaError =
17-
captchaConfig.get('provider') === 'recaptcha_v2' ? 'invalid_recaptcha' : 'invalid_captcha';
17+
const captchaError = (
18+
captchaConfig.get('provider') === 'recaptcha_v2' ||
19+
captchaConfig.get('provider') === 'recaptcha_enterprise'
20+
) ? 'invalid_recaptcha' : 'invalid_captcha';
1821

1922
const errorMessage = i18n.html(m, ['error', 'login', captchaError]);
2023

@@ -31,13 +34,14 @@ export function showMissingCaptcha(m, id) {
3134
*
3235
* @param {Object} m model
3336
* @param {Object} params
37+
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
3438
* @param {Object} fields
3539
*
3640
* @returns {Boolean} returns true if is required and missing the response from the user
3741
*/
38-
export function setCaptchaParams(m, params, fields) {
39-
const captchaConfig = l.captcha(m);
40-
const isCaptchaRequired = captchaConfig && l.captcha(m).get('required');
42+
export function setCaptchaParams(m, params, isPasswordless, fields) {
43+
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
44+
const isCaptchaRequired = captchaConfig && captchaConfig.get('required');
4145

4246
if (!isCaptchaRequired) {
4347
return true;
@@ -57,10 +61,21 @@ export function setCaptchaParams(m, params, fields) {
5761
* Get a new challenge and display the new captcha image.
5862
*
5963
* @param {number} id The id of the Lock instance.
64+
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow.
6065
* @param {boolean} wasInvalid A boolean indicating if the previous captcha was invalid.
6166
* @param {Function} [next] A callback.
6267
*/
63-
export function swapCaptcha(id, wasInvalid, next) {
68+
export function swapCaptcha(id, isPasswordless, wasInvalid, next) {
69+
if (isPasswordless) {
70+
return webApi.getPasswordlessChallenge(id, (err, newCaptcha) => {
71+
if (!err && newCaptcha) {
72+
swap(updateEntity, 'lock', id, l.setPasswordlessCaptcha, newCaptcha, wasInvalid);
73+
}
74+
if (next) {
75+
next();
76+
}
77+
});
78+
}
6479
return webApi.getChallenge(id, (err, newCaptcha) => {
6580
if (!err && newCaptcha) {
6681
swap(updateEntity, 'lock', id, l.setCaptcha, newCaptcha, wasInvalid);

0 commit comments

Comments
 (0)