use Encode qw( encode is_utf8 );
use Fcntl qw/O_RDONLY/; # O_RDONLY is used in generate_salt
use List::MoreUtils qw/ any /;
+use String::Random qw( random_string );
+
+use C4::Context;
use base 'Exporter';
return $string;
}
+=head2 is_password_valid
+
+my ( $is_valid, $error ) = is_password_valid( $password );
+
+return $is_valid == 1 if the password match minPasswordLength and RequireStrongPassword conditions
+otherwise return $is_valid == 0 and $error will contain the error ('too_short' or 'too_weak')
+
+=cut
+
+sub is_password_valid {
+ my ($password) = @_;
+ my $minPasswordLength = C4::Context->preference('minPasswordLength');
+ $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
+ if ( length($password) < $minPasswordLength ) {
+ return ( 0, 'too_short' );
+ }
+ elsif ( C4::Context->preference('RequireStrongPassword') ) {
+ return ( 0, 'too_weak' )
+ if $password !~ m|(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{$minPasswordLength,}|;
+ }
+ return ( 0, 'has_whitespaces' ) if $password =~ m[^\s|\s$];
+ return ( 1, undef );
+}
+
+=head2 generate_password
+
+my password = generate_password();
+
+Generate a password according to the minPasswordLength and RequireStrongPassword.
+
+=cut
+
+sub generate_password {
+ my $minPasswordLength = C4::Context->preference('minPasswordLength');
+ $minPasswordLength = 8 if not $minPasswordLength or $minPasswordLength < 8;
+
+ my ( $password, $is_valid );
+ do {
+ $password = random_string('.' x $minPasswordLength );
+ ( $is_valid, undef ) = is_password_valid( $password );
+ } while not $is_valid;
+ return $password;
+}
+
+
=head2 get_script_name
This returns the correct script name, for use in redirecting back to the correct page after showing
[% IF ( BADUSERID ) %]
<li>You have entered a username that already exists. Please choose another one.</li>
[% END %]
- [% IF ( SHORTPASSWORD ) %]
- <li><strong>The password entered is too short</strong>. Password must be at least [% minPasswordLength %] characters.</li>
- [% END %]
+ [% IF ( ERROR_password_too_short ) %]
+ <li id="ERROR_short_password">Password must be at least [% minPasswordLength %] characters long.</li>
+ [% END %]
+ [% IF ( ERROR_password_too_weak ) %]
+ <li id="ERROR_weak_password">Password must contain at least one digit, one lowercase and one uppercase.</li>
+ [% END %]
+ [% IF ( ERROR_password_has_whitespaces ) %]
+ <li id="ERROR_weak_password">Password must not contain leading or trailing whitespaces.</li>
+ [% END %]
[% IF ( NOPERMISSION ) %]
<li>You do not have permission to edit this patron's login information.</li>
[% END %]
[% IF ( ERROR_dateexpiry ) %]
<li id="ERROR_dateexpiry">Date of expiration is invalid.</li>
[% END %]
- [% IF ( ERROR_short_password ) %]
- <li id="ERROR_short_password">Password must be at least [% minPasswordLength %] characters long.</li>
- [% END %]
+ [% IF ( ERROR_password_too_short ) %]
+ <li id="ERROR_short_password">Password must be at least [% minPasswordLength %] characters long.</li>
+ [% END %]
+ [% IF ( ERROR_password_too_weak ) %]
+ <li id="ERROR_weak_password">Password must contain at least one digit, one lowercase and one uppercase.</li>
+ [% END %]
+ [% IF ( ERROR_password_has_whitespaces ) %]
+ <li id="ERROR_weak_password">Password must not contain leading or trailing whitespaces.</li>
+ [% END %]
[% IF ( ERROR_password_mismatch ) %]
<li id="ERROR_password_mismatch">Passwords do not match.</li>
[% END %]
[% END %]
[% END %]
[% END %]
- [% IF ( mandatorypassword ) %]<span class="required">Required</span>[% END %][% IF ( ERROR_short_password ) %]<span class="required">Password is too short</span>[% END %]
- <div class="hint">Minimum password length: [% minPasswordLength %]</div>
+ [% IF ( mandatorypassword ) %]<span class="required">Required</span>[% END %]
+ [% IF ( ERROR_password_too_short ) %]<span class="required">Password is too short</span>[% END %]
+ [% IF ( ERROR_password_too_weak ) %]<span class="required">Password is too weak</span>[% END %]
+ [% IF ( ERROR_password_has_whitespaces ) %]<span class="required">Password has leading or trailing whitespaces</span>[% END %]
+ <div class="hint">Minimum password length: [% minPasswordLength %]</div>
</li>
<li>
[% IF ( mandatorypassword ) %]
[% IF field == "emailpro" %]<li>Contact information: <a href="#borrower_emailpro">secondary email address</a></li>[% END %]
[% IF field == "B_email" %]<li>Alternate address information: <a href="#borrower_B_email">email address</a></li>[% END %]
[% IF field == "password_match" %]<li>Passwords do not match! <a href="#password">password</a></li>[% END %]
- [% IF field == "password_invalid" %]<li>Password does not meet minimum requirements! <a href="#password">password</a></li>[% END %]
- [% IF field == "password_spaces" %]<li>Password contains leading and/or trailing spaces! <a href="#password">password</a></li>[% END %]
+ [% IF field == "password_too_short" %]
+ <li>Password must be at least [% minPasswordLength %] characters long.</li>
+ [% END %]
+ [% IF field == "password_too_weak" %]
+ <li>Password must contain at least one digit, one lowercase and one uppercase.</li>
+ [% END %]
+ [% IF field == "password_has_whitespaces" %]
+ <li>Password must not contain leading or trailing whitespaces.</li>
+ [% END %]
[% IF field == "duplicate_email" %]
<li>This email address already exists in our database.</li>
[% END %]
<div class="alert">
<h3>There was a problem with your submission</h3>
<p>
- [% IF ( PassMismatch ) %]
+ [% IF ( passwords_mismatch ) %]
Passwords do not match. Please re-type your new password.
[% END %]
- [% IF ( ShortPass ) %]
- Your new password must be at least [% minPasswordLength%] characters long.
+ [% IF password_too_short %]
+ Password must be at least [% minPasswordLength %] characters long.
[% END %]
+ [% IF password_too_weak %]
+ Password must contain at least one digit, one lowercase and one uppercase.
+ [% END %]
+ [% IF password_has_whitespaces %]
+ Password must not contain leading or trailing whitespaces.
+ [% END %]
+
[% IF ( WrongPass ) %]
Your current password was entered incorrectly. If this problem persists, please ask a librarian to reset your password for you.
[% END %]
- [% IF PasswordContainsTrailingSpaces %]
- Your password contains leading and/or trailing spaces.
- [% END %]
</p>
</div>
[% END # /IF Error_messages %]
<br/>If you did not receive this email, you can request a new one: <a href="/cgi-bin/koha/opac-password-recovery.pl?resendEmail=true&email=[% email %]&username=[% username %]">Get new password recovery link</a>
[% ELSIF (errPassNotMatch) %]
The passwords do not match.
- [% ELSIF (errPassTooShort) %]
- Your chosen password is too short.
- <br/>The password must contain at least [% minPassLength %] characters.
+ [% ELSIF password_too_short %]
+ <li>Password must be at least [% minPasswordLength %] characters long.</li>
+ [% ELSIF password_too_weak %]
+ <li>Password must contain at least one digit, one lowercase and one uppercase.</li>
+ [% ELSIF password_has_whitespaces %]
+ <li>Password must not contain leading or trailing whitespaces.</li>
[% ELSIF (errLinkNotValid) %]
The link you clicked is either invalid, or expired.
<br/>Be sure you used the link from the email, or contact library staff for assistance.
<form action="/cgi-bin/koha/opac-password-recovery.pl" method="post" autocomplete="off">
<input type="hidden" name="koha_login_context" value="opac" />
<fieldset>
- <div class="alert alert-info">The password must contain at least [% minPassLength %] characters.</div>
+ <div class="alert alert-info">The password must contain at least [% minPasswordLength %] characters.</div>
<label for="password">New password:</label>
<input type="password" id="password" size="40" name="password" />
<label for="repeatPassword">Confirm new password:</label>
use C4::Circulation;
use CGI qw ( -utf8 );
use C4::Members::Attributes qw(GetBorrowerAttributes);
+use Koha::AuthUtils;
use Koha::Token;
use Koha::Patrons;
push( @errors, 'NOMATCH' ) if ( ( $newpassword && $newpassword2 ) && ( $newpassword ne $newpassword2 ) );
-my $minpw = C4::Context->preference('minPasswordLength');
-$minpw = 3 if not $minpw or $minpw < 3;
-push( @errors, 'SHORTPASSWORD' ) if ( $newpassword && $minpw && ( length($newpassword) < $minpw ) );
+if ( $newpassword and not @errors ) {
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $newpassword );
+ unless ( $is_valid ) {
+ push @errors, 'ERROR_password_too_short' if $error eq 'too_short';
+ push @errors, 'ERROR_password_too_weak' if $error eq 'too_weak';
+ push @errors, 'ERROR_password_has_whitespaces' if $error eq 'has_whitespaces';
+ }
+}
-if ( $newpassword && !scalar(@errors) ) {
+if ( $newpassword and not @errors) {
die "Wrong CSRF token"
unless Koha::Token->new->check_csrf({
use C4::Log;
use C4::Letters;
use C4::Form::MessagingPreferences;
+use Koha::AuthUtils;
use Koha::AuthorisedValues;
use Koha::Patron::Debarments;
use Koha::Cities;
unless (Check_Userid($userid,$borrowernumber)) {
push @errors, "ERROR_login_exist";
}
-
+
my $password = $input->param('password');
my $password2 = $input->param('password2');
push @errors, "ERROR_password_mismatch" if ( $password ne $password2 );
- my $minpw = C4::Context->preference('minPasswordLength');
- $minpw = 3 if not $minpw or $minpw < 3;
- push @errors, "ERROR_short_password" if( $password && $minpw && $password ne '****' && (length($password) < $minpw) );
+
+ if ( $password and $password ne '****' ) {
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $password );
+ unless ( $is_valid ) {
+ push @errors, 'ERROR_password_too_short' if $error eq 'too_short';
+ push @errors, 'ERROR_password_too_weak' if $error eq 'too_weak';
+ push @errors, 'ERROR_password_has_whitespaces' if $error eq 'has_whitespaces';
+ }
+ }
# Validate emails
my $emailprimary = $input->param('email');
use C4::Members;
use C4::Members::Attributes qw( GetBorrowerAttributes );
use C4::Form::MessagingPreferences;
+use Koha::AuthUtils;
+use Koha::Patrons;
+use Koha::Patron::Modification;
+use Koha::Patron::Modifications;
use C4::Scrubber;
use Email::Valid;
use Koha::DateUtils;
$verification_token = md5_hex( time().{}.rand().{}.$$ );
}
- $borrower{password} = random_string("..........");
+ $borrower{password} = Koha::AuthUtils::generate_password;
$borrower{verification_token} = $verification_token;
Koha::Patron::Modification->new( \%borrower )->store();
}
sub CheckForInvalidFields {
- my $minpw = C4::Context->preference('minPasswordLength');
- $minpw = 3 if not $minpw or $minpw < 3;
my $borrower = shift;
my @invalidFields;
if ($borrower->{'email'}) {
{
push( @invalidFields, "password_match" );
}
- if ( $borrower->{'password'} && $minpw && (length($borrower->{'password'}) < $minpw) ) {
- push(@invalidFields, "password_invalid");
- }
if ( $borrower->{'password'} ) {
- push(@invalidFields, "password_spaces") if ($borrower->{'password'} =~ /^\s/ or $borrower->{'password'} =~ /\s$/);
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $borrower->{password} );
+ unless ( $is_valid ) {
+ push @invalidFields, 'password_too_short' if $error eq 'too_short';
+ push @invalidFields, 'password_too_weak' if $error eq 'too_weak';
+ push @invalidFields, 'password_has_whitespaces' if $error eq 'has_whitespaces';
+ }
}
return \@invalidFields;
);
my $patron = Koha::Patrons->find( $borrowernumber );
-my $minpasslen = C4::Context->preference("minPasswordLength");
-$minpasslen = 3 if not $minpasslen or $minpasslen < 3;
if ( C4::Context->preference("OpacPasswordChange") ) {
my $sth = $dbh->prepare("UPDATE borrowers SET password = ? WHERE borrowernumber=?");
if ( $query->param('Oldkey')
&& $query->param('Newkey')
&& $query->param('Confirm') )
{
+ my $error;
+ my $new_password = $query->param('Newkey');
+ my $confirm_password = $query->param('Confirm');
if ( goodkey( $dbh, $borrowernumber, $query->param('Oldkey') ) ) {
- if ( $query->param('Newkey') =~ m|^\s+| or $query->param('Newkey') =~ m|\s+$| ) {
- $template->param(
- Error_messages => 1,
- PasswordContainsTrailingSpaces => 1,
- );
- }
- elsif ( $query->param('Newkey') eq $query->param('Confirm')
- && length( $query->param('Confirm') ) >= $minpasslen )
- { # Record password
- my $clave = hash_password( $query->param('Newkey') );
- $sth->execute( $clave, $borrowernumber );
- $template->param( 'password_updated' => '1' );
- $template->param( 'borrowernumber' => $borrowernumber );
- }
- elsif ( $query->param('Newkey') ne $query->param('Confirm') ) {
- $template->param( 'Ask_data' => '1' );
- $template->param( 'Error_messages' => '1' );
- $template->param( 'PassMismatch' => '1' );
- }
- elsif ( length( $query->param('Confirm') ) < $minpasslen ) {
+
+ if ( $new_password ne $confirm_password ) {
$template->param( 'Ask_data' => '1' );
$template->param( 'Error_messages' => '1' );
- $template->param( 'ShortPass' => '1' );
- }
- else {
- $template->param( 'Error_messages' => '1' );
+ $template->param( 'passwords_mismatch' => '1' );
+ } else {
+ my ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( $new_password );
+ unless ( $is_valid ) {
+ $error = 'password_too_short' if $error eq 'too_short';
+ $error = 'password_too_weak' if $error eq 'too_weak';
+ $error = 'password_has_whitespaces' if $error eq 'has_whitespaces';
+ } else {
+ # Password is valid and match
+ my $clave = hash_password( $new_password );
+ $sth->execute( $clave, $borrowernumber );
+ $template->param( 'password_updated' => '1' );
+ $template->param( 'borrowernumber' => $borrowernumber );
+ }
}
}
else {
- $template->param( 'Ask_data' => '1' );
- $template->param( 'Error_messages' => '1' );
- $template->param( 'WrongPass' => '1' );
+ $error = 'WrongPass';
+ }
+ if ($error) {
+ $template->param(
+ Ask_data => 1,
+ Error_messages => 1,
+ $error => 1,
+ );
+
}
}
else {
#new password form error
my $errLinkNotValid;
-my $errPassNotMatch;
-my $errPassTooShort;
if ( $query->param('sendEmail') || $query->param('resendEmail') ) {
elsif ( $query->param('passwordReset') ) {
( $borrower_number, $username ) = GetValidLinkInfo($uniqueKey);
- my $minPassLength = C4::Context->preference('minPasswordLength');
- $minPassLength = 3 if not $minPassLength or $minPassLength < 3;
- #validate password length & match
- if ( ($borrower_number)
- && ( $password eq $repeatPassword )
- && ( length($password) >= $minPassLength ) )
- { #apply changes
- Koha::Patrons->find($borrower_number)->update_password( $username, hash_password($password) );
- CompletePasswordRecovery($uniqueKey);
- $template->param(
- password_reset_done => 1,
- username => $username
- );
- }
- else { #errors
- if ( !$borrower_number ) { #parameters not valid
- $errLinkNotValid = 1;
- }
- elsif ( $password ne $repeatPassword ) { #passwords does not match
- $errPassNotMatch = 1;
- }
- elsif ( length($password) < $minPassLength ) { #password too short
- $errPassTooShort = 1;
+ my $error;
+ if ( not $borrower_number ) {
+ $error = 'errLinkNotValid';
+ } elsif ( $password ne $repeatPassword ) {
+ $error = 'errPassNotMatch';
+ } else {
+ my ( $is_valid, $err) = Koha::AuthUtils::is_password_valid( $password );
+ unless ( $is_valid ) {
+ $error = 'password_too_short' if $err eq 'too_short';
+ $error = 'password_too_weak' if $err eq 'too_weak';
+ $error = 'password_has_whitespaces' if $err eq 'has_whitespaces';
+ } else {
+ Koha::Patrons->find($borrower_number)->update_password( $username, hash_password($password) );
+ CompletePasswordRecovery($uniqueKey);
+ $template->param(
+ password_reset_done => 1,
+ username => $username
+ );
}
+ }
+ if ( $error ) {
$template->param(
- new_password => 1,
- email => $email,
- uniqueKey => $uniqueKey,
- errLinkNotValid => $errLinkNotValid,
- errPassNotMatch => $errPassNotMatch,
- errPassTooShort => $errPassTooShort,
- hasError => 1
+ new_password => 1,
+ email => $email,
+ uniqueKey => $uniqueKey,
+ hasError => 1,
+ $error => 1,
);
}
}
# This file is part of Koha.
#
# Copyright (C) 2013 Equinox Software, Inc.
+# Copyright 2017 Koha Development Team
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
-use Test::More tests => 1;
+use Test::More tests => 3;
+use t::lib::Mocks;
use Koha::AuthUtils qw/hash_password/;
my $hash1 = hash_password('password');
my $hash2 = hash_password('password');
ok($hash1 ne $hash2, 'random salts used when generating password hash');
+
+subtest 'is_password_valid' => sub {
+ plan tests => 12;
+
+ my ( $is_valid, $error );
+
+ t::lib::Mocks::mock_preference('RequireStrongPassword', 0);
+ t::lib::Mocks::mock_preference('minPasswordLength', 0);
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '12' );
+ is( $is_valid, 0, 'min password size should be 3' );
+ is( $error, 'too_short', 'min password size should be 3' );
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( ' 123' );
+ is( $is_valid, 0, 'password should not contain leading spaces' );
+ is( $error, 'has_whitespaces', 'password should not contain leading spaces' );
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '123 ' );
+ is( $is_valid, 0, 'password should not contain trailing spaces' );
+ is( $error, 'has_whitespaces', 'password should not contain trailing spaces' );
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '123' );
+ is( $is_valid, 1, 'min password size should be 3' );
+
+ t::lib::Mocks::mock_preference('RequireStrongPassword', 1);
+ t::lib::Mocks::mock_preference('minPasswordLength', 8);
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( '12345678' );
+ is( $is_valid, 0, 'password should be strong' );
+ is( $error, 'too_weak', 'password should be strong' );
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'abcd1234' );
+ is( $is_valid, 0, 'strong password should contain uppercase' );
+ is( $error, 'too_weak', 'strong password should contain uppercase' );
+
+ ( $is_valid, $error ) = Koha::AuthUtils::is_password_valid( 'abcD1234' );
+ is( $is_valid, 1, 'strong password should contain uppercase' );
+};
+
+subtest 'generate_password' => sub {
+ plan tests => 1;
+ t::lib::Mocks::mock_preference('RequireStrongPassword', 1);
+ t::lib::Mocks::mock_preference('minPasswordLength', 8);
+ my $all_valid = 1;
+ for ( 1 .. 10 ) {
+ my $password = Koha::AuthUtils::generate_password;
+ my ( $is_valid, undef ) = Koha::AuthUtils::is_password_valid( $password );
+ $all_valid = 0 unless $is_valid;
+ }
+ is ( $all_valid, 1, 'generate_password should generate valid passwords' );
+};