diff --git a/package.json b/package.json index b714bbda4..2e2bd9b8c 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ }, "dependencies": { "auth0-js": "^9.7.3", + "auth0-password-policies": "auth0/auth0-password-policies#v1.0.1", "blueimp-md5": "2.3.1", "fbjs": "^0.3.1", "immutable": "^3.7.3", diff --git a/src/__tests__/core/client/__snapshots__/index.test.js.snap b/src/__tests__/core/client/__snapshots__/index.test.js.snap new file mode 100644 index 000000000..ebdaf60a0 --- /dev/null +++ b/src/__tests__/core/client/__snapshots__/index.test.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`core/client/index initClient loads password policy 'excellent' correctly with a password_complexity_options option 1`] = ` +Object { + "minLength": 4, +} +`; + +exports[`core/client/index initClient loads password policy 'excellent' correctly without a password_complexity_options option 1`] = ` +Object { + "minLength": 10, +} +`; + +exports[`core/client/index initClient loads password policy 'fair' correctly with a password_complexity_options option 1`] = ` +Object { + "minLength": 4, +} +`; + +exports[`core/client/index initClient loads password policy 'fair' correctly without a password_complexity_options option 1`] = ` +Object { + "minLength": 8, +} +`; + +exports[`core/client/index initClient loads password policy 'good' correctly with a password_complexity_options option 1`] = ` +Object { + "minLength": 4, +} +`; + +exports[`core/client/index initClient loads password policy 'good' correctly without a password_complexity_options option 1`] = ` +Object { + "minLength": 8, +} +`; + +exports[`core/client/index initClient loads password policy 'low' correctly with a password_complexity_options option 1`] = ` +Object { + "minLength": 4, +} +`; + +exports[`core/client/index initClient loads password policy 'low' correctly without a password_complexity_options option 1`] = ` +Object { + "minLength": 6, +} +`; + +exports[`core/client/index initClient loads password policy 'none' correctly with a password_complexity_options option 1`] = ` +Object { + "minLength": 4, +} +`; + +exports[`core/client/index initClient loads password policy 'none' correctly without a password_complexity_options option 1`] = ` +Object { + "minLength": 1, +} +`; diff --git a/src/__tests__/core/client/index.test.js b/src/__tests__/core/client/index.test.js new file mode 100644 index 000000000..e0d38a4bc --- /dev/null +++ b/src/__tests__/core/client/index.test.js @@ -0,0 +1,44 @@ +import Immutable from 'immutable'; +import { initClient } from '../../../core/client'; + +describe('core/client/index', () => { + describe('initClient', () => { + ['none', 'low', 'fair', 'good', 'excellent'].forEach(policy => { + it(`loads password policy '${policy}' correctly without a password_complexity_options option`, () => { + const client = { + strategies: [ + { + name: 'auth0', + connections: [ + { + name: 'Username-Password-Authentication', + passwordPolicy: policy + } + ] + } + ] + }; + const result = initClient(Immutable.fromJS({}), client).toJS(); + expect(result.client.connections.database[0].passwordPolicy.length).toMatchSnapshot(); + }); + it(`loads password policy '${policy}' correctly with a password_complexity_options option`, () => { + const client = { + strategies: [ + { + name: 'auth0', + connections: [ + { + name: 'Username-Password-Authentication', + passwordPolicy: policy, + password_complexity_options: { min_length: 4 } + } + ] + } + ] + }; + const result = initClient(Immutable.fromJS({}), client).toJS(); + expect(result.client.connections.database[0].passwordPolicy.length).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/__tests__/field/password.test.js b/src/__tests__/field/password.test.js new file mode 100644 index 000000000..56d95dc14 --- /dev/null +++ b/src/__tests__/field/password.test.js @@ -0,0 +1,26 @@ +import Immutable from 'immutable'; + +describe('field/password', () => { + let passwordField; + beforeEach(() => { + jest.resetModules(); + jest.mock('password-sheriff/lib/policy'); + passwordField = require('field/password'); + }); + describe('validatePassword()', () => { + it(`returns true when there is no policy`, () => { + const value = passwordField.validatePassword('the-password'); + expect(value).toBe(true); + }); + it(`validates password correctly when there is a policy`, () => { + const model = { + toJS: jest.fn() + }; + passwordField.validatePassword('the-password', model); + const { mock } = require('password-sheriff/lib/policy').prototype.check; + expect(mock.calls.length).toBe(1); + expect(mock.calls[0][0]).toBe('the-password'); + expect(model.toJS).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/__tests__/ui/input/password/__snapshots__/password_strength.test.jsx.snap b/src/__tests__/ui/input/password/__snapshots__/password_strength.test.jsx.snap new file mode 100644 index 000000000..b91e15352 --- /dev/null +++ b/src/__tests__/ui/input/password/__snapshots__/password_strength.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PasswordStrength validatePassword() validates password correctly with invalid password 1`] = `"
"`; diff --git a/src/__tests__/ui/input/password/password_strength.test.jsx b/src/__tests__/ui/input/password/password_strength.test.jsx new file mode 100644 index 000000000..d17c35295 --- /dev/null +++ b/src/__tests__/ui/input/password/password_strength.test.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import PasswordStrength from '../../../../ui/input/password/password_strength'; + +describe('PasswordStrength', () => { + beforeEach(() => { + jest.resetModules(); + }); + describe('validatePassword()', () => { + it(`validates password correctly with invalid password`, () => { + const policy = { + toJS: () => ({ + length: { + minLength: 20 + } + }) + }; + const messages = { foo: 'the-message' }; + const wrapper = mount( + + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/core/client/index.js b/src/core/client/index.js index b09d01190..f3308a4c6 100644 --- a/src/core/client/index.js +++ b/src/core/client/index.js @@ -1,4 +1,6 @@ import Immutable, { List, Map } from 'immutable'; +import passwordPolicies from 'auth0-password-policies'; + import { dataFns } from '../../utils/data_utils'; // TODO: this module should depend from social stuff import { STRATEGIES as SOCIAL_STRATEGIES } from '../../connection/social/index'; @@ -112,7 +114,13 @@ function formatClientConnection(connectionType, strategyName, connection) { }; if (connectionType === 'database') { - result.passwordPolicy = connection.passwordPolicy || 'none'; + result.passwordPolicy = passwordPolicies[connection.passwordPolicy || 'none']; + if ( + connection.password_complexity_options && + connection.password_complexity_options.min_length + ) { + result.passwordPolicy.length.minLength = connection.password_complexity_options.min_length; + } result.allowSignup = typeof connection.showSignup === 'boolean' ? connection.showSignup : true; result.allowForgot = typeof connection.showForgot === 'boolean' ? connection.showForgot : true; result.requireUsername = diff --git a/src/field/password.js b/src/field/password.js index 2c8b66a43..eba939918 100644 --- a/src/field/password.js +++ b/src/field/password.js @@ -1,8 +1,11 @@ -import createPolicy from 'password-sheriff'; +import PasswordPolicy from 'password-sheriff/lib/policy'; import { setField } from './index'; export function validatePassword(password, policy) { - return createPolicy(policy).check(password); + if (!policy) { + return true; + } + return new PasswordPolicy(policy.toJS()).check(password); } export function setPassword(m, password, policy) { diff --git a/src/field/password/password_pane.jsx b/src/field/password/password_pane.jsx index 27c676fc2..57fa80e98 100644 --- a/src/field/password/password_pane.jsx +++ b/src/field/password/password_pane.jsx @@ -50,7 +50,7 @@ PasswordPane.propTypes = { lock: PropTypes.object.isRequired, onChange: PropTypes.func, placeholder: PropTypes.string.isRequired, - policy: PropTypes.string, + policy: PropTypes.object, strengthMessages: PropTypes.object, hidden: PropTypes.bool }; diff --git a/src/ui/input/password/password_strength.jsx b/src/ui/input/password/password_strength.jsx index 13343a402..30d03a945 100644 --- a/src/ui/input/password/password_strength.jsx +++ b/src/ui/input/password/password_strength.jsx @@ -1,12 +1,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import createPolicy from 'password-sheriff'; +import PasswordPolicy from 'password-sheriff/lib/policy'; import util from 'util'; export default class PasswordStrength extends React.Component { render() { const { password, policy, messages } = this.props; - const analysis = createPolicy(policy).missing(password); + const analysis = new PasswordPolicy(policy.toJS()).missing(password); // TODO: add a component for fadeIn / fadeOut animations? const className = 'auth0-lock-password-strength animated ' + (!analysis.verified ? 'fadeIn' : 'fadeOut'); @@ -39,7 +39,7 @@ export default class PasswordStrength extends React.Component { PasswordStrength.propTypes = { messages: PropTypes.object.isRequired, password: PropTypes.string.isRequired, - policy: PropTypes.oneOf(['none', 'low', 'fair', 'good', 'excellent']).isRequired + policy: PropTypes.object.isRequired }; PasswordStrength.defaultProps = { diff --git a/src/ui/input/password_input.jsx b/src/ui/input/password_input.jsx index 5f2ea0e9a..3647a0929 100644 --- a/src/ui/input/password_input.jsx +++ b/src/ui/input/password_input.jsx @@ -14,7 +14,7 @@ export default class PasswordInput extends React.Component { isValid: PropTypes.bool.isRequired, onChange: PropTypes.func.isRequired, placeholder: PropTypes.string, - policy: PropTypes.string, + policy: PropTypes.object, strengthMessages: PropTypes.object, value: PropTypes.string.isRequired, showPassword: PropTypes.bool.isRequired, diff --git a/yarn.lock b/yarn.lock index 2229e52f6..fdf1e9362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -404,6 +404,12 @@ auth0-js@^9.7.3: url-join "^1.1.0" winchan "^0.2.0" +auth0-password-policies@auth0/auth0-password-policies#v1.0.1: + version "1.0.1" + resolved "https://codeload.github.com/auth0/auth0-password-policies/tar.gz/0b3da9b369b122a8b294d496143abfd33311e6b2" + dependencies: + password-sheriff "^1.1.0" + autoprefixer-stylus@^0.9.4: version "0.9.4" resolved "https://registry.yarnpkg.com/autoprefixer-stylus/-/autoprefixer-stylus-0.9.4.tgz#760914695dce321c9980b490d81a4385c08d914d"