From 5743977bd36865059cb65232d4cc2051907808a3 Mon Sep 17 00:00:00 2001 From: Nick Clemens <nick@bywatersolutions.com> Date: Wed, 26 Jan 2022 12:10:49 +0000 Subject: [PATCH] Bug 29925: Add a password reset page for expired passwords This patch adds a new page opac-reset-password where a user cna enter their login (userid or carndumber), current password, and new password. If the user has a password expiration date and the current password is correct and the new passwords match and meet requirements their password will be updated and the expiration date reset A patron whose password does not expire will be reidrected to login to change their password To test: 1 - Apply patch, updatedatabase, enable new syspref EnableExpiredPasswordReset 2 - Set 'Password expiration' for a patron category Home->Administration->Patron categories->Edit 3 - Create a new patron in this category with a userid/password set, and an email 4 - Update the patron with an expiration to be expired UPDATE borrowers SET password_expiration='2022-01-01' WHERE borrowernumber=51; 5 - Give the borrower catalogue permission 6 - Attempt to log in to Straff interface 7 - Confirm you are signed out and notified that password must be reset 8 - Click 'Reset your password' link 9 - You should see the reset password page with fields for: login, current password, new password, conmfirm password 10 - enter invalid/incomplete credentials 11 - Confirm you are notified of invlaid credentials 12 - Fill in all fields, but enter current password as new password 13 - Confirm you are notified of no change 14 - Set minimum password length / strong password requirement for category 15 - Confirm you receive error if new password too short or not secure 16 - Enter a valid new password and submit and confirm update is successful 17 - Confirm you have buttons to go to OPAC or Staff and that both work 18 - Confirm you cna log in (i.e. expiration has been reset) 19 - Expire the users password 20 - Remove catalogue permission 21 - Reset password again and confirm only OPAC link Signed-off-by: Bob Bennhoff <bbennhoff@clicweb.org> Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> --- .../atomicupdate/bug_29925_password_reset.pl | 15 ++ .../en/modules/admin/preferences/patrons.pref | 8 + .../intranet-tmpl/prog/en/modules/auth.tt | 4 +- .../bootstrap/en/modules/opac-auth.tt | 6 +- .../en/modules/opac-reset-password.tt | 157 ++++++++++++++++++ opac/opac-reset-password.pl | 107 ++++++++++++ 6 files changed, 294 insertions(+), 3 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/bug_29925_password_reset.pl create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt create mode 100755 opac/opac-reset-password.pl diff --git a/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl b/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl new file mode 100755 index 0000000000..67e2628480 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_29925_password_reset.pl @@ -0,0 +1,15 @@ +use Modern::Perl; + +return { + bug_number => "29925", + description => "Add password reset option", + up => sub { + my ($args) = @_; + my ($dbh, $out) = @$args{qw(dbh out)}; + $dbh->do(q{ + INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES + ('EnableExpiredPasswordReset', '0', NULL, 'Enable ability for patrons with expired password to reset their password directly', 'YesNo') + }); + say $out "Added EnableExpirePasswordReset system preference"; + }, +}; diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref index 76ebd9fd09..871c63722f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -365,6 +365,14 @@ Patrons: 0: "Don't require" - a strong password for staff and patrons - (must contain at least one digit, one lowercase and one uppercase). + - + - pref: EnableExpiredPasswordReset + choices: + 1: "Enable" + 0: "Don't enable" + - the ability for patrons to directly reset their password when it is expired. + - If not enable patrons must either use the 'Forgot your password' feature or have staff + - reset their password. - - Block a patron's account if it reaches - pref: FailedLoginAttempts diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt index 4dd1505c04..531235034b 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt @@ -52,7 +52,9 @@ [% END %] [% ELSIF password_has_expired %] <div id="login_error"><strong>Error: </strong>Your password has expired!</div> - [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %] + [% IF Koha.Preference('EnableExpiredPasswordReset') && Koha.Preference('OpacBaseURL') %] + <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-reset-password.pl">You must reset your password</a>. + [% ELSIF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %] <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-password-recovery.pl">You must reset your password</a>. [% ELSE %] <p>You must contact the library to have your password reset</p> diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt index acba29d1b0..0fd0d32bba 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -171,9 +171,11 @@ <div class="alert alert-info"> <p><strong>Error: </strong>Your password has expired!</p> </div> - [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %] + [% IF Koha.Preference('EnableExpiredPasswordReset') %] + <a href="/cgi-bin/koha/opac-reset-password.pl">Reset your password</a>. + [% ELSIF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %] <div id="resetpassword"> - <a href="/cgi-bin/koha/opac-password-recovery.pl">Reset your password?</a> + <a href="/cgi-bin/koha/opac-password-recovery.pl">Reset your password</a> </div> [% ELSE %] <p>You must contact the library to reset your password</p> diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt new file mode 100644 index 0000000000..c770189a48 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reset-password.tt @@ -0,0 +1,157 @@ +[% USE raw %] +[% USE Koha %] +[% USE Categories %] +[% USE AdditionalContents %] +[% PROCESS 'html_helpers.inc' %] +[% INCLUDE 'doc-head-open.inc' %] +<title> + Reset your password + › + [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog +</title> +[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] +</head> +[% INCLUDE 'bodytag.inc' bodyid='opac-login-page' bodyclass='scrollto' %] +[% INCLUDE 'masthead.inc' %] + +<div class="main"> + <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a href="/cgi-bin/koha/opac-main.pl">Home</a> + </li> + <li class="breadcrumb-item active"> + <a href="#" aria-current="page">Reset your password</a> + </li> + </ + </nav> <!-- /#breadcrumbs --> + + <div class="container-fluid"> + <div class="row justify-content-center"> + <div class="col-md-10 col-lg-6"> + <div id="opac-auth" class="maincontent"> + [% IF Koha.Preference( 'opacuserlogin' ) && Koha.Preference('EnableExpiredPasswordReset') %] + [% IF ( error ) %] + <div class="alert alert-warning"> + <h2>There was a problem with your submission</h2> + <p> + [% SWITCH error %] + [% CASE 'no_change' %] + New password must not be the same as old password. + [% CASE 'passwords_mismatch' %] + Passwords do not match. Please re-type your new password. + [% CASE 'password_too_short' %] + Password must be at least [% minPasswordLength | html %] characters long. + [% CASE 'password_too_weak' %] + Password must contain at least one digit, one lowercase and one uppercase. + [% CASE 'password_has_whitespaces' %] + Password must not contain leading or trailing whitespaces. + [% CASE 'invalid_credentials' %] + You entered an incorrect username or password. Please try again! But note that passwords are case sensitive[% IF Koha.Preference('FailedLoginAttempts') %] and that your account will be locked out after a fixed number of failed login attempts[% END %]. Please contact a library staff member if you continue to have problems. + [% CASE 'no_expire' %] + Please log-in to account to update your password.</br> + <a href="/cgi-bin/koha/opac-user.pl" class="nav-link login-link loginModal-trigger"><i class="fa fa-user fa-icon-black fa-fw" aria-hidden="true"></i> <span class="userlabel">Log in to your account</span></a> + [% CASE 'account_locked' %] + This account has been locked! + [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %] + <a href="[% Koha.Preference('OpacBaseURL') | url %]/cgi-bin/koha/opac-password-recovery.pl">You must reset your password via e-mail</a>. + [% ELSE %] + You must contact the library for assistance + [% END %] + [% CASE %] + An unknown error occurred. PLease try again or contact the library for assistance + [% END %] + </p> + </div> + [% END %] + [% IF ( password_updated ) %] + <div class="alert dialog-alert"> + <h2>Your password has successfully been updated</h2> + <a href="/cgi-bin/koha/opac-main.pl" class="btn btn-primary">Go to OPAC</a> + [% IF ( Koha.Preference('staffClientBaseURL') && staff_access ) %] + <a href="[% Koha.Preference('staffClientBaseURL') | url %]" class="btn btn-primary">Go to Staff client</a> + [% END %] + </div> + [% ELSE %] + <form action="/cgi-bin/koha/opac-reset-password.pl" name="mainform" id="mainform" method="post" autocomplete="off"> + <legend class="sr-only">Reset your password</legend> + + <fieldset class="brief"> + <div class="form-group"> + <label for="userid">Login:</label> + <input class="form-control" type="text" size="25" id="userid" name="userid" /> + </div> + <div class="form-group"> + <label for="currentpassword">Current password:</label> + <input class="form-control" autocomplete="off" type="password" size="25" id="currentpassword" name="currentpassword" /> + </div> + <div class="form-group"> + <label for="newpassword">New password:</label> + <input class="form-control" autocomplete="off" type="password" size="25" id="newpassword" name="newpassword" /> + </div> + <div class="form-group"> + <label for="confirmpassword">Confirm new password:</label> + <input class="form-control" autocomplete="off" type="password" size="25" id="confirmpassword" name="confirmpassword" /> + </div> + <fieldset class="action"> + <input type="submit" value="Update password" class="btn btn-primary" /> + </fieldset> + <input type="hidden" name="op" value="update" /> + </fieldset> + [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %] + <div id="resetpassword"> + <a href="/cgi-bin/koha/opac-password-recovery.pl">Forgot your password?</a> + </div> + [% END %] + </form> + [% END # /IF Error_messages %] + + [% ELSE %] + <h1>Resetting your password has not been enabled by the library.</h1> + [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %] + <div id="resetpassword"> + <a href="/cgi-bin/koha/opac-password-recovery.pl">Forgot your password?</a> + </div> + [% ELSE %] + <p>You must contact the library to reset your password</p> + [% END %] + [% END # / IF opacuserlogin %] + + </div> <!-- /.opac-auth --> + </div> <!-- /.col-md-10 col-lg-6 --> + </div> <!-- /.row --> + </div> <!-- /.container-fluid --> +</div> <!-- /.main --> + +[% INCLUDE 'opac-bottom.inc' %] +[% BLOCK jsinclude %] + [% Asset.js("lib/jquery/plugins/jquery.validate.min.js") | $raw %] + <script> + jQuery.validator.addMethod("password_no_spaces", function(value, element){ + return ( this.optional(element) || !value.match(/^\s/) && !value.match(/\s$/) ); + }, _("Password contains leading and/or trailing spaces")); + jQuery.validator.addMethod("password_match", function(value, element){ + var new_password_node = $("input[name='newpassword']:first"); + return value == $(new_password_node).val(); + }, _("Please enter the same password as above")); + + $(document).ready(function() { + $("#mainform").validate({ + rules: { + currentpassword: { + required: true, + }, + newpassword: { + required: true, + password_no_spaces: true + }, + confirmpassword: { + required: true, + password_match: true + } + } + }); + }); + </script> +[% END %] diff --git a/opac/opac-reset-password.pl b/opac/opac-reset-password.pl new file mode 100755 index 0000000000..2090ddae93 --- /dev/null +++ b/opac/opac-reset-password.pl @@ -0,0 +1,107 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see <http://www.gnu.org/licenses>. + +use Modern::Perl; + +use CGI qw ( -utf8 ); + +use C4::Auth qw( get_template_and_user checkpw checkpw_hash ); +use C4::Context; +use C4::Output qw( output_html_with_http_headers ); +use Koha::Patrons; + +use Try::Tiny qw( catch try ); + +my $query = CGI->new; + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { + template_name => "opac-reset-password.tt", + query => $query, + type => "opac", + authnotrequired => 1, + } +); + +my $op = $query->param('op'); + +if ( $op eq 'update' ) { + my $userid = $query->param('userid'); + my $currentpassword = $query->param('currentpassword'); + my $newpassword = $query->param('newpassword'); + my $confirmpassword = $query->param('confirmpassword'); + + my $patron = Koha::Patrons->find( { userid => $userid } ); + $patron = Koha::Patrons->find( { cardnumber => $userid } ) unless $patron; + + if ( $patron && $patron->password_expiration_date ) { + if ( $patron->account_locked ) { + $template->param( error => 'account_locked' ); + } + elsif ( $currentpassword && $newpassword && $confirmpassword ) { + my $error; + if ( C4::Auth::checkpw_hash( $currentpassword, $patron->password ) ) { + + if ( $newpassword ne $confirmpassword ) { + $template->param( 'error' => 'passwords_mismatch' ); + } + elsif ( $currentpassword eq $newpassword ) { + $template->param( 'error' => 'no_change' ); + } + else { + try { + $patron->set_password( { password => $newpassword } ); + $template->param( 'password_updated' => '1' ); + $template->param( 'staff_access' => 1 ) + if $patron->has_permission( { catalogue => 1 } ); + } + catch { + $error = 'password_too_short' + if $_->isa('Koha::Exceptions::Password::TooShort'); + $error = 'password_too_weak' + if $_->isa('Koha::Exceptions::Password::TooWeak'); + $error = 'password_has_whitespaces' + if $_->isa( + 'Koha::Exceptions::Password::WhitespaceCharacters'); + $template->param( 'error' => $error ); + }; + } + } + else { + $template->param( 'error' => 'invalid_credentials' ); + $patron->update( + { login_attempts => $patron->login_attempts + 1 } ) + if !$patron->account_locked; + } + } + else { + $template->param( 'incomplete_form' => '1' ); + } + } + elsif ( !$patron ) { + template->param( 'error' => 'invalid_credentials' ); + } + elsif ( !$patron->password_expiration_date ) { + $template->param( 'error' => 'no_expire' ); + } + else { + $template->param( 'error' => 'unknown' ); + } +} + +output_html_with_http_headers $query, $cookie, $template->output, undef, + { force_no_caching => 1 }; -- 2.36.0