From 3e201f8d48554ab8c4b8132eaa50b5fe7dd1d67e Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Thu, 1 Feb 2018 14:37:35 +0000 Subject: Add length/common password checking. --- CHANGELOG.md | 1 + cpanfile | 1 + cpanfile.snapshot | 14 ++++++ perllib/FixMyStreet/App/Controller/Auth.pm | 58 +++++++++++++++++++++- perllib/FixMyStreet/App/Controller/Auth/Phone.pm | 8 ++- perllib/FixMyStreet/App/Controller/Auth/Profile.pm | 1 + perllib/FixMyStreet/App/Controller/Report/New.pm | 6 ++- .../FixMyStreet/App/Controller/Report/Update.pm | 7 ++- perllib/FixMyStreet/Cobrand/Borsetshire.pm | 2 + perllib/FixMyStreet/Cobrand/Default.pm | 8 +++ t/app/controller/auth.t | 25 +++++++++- t/app/controller/auth_profile.t | 16 +++++- templates/web/base/auth/change_password.html | 4 +- templates/web/base/auth/general.html | 12 ++++- templates/web/base/js/translation_strings.html | 6 +++ .../report/new/form_user_loggedout_by_email.html | 11 +++- .../update/form_user_loggedout_by_email.html | 9 +++- templates/web/bromley/report/new/form_user.html | 10 +++- templates/web/bromley/report/update-form.html | 10 +++- .../report/new/form_user_loggedout.html | 11 +++- templates/web/zurich/auth/general.html | 21 ++++---- web/cobrands/fixmystreet/fixmystreet.js | 3 ++ web/js/validation_rules.js | 8 ++- 23 files changed, 221 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 680d4b412..d94138d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Front end improvements: - Zoom out as much as necessary on body map page, even on mobile. #1958 - Show loading message on initial /around map load #1976 + - Add minimum password length and common password checking. - Bugfixes: - Fix bug specifying category in URL on /around. #1950 - Fix bug with multiple select-multiples on a page. #1951 diff --git a/cpanfile b/cpanfile index 3d29ac4f8..1c3884011 100644 --- a/cpanfile +++ b/cpanfile @@ -34,6 +34,7 @@ requires 'Authen::SASL'; requires 'Cache::Memcached'; requires 'Carp'; requires 'Crypt::Eksblowfish::Bcrypt'; +requires 'Data::Password::Common'; requires 'DateTime'; requires 'DateTime::Format::HTTP'; requires 'DateTime::Format::ISO8601'; diff --git a/cpanfile.snapshot b/cpanfile.snapshot index c5e5b2a46..c43ad0653 100644 --- a/cpanfile.snapshot +++ b/cpanfile.snapshot @@ -1401,6 +1401,20 @@ DISTRIBUTIONS Module::Build 0.35 Test::Exception 0 Test::More 0 + Data-Password-Common-0.004 + pathname: D/DA/DAGOLDEN/Data-Password-Common-0.004.tar.gz + provides: + Data::Password::Common 0.004 + requirements: + ExtUtils::MakeMaker 6.17 + File::ShareDir 0 + File::ShareDir::Install 0.03 + IO::File 0 + Search::Dict 0 + Sub::Exporter 0 + autodie 2.00 + strict 0 + warnings 0 Data-Visitor-0.28 pathname: D/DO/DOY/Data-Visitor-0.28.tar.gz provides: diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 455022e03..86e3e8434 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -5,6 +5,7 @@ use namespace::autoclean; BEGIN { extends 'Catalyst::Controller'; } use Email::Valid; +use Data::Password::Common 'found'; use Digest::HMAC_SHA1 qw(hmac_sha1); use JSON::MaybeXS; use MIME::Base64; @@ -143,6 +144,11 @@ sub email_sign_in : Private { return; } + my $password = $c->get_param('password_register'); + if ($password) { + return unless $c->forward('/auth/test_password', [ $password ]); + } + # If user registration is disabled then bail out at this point # if there's not already a user with this email address. # NB this uses the same template as a successful sign in to stop @@ -156,8 +162,7 @@ sub email_sign_in : Private { } my $user_params = {}; - $user_params->{password} = $c->get_param('password_register') - if $c->get_param('password_register'); + $user_params->{password} = $password if $password; my $user = $c->model('DB::User')->new( $user_params ); my $token_data = { @@ -348,6 +353,55 @@ sub no_csrf_token : Private { $c->detach('/page_error_400_bad_request', []); } +=item common_password + +Returns 1/0 depending on if password is common or not. + +=cut + +sub common_password : Local : Args(0) { + my ($self, $c) = @_; + + my $password = $c->get_param('password_register'); + + my $return = JSON->true; + if (!$c->cobrand->call_hook('bypass_password_checks') && found($password)) { + $return = _('Please choose a less commonly-used password'); + } + + my $body = JSON->new->utf8->allow_nonref->encode($return); + $c->res->content_type('application/json; charset=utf-8'); + $c->res->body($body); +} + +=item test_password + +Checks a password is not too weak; returns true if okay, +false if weak (and sets stash error). + +=cut + +sub test_password : Private { + my ($self, $c, $password) = @_; + + return 1 if $c->cobrand->call_hook('bypass_password_checks'); + + my @errors; + + my $min_length = $c->cobrand->password_minimum_length; + push @errors, sprintf(_('Please make sure your password is at least %d characters long'), $min_length) + if length($password) < $min_length; + + push @errors, _('Please choose a less commonly-used password') + if found($password); + + if (@errors) { + $c->stash->{field_errors}->{password_register} = join('
', @errors); + return 0; + } + return 1; +} + =head2 sign_out Log the user out. Tell them we've done so. diff --git a/perllib/FixMyStreet/App/Controller/Auth/Phone.pm b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm index 8387b9d64..8e3150df9 100644 --- a/perllib/FixMyStreet/App/Controller/Auth/Phone.pm +++ b/perllib/FixMyStreet/App/Controller/Auth/Phone.pm @@ -59,6 +59,11 @@ sub sign_in : Private { return; } + my $password = $c->get_param('password_register'); + if ($password) { + return unless $c->forward('/auth/test_password', [ $password ]); + } + (my $number = $parsed->{phone}->format) =~ s/\s+//g; if ( FixMyStreet->config('SIGNUPS_DISABLED') @@ -70,8 +75,7 @@ sub sign_in : Private { } my $user_params = {}; - $user_params->{password} = $c->get_param('password_register') - if $c->get_param('password_register'); + $user_params->{password} = $password if $password; my $user = $c->model('DB::User')->new( $user_params ); my $token_data = { diff --git a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm index 5e6fe6266..d1fb32c41 100644 --- a/perllib/FixMyStreet/App/Controller/Auth/Profile.pm +++ b/perllib/FixMyStreet/App/Controller/Auth/Profile.pm @@ -53,6 +53,7 @@ sub change_password : Path('/auth/change_password') { my $password_error = !$new && !$confirm ? 'missing' : $new ne $confirm ? 'mismatch' + : !$c->forward('/auth/test_password', [ $new ]) ? 'failed' : ''; if ($password_error) { diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index 166c0614d..05d082e45 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -813,8 +813,10 @@ sub process_user : Private { } $c->forward('update_user', [ \%params ]); - $report->user->password( Utils::trim_text( $params{password_register} ) ) - if $params{password_register}; + if ($params{password_register}) { + $c->forward('/auth/test_password', [ $params{password_register} ]); + $report->user->password(Utils::trim_text($params{password_register})); + } return 1; } diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index c28039808..c684e46ad 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -144,11 +144,14 @@ sub process_user : Private { $update->user->name( Utils::trim_text( $params{name} ) ) if $params{name}; - $update->user->password( Utils::trim_text( $params{password_register} ) ) - if $params{password_register}; $update->user->title( Utils::trim_text( $params{fms_extra_title} ) ) if $params{fms_extra_title}; + if ($params{password_register}) { + $c->forward('/auth/test_password', [ $params{password_register} ]); + $update->user->password(Utils::trim_text($params{password_register})); + } + return 1; } diff --git a/perllib/FixMyStreet/Cobrand/Borsetshire.pm b/perllib/FixMyStreet/Cobrand/Borsetshire.pm index 7ddcff469..d9b018d69 100644 --- a/perllib/FixMyStreet/Cobrand/Borsetshire.pm +++ b/perllib/FixMyStreet/Cobrand/Borsetshire.pm @@ -29,4 +29,6 @@ sub send_questionnaires { return 0; } +sub bypass_password_checks { 1 } + 1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 7888f8ccf..c6ca5c56b 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -59,6 +59,14 @@ sub path_to_email_templates { return $paths; } +=item password_minimum_length + +Returns the minimum length a password can be set to. + +=cut + +sub password_minimum_length { 6 } + =item country Returns the country that this cobrand operates in, as an ISO3166-alpha2 code. diff --git a/t/app/controller/auth.t b/t/app/controller/auth.t index 8d60137a2..bec8698d5 100644 --- a/t/app/controller/auth.t +++ b/t/app/controller/auth.t @@ -5,7 +5,7 @@ my $mech = FixMyStreet::TestMech->new; my $test_email = 'test@example.com'; my $test_email3 = 'newuser@example.org'; -my $test_password = 'foobar'; +my $test_password = 'foobar123'; END { done_testing(); @@ -276,3 +276,26 @@ subtest "check logging in with token" => sub { $mech->delete_header('Authorization'); }; + +subtest 'check password length/common' => sub { + $mech->get_ok('/auth'); + $mech->submit_form_ok({ + form_name => 'general_auth', + fields => { username => $test_email, password_register => 'short' }, + button => 'sign_in_by_code', + }); + $mech->content_contains("Please make sure your password is at least"); + $mech->submit_form_ok({ + form_name => 'general_auth', + fields => { username => $test_email, password_register => 'common' }, + button => 'sign_in_by_code', + }); + $mech->content_contains("Please choose a less commonly-used password"); +}; + +subtest 'check common password AJAX call' => sub { + $mech->post_ok('/auth/common_password', { password_register => 'password' }); + $mech->content_contains("Please choose a less commonly-used password"); + $mech->post_ok('/auth/common_password', { password_register => 'squirblewirble' }); + $mech->content_contains("true"); +}; diff --git a/t/app/controller/auth_profile.t b/t/app/controller/auth_profile.t index 74edccfe6..224365eb6 100644 --- a/t/app/controller/auth_profile.t +++ b/t/app/controller/auth_profile.t @@ -8,7 +8,7 @@ LWP::Protocol::PSGI->register($twilio->to_psgi_app, host => 'api.twilio.com'); my $test_email = 'test@example.com'; my $test_email2 = 'test@example.net'; -my $test_password = 'foobar'; +my $test_password = 'foobar123'; END { done_testing(); @@ -88,6 +88,20 @@ subtest "Test change password page" => sub { ok $user->password, "user now has a password"; }; +subtest 'check password length/common' => sub { + $mech->get_ok('/auth/change_password'); + $mech->submit_form_ok({ + form_name => 'change_password', + fields => { new_password => 'short', confirm => 'short' }, + }); + $mech->content_contains("Please make sure your password is at least"); + $mech->submit_form_ok({ + form_name => 'change_password', + fields => { new_password => 'common', confirm => 'common' }, + }); + $mech->content_contains("Please choose a less commonly-used password"); +}; + subtest "Test change email page" => sub { $mech->create_problems_for_body(1, 2514, 'Title1', { user => FixMyStreet::DB->resultset('User')->find( { email => $test_email } ) } ); diff --git a/templates/web/base/auth/change_password.html b/templates/web/base/auth/change_password.html index a32dbaf9c..543a33ed5 100644 --- a/templates/web/base/auth/change_password.html +++ b/templates/web/base/auth/change_password.html @@ -19,7 +19,9 @@ INCLUDE 'header.html', title = loc('Change password'), bodyclass = bclass
- [% IF password_error; + [% IF password_error == 'failed' %] +
[% field_errors.password_register %]
+ [% ELSIF password_error; errors = { missing => loc('Please enter a password'), diff --git a/templates/web/base/auth/general.html b/templates/web/base/auth/general.html index 8fc5578c1..1e44bb68e 100644 --- a/templates/web/base/auth/general.html +++ b/templates/web/base/auth/general.html @@ -60,7 +60,9 @@ [% INCLUDE form_sign_in_yes %] [% ELSE %] + [% IF NOT field_errors.password_register %] [% INCLUDE form_sign_in_yes %] + [% END %] [% INCLUDE form_sign_in_no %] [% END %] @@ -114,14 +116,22 @@ + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a name and password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]

- +
+ +
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+ [% END %] diff --git a/templates/web/base/js/translation_strings.html b/templates/web/base/js/translation_strings.html index ed95335a6..a2e3c16c5 100644 --- a/templates/web/base/js/translation_strings.html +++ b/templates/web/base/js/translation_strings.html @@ -1,4 +1,7 @@ [% FILTER collapse %] +var fixmystreet = fixmystreet || {}; +fixmystreet.password_minimum_length = [% c.cobrand.password_minimum_length %]; + translation_strings = { update: '[% loc('Please enter a message') | replace("'", "\\'") %]', title: '[% loc('Please enter a subject') | replace("'", "\\'") %]', @@ -19,6 +22,9 @@ password_sign_in: { required: '[% loc('Please enter a password') | replace("'", "\\'") %]' }, + password_register: { + short: '[% tprintf(loc('Please make sure your password is at least %d characters long'), c.cobrand.password_minimum_length) | replace("'", "\\'") %]', + }, phone: { required: '[% loc('Please enter your phone number') | replace("'", "\\'") %]' }, diff --git a/templates/web/base/report/new/form_user_loggedout_by_email.html b/templates/web/base/report/new/form_user_loggedout_by_email.html index e9519f573..975dbe704 100644 --- a/templates/web/base/report/new/form_user_loggedout_by_email.html +++ b/templates/web/base/report/new/form_user_loggedout_by_email.html @@ -36,13 +36,20 @@ - + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]

- +
+ +
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+ diff --git a/templates/web/base/report/update/form_user_loggedout_by_email.html b/templates/web/base/report/update/form_user_loggedout_by_email.html index 7d10fe391..d038cdb23 100644 --- a/templates/web/base/report/update/form_user_loggedout_by_email.html +++ b/templates/web/base/report/update/form_user_loggedout_by_email.html @@ -8,14 +8,21 @@ [% INCLUDE 'report/update/form_name.html' %] + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]

- +
+
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+ diff --git a/templates/web/bromley/report/new/form_user.html b/templates/web/bromley/report/new/form_user.html index e6749f5ab..cce985c95 100644 --- a/templates/web/bromley/report/new/form_user.html +++ b/templates/web/bromley/report/new/form_user.html @@ -91,15 +91,23 @@ + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a password is optional, but doing so will allow you to more easily report future problems, leave updates and manage your reports.') %]

- +
+ +
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+
diff --git a/templates/web/bromley/report/update-form.html b/templates/web/bromley/report/update-form.html index 45d7aed5e..9778a0db3 100644 --- a/templates/web/bromley/report/update-form.html +++ b/templates/web/bromley/report/update-form.html @@ -98,15 +98,23 @@ [% INCLUDE 'report/update/form_name.html' %] + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]

