Skip to content

Commit

Permalink
Display error message when password is pwned
Browse files Browse the repository at this point in the history
  • Loading branch information
hotblac committed Aug 5, 2018
1 parent b40375f commit 54d4371
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 35 deletions.
4 changes: 2 additions & 2 deletions src/Pwnedpasswords.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const sha1 = require('sha1');
* @returns
* @link https://haveibeenpwned.com/API/v2#SearchingPwnedPasswordsByRange
*/
export const isPasswordPwned = (password) => {
export function isPasswordPwned(password) {

// Send the first 5 digits of the hash to API
const hash = sha1(password);
Expand All @@ -18,4 +18,4 @@ export const isPasswordPwned = (password) => {
.then(response => response.split(/\r?\n/)) // Make an array of results
.then(lines => lines.map(line => line.split(':')[0].toLowerCase())) // Hash suffix is string up to colon
.then(lines => lines.includes(suffix.toLowerCase()));
};
}
20 changes: 16 additions & 4 deletions src/UserRegistration.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component } from 'react';
import * as pwn from "./Pwnedpasswords.api";
import 'bulma/css/bulma.css'
import 'bulma-tooltip/dist/css/bulma-tooltip.min.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
Expand All @@ -12,22 +13,28 @@ export class UserRegistration extends Component {
username: '',
password: '',
confirm: '',
passwordMatchesConfirm: true
passwordMatchesConfirm: true,
passwordIsPwned: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}

submitEnabled = () => {
return this.state.password && this.state.passwordMatchesConfirm;
};

checkForPwnedPassword = (password) => {
pwn.isPasswordPwned(password)
.then(result => this.setState({passwordIsPwned: result}));
};

