From a732138d9d41438c430f0231d001b4d1087286e8 Mon Sep 17 00:00:00 2001 From: Nick Clemens Date: Fri, 21 Jan 2022 17:01:10 +0000 Subject: [PATCH] Bug 29924: Add password expiration feature This patch adds the ability to define password_expiry_days for a patron category. When defined a patron's password will expire after X days and they will be required to reset their password. If OPAC resets are enabled for the catgeory they may do so on their own, otherwise they will need to contact the library To test: 1 - Apply patch, updatedatabase 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 - Confirm their password_expiration_date field is set SELECT password_expiration_date FROM borrowers WHERE borrowernumber=51; 5 - Create a new patron, do not set a password 6 - Confirm their password_expiration_date field is NULL 7 - Update the patron with an expiration to be expired UPDATE borrowers SET password_expiration_date='2022-01-01' WHERE borrowernumber=51; 8 - Give the borrower catalogue permission 9 - Attempt to log in to Straff interface 10 - Confirm you are signed out and notified that password must be reset 11 - Attempt to sign in to OPAC 12 - Confirm you are signed out and notified password must be reset 13 - Enable password reset for the patron's category and perform a password reset Note: you will have to find the link in the message_queue unless you have emails setup on your test environment SELECT * FROM message_queue WHERE borrowernumber=51; 14 - Confirm that you can now sign in and password_expiration_date field is set 10 days in the future 15 - Expire the patron's password again 16 - Change the patron's password via the staff interface 17 - Confirm they can sign in and the expiration is updated Signed-off-by: Owen Leonard Signed-off-by: Bob Bennhoff Signed-off-by: Andrew Fuerste-Henry Signed-off-by: Tomas Cohen Arazi Signed-off-by: Fridolin Somers --- C4/Auth.pm | 12 ++++++++--- Koha/Patron.pm | 20 +++++++++++++++++++ Koha/Patron/Category.pm | 20 +++++++++++++++++++ Koha/REST/V1/Auth.pm | 7 ++++++- admin/categories.pl | 3 +++ .../prog/en/modules/admin/categories.tt | 11 ++++++++++ .../intranet-tmpl/prog/en/modules/auth.tt | 7 +++++++ .../bootstrap/en/modules/opac-auth.tt | 13 ++++++++++-- 8 files changed, 87 insertions(+), 6 deletions(-) diff --git a/C4/Auth.pm b/C4/Auth.pm index c1c27e0eb9..4007179cc3 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -966,6 +966,8 @@ sub checkauth { $info{oldip} = $more_info->{old_ip}; $info{newip} = $more_info->{new_ip}; $info{different_ip} = 1; + } elsif ( $return eq 'password_expired' ) { + $info{password_has_expired} = 1; } } } @@ -1404,7 +1406,8 @@ sub checkauth { PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"), PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"), opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'}, - too_many_login_attempts => ( $patron and $patron->account_locked ) + too_many_login_attempts => ( $patron and $patron->account_locked ), + password_has_expired => ( $patron and $patron->password_expired ) ); $template->param( SCO_login => 1 ) if ( $query->param('sco_user_login') ); @@ -1792,6 +1795,9 @@ sub check_cookie_auth { } elsif ( $userid ) { $session->param( 'lasttime', time() ); + my $patron = Koha::Patrons->find({ userid => $userid }); + $patron = Koha::Patron->find({ cardnumber => $userid }) unless $patron; + return ("password_expired", undef ) if $patron->password_expired; my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1; if ($flags) { C4::Context->_new_userenv($sessionID); @@ -1903,8 +1909,8 @@ sub checkpw { # 0 if auth is nok # -1 if user bind failed (LDAP only) - if ( $patron and $patron->account_locked ) { - # Nothing to check, account is locked + if ( $patron and ( $patron->account_locked || $patron->password_expired ) ) { + # Nothing to check, account is locked or password expired } elsif ($ldap && defined($password)) { my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_); # EXTERNAL AUTH if ( $retval == 1 ) { diff --git a/Koha/Patron.pm b/Koha/Patron.pm index f2d731d541..5eee7b9a7a 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -268,6 +268,9 @@ sub store { # Make a copy of the plain text password for later use $self->plain_text_password( $self->password ); + $self->password_expiration_date( $self->password + ? $self->category->get_password_expiry_date + : undef ); # Create a disabled account if no password provided $self->password( $self->password ? Koha::AuthUtils::hash_password( $self->password ) @@ -788,6 +791,21 @@ sub is_expired { return 0; } +=head3 password_expired + +my $password_expired = $patron->password_expired; + +Returns 1 if the patron's password is expired or 0; + +=cut + +sub password_expired { + my ($self) = @_; + return 0 unless $self->password_expiration_date; + return 1 if dt_from_string( $self->password_expiration_date ) < dt_from_string->truncate( to => 'day' ); + return 0; +} + =head3 is_going_to_expire my $is_going_to_expire = $patron->is_going_to_expire; @@ -887,6 +905,8 @@ sub set_password { my $digest = Koha::AuthUtils::hash_password($password); + $self->password_expiration_date( $self->category->get_password_expiry_date ); + # We do not want to call $self->store and retrieve password from DB $self->password($digest); $self->login_attempts(0); diff --git a/Koha/Patron/Category.pm b/Koha/Patron/Category.pm index 892d00c2ae..c6837ca00f 100644 --- a/Koha/Patron/Category.pm +++ b/Koha/Patron/Category.pm @@ -113,6 +113,26 @@ sub get_expiry_date { } } +=head3 get_password_expiry_date + +Returns date based on password expiry days set for the category. If the value is not set +we return undef, password does not expire + +my $expiry_date = $category->get_password_expiry_date(); + +=cut + +sub get_password_expiry_date { + my ($self, $date ) = @_; + if ( $self->password_expiry_days ) { + $date ||= dt_from_string; + $date = dt_from_string( $date ) unless ref $date; + return $date->add( days => $self->password_expiry_days )->ymd; + } else { + return; + } +} + =head3 effective_reset_password Returns if patrons in this category can reset their password. If set in $self->reset_password diff --git a/Koha/REST/V1/Auth.pm b/Koha/REST/V1/Auth.pm index d535770f6e..182db518f6 100644 --- a/Koha/REST/V1/Auth.pm +++ b/Koha/REST/V1/Auth.pm @@ -491,7 +491,12 @@ sub _basic_auth { Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Invalid password' ); } - return Koha::Patrons->find({ userid => $user_id }); + my $patron = Koha::Patrons->find({ userid => $user_id }); + if ( $patron->password_expired ) { + Koha::Exceptions::Authorization::Unauthorized->throw( error => 'Password has expired' ); + } + + return $patron; } =head3 _set_userenv diff --git a/admin/categories.pl b/admin/categories.pl index 368960f9cd..e41a28904d 100755 --- a/admin/categories.pl +++ b/admin/categories.pl @@ -63,6 +63,7 @@ elsif ( $op eq 'add_validate' ) { my $description = $input->param('description'); my $enrolmentperiod = $input->param('enrolmentperiod'); my $enrolmentperioddate = $input->param('enrolmentperioddate') || undef; + my $password_expiry_days = $input->param('password_expiry_days') || undef; my $upperagelimit = $input->param('upperagelimit'); my $dateofbirthrequired = $input->param('dateofbirthrequired'); my $enrolmentfee = $input->param('enrolmentfee'); @@ -103,6 +104,7 @@ elsif ( $op eq 'add_validate' ) { $category->description($description); $category->enrolmentperiod($enrolmentperiod); $category->enrolmentperioddate($enrolmentperioddate); + $category->password_expiry_days($password_expiry_days); $category->upperagelimit($upperagelimit); $category->dateofbirthrequired($dateofbirthrequired); $category->enrolmentfee($enrolmentfee); @@ -134,6 +136,7 @@ elsif ( $op eq 'add_validate' ) { description => $description, enrolmentperiod => $enrolmentperiod, enrolmentperioddate => $enrolmentperioddate, + password_expiry_days => $password_expiry_days, upperagelimit => $upperagelimit, dateofbirthrequired => $dateofbirthrequired, enrolmentfee => $enrolmentfee, diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt index efdb5f86d4..ca6ba5c44c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt @@ -162,6 +162,10 @@ +
  • + + days +
  • years @@ -451,6 +455,7 @@ [% END %] + Password expiration: [% category.password_expiry_days | html %] days Age required: [% category.dateofbirthrequired | html %] years Upperage limit: [% category.upperagelimit | html %] years Enrollment fee: [% category.enrolmentfee | $Price %] @@ -519,6 +524,7 @@ Category name Type Enrollment period + Password expiration Age required Upper age limit Enrollment fee @@ -561,6 +567,11 @@ until [% category.enrolmentperioddate | $KohaDates %] [% END %] + [% IF (category.password_expiry_days) %] + [% category.password_expiry_days | html %] days + [% ELSE %] + - + [% END %] [% IF (category.dateofbirthrequired) %] [% category.dateofbirthrequired | html %] years [% ELSE %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt index a7e663bec7..736b54fa44 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt @@ -50,6 +50,13 @@ [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %] You must reset your password. [% END %] +[% ELSIF password_has_expired %] +
    Error: Your password has expired!
    + [% IF Categories.can_any_reset_password && Koha.Preference('OpacBaseURL') %] + You must reset your password. + [% ELSE %] +

    You must contact the library to have your password reset

    + [% END %] [% ELSIF invalid_username_or_password %]
    Error: Invalid username or password
    [% END %] 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 99fc207edc..dc8f2d9c1a 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-auth.tt @@ -167,7 +167,16 @@ [% END # /IF GoogleOpenIDConnect %] [% END # /UNLESS OPACShibOnly %] - [% IF !Koha.Preference('OPACShibOnly') or SCO_login or SCI_login %] + [% IF password_has_expired %] +

    Error: Your password has expired!

    + [% IF Koha.Preference('OpacPasswordChange') && Categories.can_any_reset_password %] + + [% ELSE %] +

    You must contact the library to reset your password

    + [% END %] + [% ELSIF !Koha.Preference('OPACShibOnly') or SCO_login or SCI_login %] [% IF SCO_login %]
    [% ELSIF SCI_login %] @@ -218,7 +227,7 @@ [% END %]
    - [% END # / IF !OPACShibOnly or SCO_login or SCI_login %] + [% END # / IF password_has_expired / ELSIF !OPACShibOnly or SCO_login or SCI_login %] [% END # / IF loginprompt %] [% ELSE %] -- 2.39.5