Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IAMRISK-1790 Support captcha for Passwordless #2222

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
"webpack-dev-server": "^2.3.0"
},
"dependencies": {
"auth0-js": "^9.19.2",
"auth0-js": "^9.20.0",
"auth0-password-policies": "^1.0.2",
"blueimp-md5": "^2.19.0",
"classnames": "^2.3.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ describe('passwordless actions', () => {
}));
jest.mock('core/web_api', () => ({
startPasswordless: jest.fn(),
passwordlessVerify: jest.fn()
passwordlessVerify: jest.fn(),
getPasswordlessChallenge: jest.fn()
}));
jest.mock('core/actions', () => ({
closeLock: jest.fn(),
Expand All @@ -50,7 +51,8 @@ describe('passwordless actions', () => {
},
emitAuthorizationErrorEvent: jest.fn(),
connections: jest.fn(),
useCustomPasswordlessConnection: jest.fn(() => false)
useCustomPasswordlessConnection: jest.fn(() => false),
passwordlessCaptcha: jest.fn()
}));
jest.mock('store/index', () => ({
read: jest.fn(() => 'model'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`email passwordless renders a captcha 1`] = `
<div>
<div
data-__type="social_buttons_pane"
data-instructions="socialLoginInstructions"
data-labelFn={[Function]}
data-lock="model"
data-signUp={true}
/>
<div
data-__type="pane_separator"
/>
<p>
passwordlessEmailAlternativeInstructions
</p>
<div
data-__type="email_pane"
data-i18n={
Object {
"html": [Function],
"str": [Function],
}
}
data-lock="model"
data-placeholder="emailInputPlaceholder"
data-strictValidation={false}
/>
<div
data-__type="captcha_pane"
data-i18n={
Object {
"html": [Function],
"str": [Function],
}
}
data-isPasswordless={true}
data-lock="model"
data-onReload={[Function]}
/>
</div>
`;

exports[`email passwordless renders correctly 1`] = `
<div>
<div
data-__type="social_buttons_pane"
data-instructions="socialLoginInstructions"
data-labelFn={[Function]}
data-lock="model"
data-signUp={true}
/>
<div
data-__type="pane_separator"
/>
<p>
passwordlessEmailAlternativeInstructions
</p>
<div
data-__type="email_pane"
data-i18n={
Object {
"html": [Function],
"str": [Function],
}
}
data-lock="model"
data-placeholder="emailInputPlaceholder"
data-strictValidation={false}
/>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`sms passwordless renders a captcha 1`] = `
<div>
<div
data-__type="social_buttons_pane"
data-instructions="socialLoginInstructions"
data-labelFn={[Function]}
data-lock="model"
data-signUp={true}
/>
<div
data-__type="pane_separator"
/>
<div
data-__type="phone_number_pane"
data-instructions="passwordlessSMSAlternativeInstructions"
data-invalidHint="phoneNumberInputInvalidHint"
data-lock="model"
data-placeholder="phoneNumberInputPlaceholder"
/>
<div
data-__type="captcha_pane"
data-i18n={
Object {
"html": [Function],
"str": [Function],
}
}
data-isPasswordless={true}
data-lock="model"
data-onReload={[Function]}
/>
</div>
`;

exports[`sms passwordless renders correctly 1`] = `
<div>
<div
data-__type="social_buttons_pane"
data-instructions="socialLoginInstructions"
data-labelFn={[Function]}
data-lock="model"
data-signUp={true}
/>
<div
data-__type="pane_separator"
/>
<div
data-__type="phone_number_pane"
data-instructions="passwordlessSMSAlternativeInstructions"
data-invalidHint="phoneNumberInputInvalidHint"
data-lock="model"
data-placeholder="phoneNumberInputPlaceholder"
/>
</div>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

import { expectComponent, mockComponent } from 'testUtils';

jest.mock('connection/enterprise');
jest.mock('core/index');

jest.mock('field/social/social_buttons_pane', () => mockComponent('social_buttons_pane'));
jest.mock('field/email/email_pane', () => mockComponent('email_pane'));
jest.mock('field/captcha/captcha_pane', () => mockComponent('captcha_pane'));
jest.mock('core/pane_separator', () => mockComponent('pane_separator'));
jest.mock('connection/database/sign_up_terms', () => mockComponent('sign_up_terms'));
jest.mock('connection/passwordless/index', () => ({
isEmail: jest.fn()
}));

const getComponent = () => {
const SocialOrEmailScreen = require('engine/passwordless/social_or_email_login_screen').default;
const screen = new SocialOrEmailScreen();
return screen.render();
};

describe('email passwordless', () => {
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();

jest.mock('connection/database/index', () => ({
hasScreen: () => false,
databaseUsernameValue: jest.fn()
}));

jest.mock('connection/database/actions', () => ({
cancelMFALogin: jest.fn(),
logIn: jest.fn()
}));

jest.mock('core/signed_in_confirmation', () => ({
renderSignedInConfirmation: jest.fn()
}));

jest.mock('connection/enterprise', () => ({
isHRDEmailValid: jest.fn(() => false),
isHRDDomain: jest.fn(() => true)
}));

jest.mock('core/index', () => ({
hasSomeConnections: jest.fn(() => true),
passwordlessCaptcha: jest.fn()
}));
});

const defaultProps = {
i18n: {
str: (...keys) => keys.join(','),
html: (...keys) => keys.join(',')
},
model: 'model'
};

it('renders correctly', () => {
const Component = getComponent();

expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
});

it('renders a captcha', () => {
const Component = getComponent();

require('core/index').passwordlessCaptcha.mockReturnValue({
get() {
return true;
}
});

expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

import { expectComponent, mockComponent } from 'testUtils';

jest.mock('connection/enterprise');
jest.mock('core/index');

jest.mock('field/social/social_buttons_pane', () => mockComponent('social_buttons_pane'));
jest.mock('field/phone-number/phone_number_pane', () => mockComponent('phone_number_pane'));
jest.mock('field/captcha/captcha_pane', () => mockComponent('captcha_pane'));
jest.mock('core/pane_separator', () => mockComponent('pane_separator'));
jest.mock('connection/database/sign_up_terms', () => mockComponent('sign_up_terms'));
jest.mock('connection/passwordless/index', () => ({
isEmail: jest.fn()
}));

const getComponent = () => {
const SocialOrPhoneNumberScreen = require('engine/passwordless/social_or_phone_number_login_screen').default;
const screen = new SocialOrPhoneNumberScreen();
return screen.render();
};

describe('sms passwordless', () => {
beforeEach(() => {
jest.resetModules();
jest.resetAllMocks();

jest.mock('connection/database/index', () => ({
hasScreen: () => false,
databaseUsernameValue: jest.fn()
}));

jest.mock('connection/database/actions', () => ({
cancelMFALogin: jest.fn(),
logIn: jest.fn()
}));

jest.mock('core/signed_in_confirmation', () => ({
renderSignedInConfirmation: jest.fn()
}));

jest.mock('connection/enterprise', () => ({
isHRDEmailValid: jest.fn(() => false),
isHRDDomain: jest.fn(() => true)
}));

jest.mock('core/index', () => ({
hasSomeConnections: jest.fn(() => true),
passwordlessCaptcha: jest.fn()
}));
});

const defaultProps = {
i18n: {
str: (...keys) => keys.join(','),
html: (...keys) => keys.join(',')
},
model: 'model'
};

it('renders correctly', () => {
const Component = getComponent();

expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
});

it('renders a captcha', () => {
const Component = getComponent();

require('core/index').passwordlessCaptcha.mockReturnValue({
get() {
return true;
}
});

expectComponent(<Component {...defaultProps} />).toMatchSnapshot();
});
});
31 changes: 23 additions & 8 deletions src/connection/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import webApi from '../core/web_api';
*
* @param {Object} m model
* @param {Number} id
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
*/
export function showMissingCaptcha(m, id) {
const captchaConfig = l.captcha(m);
export function showMissingCaptcha(m, id, isPasswordless = false) {
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);

const captchaError =
captchaConfig.get('provider') === 'recaptcha_v2' ? 'invalid_recaptcha' : 'invalid_captcha';
const captchaError = (
captchaConfig.get('provider') === 'recaptcha_v2' ||
captchaConfig.get('provider') === 'recaptcha_enterprise'
) ? 'invalid_recaptcha' : 'invalid_captcha';

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

Expand All @@ -31,13 +34,14 @@ export function showMissingCaptcha(m, id) {
*
* @param {Object} m model
* @param {Object} params
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow
* @param {Object} fields
*
* @returns {Boolean} returns true if is required and missing the response from the user
*/
export function setCaptchaParams(m, params, fields) {
const captchaConfig = l.captcha(m);
const isCaptchaRequired = captchaConfig && l.captcha(m).get('required');
export function setCaptchaParams(m, params, isPasswordless, fields) {
const captchaConfig = isPasswordless ? l.passwordlessCaptcha(m) : l.captcha(m);
const isCaptchaRequired = captchaConfig && captchaConfig.get('required');

if (!isCaptchaRequired) {
return true;
Expand All @@ -57,10 +61,21 @@ export function setCaptchaParams(m, params, fields) {
* Get a new challenge and display the new captcha image.
*
* @param {number} id The id of the Lock instance.
* @param {Boolean} isPasswordless Whether the captcha is being rendered in a passwordless flow.
* @param {boolean} wasInvalid A boolean indicating if the previous captcha was invalid.
* @param {Function} [next] A callback.
*/
export function swapCaptcha(id, wasInvalid, next) {
export function swapCaptcha(id, isPasswordless, wasInvalid, next) {
if (isPasswordless) {
return webApi.getPasswordlessChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setPasswordlessCaptcha, newCaptcha, wasInvalid);
}
if (next) {
next();
}
});
}
return webApi.getChallenge(id, (err, newCaptcha) => {
if (!err && newCaptcha) {
swap(updateEntity, 'lock', id, l.setCaptcha, newCaptcha, wasInvalid);
Expand Down
Loading