handleInputChange = (event) => {
const target = event.target;

// Logic to check passwords match must check input change against existing field value.
let passwordMatchesConfirm = this.state.passwordMatchesConfirm;
if (target.name === 'password') {
passwordMatchesConfirm = target.value && (target.value === this.state.confirm);
this.checkForPwnedPassword(target.value);
} else if (target.name === 'confirm') {
passwordMatchesConfirm = target.value && (target.value === this.state.password);
}
Expand Down Expand Up @@ -58,8 +65,13 @@ export class UserRegistration extends Component {
</div>
<div className="field" id="passwordField">
<label className="label">Password</label>
<div className="control">
<input name="password" type="password" className="input" onChange={this.handleInputChange}/>
<div className="control has-icons-right">
<input name="password" type="password" className={'input' + (this.state.passwordIsPwned ? ' is-danger' : '')} onChange={this.handleInputChange}/>
{this.state.passwordIsPwned &&
<span className="icon is-right tooltip" style={{pointerEvents: 'inherit'}} data-tooltip="Password is pwned">
<FontAwesomeIcon icon={faExclamationCircle}/>
</span>
}
</div>
</div>
<div className="field" id="confirmField">
Expand Down
108 changes: 79 additions & 29 deletions src/UserRegistration.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import React from 'react';
import { shallow } from 'enzyme';
import {UserRegistration} from "./UserRegistration";
import * as pwn from "./Pwnedpasswords.api";

describe ('<UserRegistration/>', () => {

const username = 'username';
const password = 'password';
const goodPassword = 'password';
const pwnedPassword = "pwned";

pwn.isPasswordPwned = jest.fn((pwd) => {
return Promise.resolve(pwd !== goodPassword)
});
const onSubmit = jest.fn();

beforeEach(() => {
Expand All @@ -21,18 +27,74 @@ describe ('<UserRegistration/>', () => {

it('notifies caller of username and password', () => {
const wrapper = shallow(<UserRegistration onSubmit={onSubmit}/>);
setFieldValues(wrapper, username, password, password);
setFieldValues(wrapper, username, goodPassword, goodPassword);

const submitButton = wrapper.find('button');
submitButton.simulate('click');

expect(onSubmit).toBeCalledWith(username, password);
expect(onSubmit).toBeCalledWith(username, goodPassword);
});


it('shows error markers when password does not match confirmation', () => {
const wrapper = shallow(<UserRegistration/>);
setFieldValues(wrapper, username, goodPassword, 'mismatch');

const inputField = wrapper.find('#confirmField input')
const errorIcon = wrapper.find('#confirmField .icon');

expect(inputField.hasClass('is-danger')).toBe(true);
expect(errorIcon.exists()).toBe(true);
});

it('hides error marker when password does matches confirmation', () => {
const wrapper = shallow(<UserRegistration/>);
setFieldValues(wrapper, username, goodPassword, goodPassword);

const inputField = wrapper.find('#confirmField input')
const errorIcon = wrapper.find('#confirmField .icon');

expect(inputField.hasClass('is-danger')).toBe(false);
expect(errorIcon.exists()).toBe(false);
});

it('shows error marker when password is pwned', async () => {
const wrapper = shallow(<UserRegistration/>);
await setFieldValuesAndUpdate(wrapper, username, pwnedPassword, pwnedPassword);

const inputField = wrapper.find('#passwordField input');
const errorIcon = wrapper.find('#passwordField .icon');

expect(inputField.hasClass('is-danger')).toBe(true);
expect(errorIcon.exists()).toBe(true);
});

it ('hides error marker when password is blank', () => {
const wrapper = shallow(<UserRegistration/>);
setFieldValues(wrapper, username, '', '');

const inputField = wrapper.find('#passwordField input')
const errorIcon = wrapper.find('#passwordField .icon');

expect(inputField.hasClass('is-danger')).toBe(false);
expect(errorIcon.exists()).toBe(false);
});

it ('hides error marker when password is not pwned', async () => {
const wrapper = shallow(<UserRegistration/>);
await setFieldValuesAndUpdate(wrapper, username, goodPassword, goodPassword);

const inputField = wrapper.find('#passwordField input')
const errorIcon = wrapper.find('#passwordField .icon');

expect(inputField.hasClass('is-danger')).toBe(false);
expect(errorIcon.exists()).toBe(false);
});


it('disables submit when password does not match confirmation', () => {
const wrapper = shallow(<UserRegistration onSubmit={onSubmit}/>);
setFieldValues(wrapper, username, password, 'mismatch');
setFieldValues(wrapper, username, goodPassword, 'mismatch');

const submitButton = wrapper.find('button');
expect(submitButton.prop('disabled')).toBeTruthy();
Expand All @@ -45,7 +107,7 @@ describe ('<UserRegistration/>', () => {

it('enables submit when password matches confirmation', () => {
const wrapper = shallow(<UserRegistration onSubmit={onSubmit}/>);
setFieldValues(wrapper, username, password, password);
setFieldValues(wrapper, username, goodPassword, goodPassword);

const submitButton = wrapper.find('button');
expect(submitButton.prop('disabled')).toBeFalsy();
Expand All @@ -67,29 +129,6 @@ describe ('<UserRegistration/>', () => {
expect(onSubmit).not.toBeCalled();
});

it('shows error markers when password does not match confirmation', () => {
const wrapper = shallow(<UserRegistration/>);
setFieldValues(wrapper, username, password, 'mismatch');

const inputField = wrapper.find('#confirmField input')
const errorIcon = wrapper.find('#confirmField .icon');

expect(inputField.hasClass('is-danger')).toBe(true);
expect(errorIcon.exists()).toBe(true);
});

it('hides error marker when password does matches confirmation', () => {
const wrapper = shallow(<UserRegistration/>);
setFieldValues(wrapper, username, password, password);

const inputField = wrapper.find('#confirmField input')
const errorIcon = wrapper.find('#confirmField .icon');

expect(inputField.hasClass('is-danger')).toBe(false);
expect(errorIcon.exists()).toBe(false);
});


function setFieldValues(wrapper, username, password, confirm) {
const usernameField = wrapper.find('#usernameField input');
const passwordField = wrapper.find('#passwordField input');
Expand All @@ -99,6 +138,13 @@ describe ('<UserRegistration/>', () => {
passwordField.simulate('change', stubEvent(passwordField, password));
confirmField.simulate('change', stubEvent(confirmField, confirm));
}


async function setFieldValuesAndUpdate(wrapper, username, password, confirm) {
setFieldValues(wrapper, username, password, confirm);
await flushPromises();
wrapper.update();
}
});

/**
Expand All @@ -113,4 +159,8 @@ function stubEvent(node, value) {
name: name,
value: value
}};
};
}

function flushPromises() {
return new Promise(resolve => setImmediate(resolve));
}

0 comments on commit 54d4371

Please sign in to comment.