- +
+ +
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+
Confirm my report with my FixMyStreet password
diff --git a/templates/web/fixamingata/report/new/form_user_loggedout.html b/templates/web/fixamingata/report/new/form_user_loggedout.html index 1f7cf2aeb..7505730e5 100644 --- a/templates/web/fixamingata/report/new/form_user_loggedout.html +++ b/templates/web/fixamingata/report/new/form_user_loggedout.html @@ -34,16 +34,25 @@
+ [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]

[% loc('Providing a password is optional, but doing so will allow you to more easily report problems, leave updates and manage your reports.') %]

- +
+ +
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+ +
Jag har ett lösenord sedan tidigare:
diff --git a/templates/web/zurich/auth/general.html b/templates/web/zurich/auth/general.html index 899f0ca71..555a72374 100644 --- a/templates/web/zurich/auth/general.html +++ b/templates/web/zurich/auth/general.html @@ -1,16 +1,6 @@ [% INCLUDE 'header.html', title = loc('Sign in or create an account') %] -[% IF username_error; - - # other keys include fqdn, mxcheck if you'd like to write a custom error message - - errors = { - missing_email = loc('Please enter your email'), - other_email = loc('Please check your email address is correct') - }; - - loc_username_error = errors.$username_error || errors.other_email; -END %] +[% loc_username_error = INCLUDE 'auth/_username_error.html' default='email' %]
@@ -61,11 +51,18 @@ END %] + [% IF field_errors.password_register %] +

[% field_errors.password_register %]

+ [% END %]
- +
+
+

[% tprintf(loc('Your password should include %d or more characters.'), c.cobrand.password_minimum_length) %]

+
+
diff --git a/web/cobrands/fixmystreet/fixmystreet.js b/web/cobrands/fixmystreet/fixmystreet.js index a3ac5b71a..0aa01e483 100644 --- a/web/cobrands/fixmystreet/fixmystreet.js +++ b/web/cobrands/fixmystreet/fixmystreet.js @@ -285,6 +285,9 @@ $.extend(fixmystreet.set_up, { if (jQuery.validator) { jQuery.validator.addMethod('validCategory', function(value, element) { return this.optional(element) || value != '-- Pick a category --'; }, translation_strings.category ); + jQuery.validator.addMethod('js-password-validate', function(value, element) { + return !value || value.length >= fixmystreet.password_minimum_length; + }, translation_strings.password_register.short); } var submitted = false; diff --git a/web/js/validation_rules.js b/web/js/validation_rules.js index 5295a53ca..e6d745336 100644 --- a/web/js/validation_rules.js +++ b/web/js/validation_rules.js @@ -1,5 +1,11 @@ validation_rules = { title: { required: true }, detail: { required: true }, - update: { required: true } + update: { required: true }, + password_register: { + remote: { + url: '/auth/common_password', + type: 'post' + } + } }; -- cgit v1.2.3