Bug 28290: Don't send subfields to 'as_string' if none to send
[koha-ffzg.git] / C4 / Auth.pm
index a48eaa0..a2b1b84 100644 (file)
@@ -35,6 +35,7 @@ use Koha;
 use Koha::Logger;
 use Koha::Caches;
 use Koha::AuthUtils qw( get_script_name hash_password );
+use Koha::Auth::TwoFactorAuth;
 use Koha::Checkouts;
 use Koha::DateUtils qw( dt_from_string );
 use Koha::Library::Groups;
@@ -48,6 +49,7 @@ use Encode;
 use C4::Auth_with_shibboleth qw( shib_ok get_login_shib login_shib_url logout_shib checkpw_shib );
 use Net::CIDR;
 use C4::Log qw( logaction );
+use Koha::CookieManager;
 
 # use utf8;
 
@@ -154,6 +156,9 @@ sub get_template_and_user {
 
     my $in = shift;
     my ( $user, $cookie, $sessionID, $flags );
+    $cookie = [];
+
+    my $cookie_mgr = Koha::CookieManager->new;
 
     # Get shibboleth login attribute
     my $shib = C4::Context->config('useshibboleth') && shib_ok();
@@ -243,13 +248,13 @@ sub get_template_and_user {
         if ($kick_out) {
             $template = C4::Templates::gettemplate( 'opac-auth.tt', 'opac',
                 $in->{query} );
-            $cookie = $in->{query}->cookie(
+            $cookie = $cookie_mgr->replace_in_list( $cookie, $in->{query}->cookie(
                 -name     => 'CGISESSID',
                 -value    => '',
-                -expires  => '',
                 -HttpOnly => 1,
                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-            );
+                -sameSite => 'Lax',
+            ));
 
             $template->param(
                 loginprompt => 1,
@@ -399,7 +404,6 @@ sub get_template_and_user {
                 # we add them to the logged-in search history
                 my @recentSearches = C4::Search::History::get_from_session( { cgi => $in->{'query'} } );
                 if (@recentSearches) {
-                    my $dbh   = C4::Context->dbh;
                     my $query = q{
                         INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, type,  total, time )
                         VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -498,7 +502,6 @@ sub get_template_and_user {
     my $minPasswordLength = C4::Context->preference('minPasswordLength');
     $minPasswordLength = 3 if not $minPasswordLength or $minPasswordLength < 3;
     $template->param(
-        "BiblioDefaultView" . C4::Context->preference("BiblioDefaultView") => 1,
         EnhancedMessagingPreferences                                       => C4::Context->preference('EnhancedMessagingPreferences'),
         GoogleJackets                                                      => C4::Context->preference("GoogleJackets"),
         OpenLibraryCovers                                                  => C4::Context->preference("OpenLibraryCovers"),
@@ -520,7 +523,6 @@ sub get_template_and_user {
         $template->param(
             AmazonCoverImages                                                          => C4::Context->preference("AmazonCoverImages"),
             AutoLocation                                                               => C4::Context->preference("AutoLocation"),
-            "BiblioDefaultView" . C4::Context->preference("IntranetBiblioDefaultView") => 1,
             PatronAutoComplete                                                       => C4::Context->preference("PatronAutoComplete"),
             FRBRizeEditions                                                            => C4::Context->preference("FRBRizeEditions"),
             IndependentBranches                                                        => C4::Context->preference("IndependentBranches"),
@@ -656,11 +658,7 @@ sub get_template_and_user {
         # what to do
         my $language = C4::Languages::getlanguage( $in->{'query'} );
         my $languagecookie = C4::Templates::getlanguagecookie( $in->{'query'}, $language );
-        if ( ref $cookie eq 'ARRAY' ) {
-            push @{$cookie}, $languagecookie;
-        } else {
-            $cookie = [ $cookie, $languagecookie ];
-        }
+        $cookie = $cookie_mgr->replace_in_list( $cookie, $languagecookie );
     }
 
     return ( $template, $borrowernumber, $cookie, $flags );
@@ -825,27 +823,31 @@ sub checkauth {
     my $template_name   = shift;
     $type = 'opac' unless $type;
 
-    unless ( C4::Context->preference("OpacPublic") ) {
+    if ( $type eq 'opac' && !C4::Context->preference("OpacPublic") ) {
         my @allowed_scripts_for_private_opac = qw(
           opac-memberentry.tt
           opac-registration-email-sent.tt
           opac-registration-confirmation.tt
           opac-memberentry-update-submitted.tt
           opac-password-recovery.tt
+          opac-reset-password.tt
         );
         $authnotrequired = 0 unless grep { $_ eq $template_name }
           @allowed_scripts_for_private_opac;
     }
 
-    my $dbh     = C4::Context->dbh;
     my $timeout = _timeout_syspref();
 
+    my $cookie_mgr = Koha::CookieManager->new;
+
     _version_check( $type, $query );
 
     # state variables
     my $loggedin = 0;
+    my $auth_state = 'failed';
     my %info;
     my ( $userid, $cookie, $sessionID, $flags );
+    $cookie = [];
     my $logout = $query->param('logout.x');
 
     my $anon_search_history;
@@ -856,6 +858,8 @@ sub checkauth {
     my $q_userid = $query->param('userid') // '';
 
     my $session;
+    my $invalid_otp_token;
+    my $require_2FA = ( C4::Context->preference('TwoFactorAuthentication') && $type ne "opac" ) ? 1 : 0;
 
     # Basic authentication is incompatible with the use of Shibboleth,
     # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
@@ -869,13 +873,13 @@ sub checkauth {
     if ( !$shib and defined( $ENV{'REMOTE_USER'} ) and $ENV{'REMOTE_USER'} ne '' and $userid = $ENV{'REMOTE_USER'} ) {
 
         # Using Basic Authentication, no cookies required
-        $cookie = $query->cookie(
+        $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
             -name     => 'CGISESSID',
             -value    => '',
-            -expires  => '',
             -HttpOnly => 1,
             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-        );
+            -sameSite => 'Lax',
+        ));
         $loggedin = 1;
     }
     elsif ( $emailaddress) {
@@ -883,17 +887,49 @@ sub checkauth {
     }
     elsif ( $sessionID = $query->cookie("CGISESSID") ) {    # assignment, not comparison
         my ( $return, $more_info );
+        # NOTE: $flags in the following call is still undefined !
         ( $return, $session, $more_info ) = check_cookie_auth( $sessionID, $flags,
             { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
         );
 
-        if ( $return eq 'ok' ) {
-            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
+        if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
+            $userid = $session->param('id');
+        }
+
+        $auth_state =
+            $return eq 'ok'                     ? 'completed'
+          : $return eq 'additional-auth-needed' ? 'additional-auth-needed'
+          :                                       'failed';
+
+        # We are at the second screen if the waiting-for-2FA is set in session
+        # and otp_token param has been passed
+        if (   $require_2FA
+            && $auth_state eq 'additional-auth-needed'
+            && ( my $otp_token = $query->param('otp_token') ) )
+        {
+            my $patron    = Koha::Patrons->find( { userid => $userid } );
+            my $auth      = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
+            my $verified = $auth->verify($otp_token, 1);
+            $auth->clear;
+            if ( $verified ) {
+                # The token is correct, the user is fully logged in!
+                $auth_state = 'completed';
+                $session->param( 'waiting-for-2FA', 0 );
+
+               # This is an ugly trick to pass the test
+               # $query->param('koha_login_context') && ( $q_userid ne $userid )
+               # few lines later
+                $q_userid = $userid;
+            }
+            else {
+                $invalid_otp_token = 1;
+            }
+        }
 
-            my $s_userid = $session->param('id');
-            $userid      = $s_userid;
+        if ( $auth_state eq 'completed' ) {
+            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
 
-            if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
+            if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
                 || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
                 || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
             ) {
@@ -903,40 +939,22 @@ sub checkauth {
                 $anon_search_history = $session->param('search_history');
                 $session->delete();
                 $session->flush;
+                $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
                 C4::Context::_unset_userenv($sessionID);
                 $sessionID = undef;
-            }
-            elsif ($logout) {
-
-                # voluntary logout the user
-                # check wether the user was using their shibboleth session or a local one
-                my $shibSuccess = C4::Context->userenv->{'shibboleth'};
-                $session->delete();
-                $session->flush;
-                C4::Context::_unset_userenv($sessionID);
-                $sessionID = undef;
+            } elsif (!$logout) {
 
-                if ($cas and $caslogout) {
-                    logout_cas($query, $type);
-                }
-
-                # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
-                if ( $shib and $shib_login and $shibSuccess) {
-                    logout_shib($query);
-                }
-            } else {
-
-                $cookie = $query->cookie(
+                $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
                     -name     => 'CGISESSID',
                     -value    => $session->id,
                     -HttpOnly => 1,
                     -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-                );
+                    -sameSite => 'Lax',
+                ));
 
                 $flags = haspermission( $userid, $flagsrequired );
-                if ($flags) {
-                    $loggedin = 1;
-                } else {
+                unless ( $flags ) {
+                    $auth_state = 'failed';
                     $info{'nopermission'} = 1;
                 }
             }
@@ -947,17 +965,47 @@ 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;
             }
         }
     }
 
-    unless ( $loggedin ) {
+    if ( $auth_state eq 'failed' || $logout ) {
+        $sessionID = undef;
         $userid    = undef;
     }
 
+    if ($logout) {
+
+        # voluntary logout the user
+        # check wether the user was using their shibboleth session or a local one
+        my $shibSuccess = C4::Context->userenv ? C4::Context->userenv->{'shibboleth'} : undef;
+        if ( $session ) {
+            $session->delete();
+            $session->flush;
+        }
+        C4::Context::_unset_userenv($sessionID);
+        $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
+
+        if ($cas and $caslogout) {
+            logout_cas($query, $type);
+        }
+
+        # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
+        if ( $shib and $shib_login and $shibSuccess) {
+            logout_shib($query);
+        }
+
+        $session   = undef;
+        $auth_state = 'logout';
+    }
+
     unless ( $userid ) {
         #we initiate a session prior to checking for a username to allow for anonymous sessions...
-        $session ||= get_session("") or die "Auth ERROR: Cannot get_session()";
+        if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
+            $session = get_session() or die "Auth ERROR: Cannot get_session()";
+        }
 
         # Save anonymous search history in new session so it can be retrieved
         # by get_template_and_user to store it in user's search history after
@@ -968,12 +1016,13 @@ sub checkauth {
 
         $sessionID = $session->id;
         C4::Context->_new_userenv($sessionID);
-        $cookie = $query->cookie(
+        $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
             -name     => 'CGISESSID',
             -value    => $sessionID,
             -HttpOnly => 1,
             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-        );
+            -sameSite => 'Lax',
+        ));
         my $pki_field = C4::Context->preference('AllowPKIAuth');
         if ( !defined($pki_field) ) {
             print STDERR "ERROR: Missing system preference AllowPKIAuth.\n";
@@ -994,7 +1043,7 @@ sub checkauth {
                 my $retuserid;
 
                 # Do not pass password here, else shib will not be checked in checkpw.
-                ( $return, $cardnumber, $retuserid ) = checkpw( $dbh, $q_userid, undef, $query );
+                ( $return, $cardnumber, $retuserid ) = checkpw( $q_userid, undef, $query );
                 $userid      = $retuserid;
                 $shibSuccess = $return;
                 $info{'invalidShibLogin'} = 1 unless ($return);
@@ -1005,7 +1054,7 @@ sub checkauth {
                 if ( $cas && $query->param('ticket') ) {
                     my $retuserid;
                     ( $return, $cardnumber, $retuserid, $cas_ticket ) =
-                      checkpw( $dbh, $userid, $password, $query, $type );
+                      checkpw( $userid, $password, $query, $type );
                     $userid = $retuserid;
                     $info{'invalidCasLogin'} = 1 unless ($return);
                 }
@@ -1074,7 +1123,7 @@ sub checkauth {
                     {
 
                         ( $return, $cardnumber, $retuserid, $cas_ticket ) =
-                          checkpw( $dbh, $q_userid, $password, $query, $type );
+                          checkpw( $q_userid, $password, $query, $type );
                         $userid = $retuserid if ($retuserid);
                         $info{'invalid_username_or_password'} = 1 unless ($return);
                     }
@@ -1099,10 +1148,10 @@ sub checkauth {
             }
 
             # $return: 1 = valid user
-            if ($return) {
+            if ($return > 0) {
 
                 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
-                    $loggedin = 1;
+                    $auth_state = "logged_in";
                 }
                 else {
                     $info{'nopermission'} = 1;
@@ -1119,6 +1168,7 @@ sub checkauth {
                     FROM borrowers
                     LEFT JOIN branches on borrowers.branchcode=branches.branchcode
                     ";
+                    my $dbh = C4::Context->dbh;
                     my $sth = $dbh->prepare("$select where userid=?");
                     $sth->execute($userid);
                     unless ( $sth->rows ) {
@@ -1168,12 +1218,13 @@ sub checkauth {
                         $domain =~ s|\.\*||g;
                         if ( $ip !~ /^$domain/ ) {
                             $loggedin = 0;
-                            $cookie = $query->cookie(
+                            $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
                                 -name     => 'CGISESSID',
                                 -value    => '',
                                 -HttpOnly => 1,
                                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-                            );
+                                -sameSite => 'Lax',
+                            ));
                             $info{'wrongip'} = 1;
                         }
                     }
@@ -1252,17 +1303,33 @@ sub checkauth {
         $session->flush;
     }    # END unless ($userid)
 
+
+    if ( $auth_state eq 'logged_in' ) {
+        $auth_state = 'completed';
+
+        # Auth is completed unless an additional auth is needed
+        if ( $require_2FA ) {
+            my $patron = Koha::Patrons->find({userid => $userid});
+            if ( $patron->auth_method eq 'two-factor' ) {
+                # Ask for the OTP token
+                $auth_state = 'additional-auth-needed';
+                $session->param('waiting-for-2FA', 1);
+                %info = ();# We remove the warnings/errors we may have set incorrectly before
+            }
+        }
+    }
+
     # finished authentification, now respond
-    if ( $loggedin || $authnotrequired )
-    {
+    if ( $auth_state eq 'completed' || $authnotrequired ) {
         # successful login
-        unless ($cookie) {
-            $cookie = $query->cookie(
+        unless (@$cookie) {
+            $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
                 -name     => 'CGISESSID',
                 -value    => '',
                 -HttpOnly => 1,
                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-            );
+                -sameSite => 'Lax',
+            ));
         }
 
         track_login_daily( $userid );
@@ -1288,16 +1355,17 @@ sub checkauth {
     #
     #
 
+    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
+
     # get the inputs from the incoming query
     my @inputs = ();
+    my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
     foreach my $name ( param $query) {
-        (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
+        next if grep { $name eq $_ } @inputs_to_clean;
         my @value = $query->multi_param($name);
         push @inputs, { name => $name, value => $_ } for @value;
     }
 
-    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
-
     my $LibraryNameTitle = C4::Context->preference("LibraryName");
     $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
     $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
@@ -1338,13 +1406,22 @@ 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') );
     $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
     $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
     $template->param( loginprompt => 1 ) unless $info{'nopermission'};
+    if ( $auth_state eq 'additional-auth-needed' ) {
+        my $patron = Koha::Patrons->find( { userid => $userid } );
+        $template->param(
+            TwoFA_prompt => 1,
+            invalid_otp_token => $invalid_otp_token,
+            notice_email_address => $patron->notice_email_address, # We could also pass logged_in_user if necessary
+        );
+    }
 
     if ( $type eq 'opac' ) {
         require Koha::Virtualshelves;
@@ -1414,7 +1491,8 @@ sub checkauth {
         {   type              => 'text/html',
             charset           => 'utf-8',
             cookie            => $cookie,
-            'X-Frame-Options' => 'SAMEORIGIN'
+            'X-Frame-Options' => 'SAMEORIGIN',
+            -sameSite => 'Lax'
         }
       ),
       $template->output;
@@ -1453,6 +1531,8 @@ Possible return values in C<$status> are:
 
 =item "restricted" -- The IP has changed (if SessionRestrictionByIP)
 
+=item "additional-auth-needed -- User is in an authentication process that is not finished
+
 =back
 
 =cut
@@ -1461,7 +1541,6 @@ sub check_api_auth {
 
     my $query         = shift;
     my $flagsrequired = shift;
-    my $dbh     = C4::Context->dbh;
     my $timeout = _timeout_syspref();
 
     unless ( C4::Context->preference('Version') ) {
@@ -1497,8 +1576,9 @@ sub check_api_auth {
             -value    => $session->id,
             -HttpOnly => 1,
             -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
+            -sameSite => 'Lax'
         );
-        return ( $return, undef, $session );
+        return ( $return, $cookie, $session ); # return == 'ok' here
 
     } else {
 
@@ -1513,7 +1593,7 @@ sub check_api_auth {
 
             # In case of a CAS authentication, we use the ticket instead of the password
             my $PT = $query->param('PT');
-            ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $dbh, $PT, $query );    # EXTERNAL AUTH
+            ( $return, $cardnumber, $userid, $cas_ticket ) = check_api_auth_cas( $PT, $query );    # EXTERNAL AUTH
         } else {
 
             # User / password auth
@@ -1523,7 +1603,7 @@ sub check_api_auth {
                 return ( "failed", undef, undef );
             }
             my $newuserid;
-            ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $dbh, $userid, $password, $query );
+            ( $return, $cardnumber, $newuserid, $cas_ticket ) = checkpw( $userid, $password, $query );
         }
 
         if ( $return and haspermission( $userid, $flagsrequired ) ) {
@@ -1537,6 +1617,7 @@ sub check_api_auth {
                 -value    => $sessionID,
                 -HttpOnly => 1,
                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
+                -sameSite => 'Lax'
             );
             if ( $return == 1 ) {
                 my (
@@ -1544,6 +1625,7 @@ sub check_api_auth {
                     $userflags,      $branchcode, $branchname,
                     $emailaddress
                 );
+                my $dbh = C4::Context->dbh;
                 my $sth =
                   $dbh->prepare(
 "select borrowernumber, firstname, surname, flags, borrowers.branchcode, branches.branchname as branchname, email from borrowers left join branches on borrowers.branchcode=branches.branchcode where userid=?"
@@ -1715,6 +1797,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);
@@ -1728,6 +1813,9 @@ sub check_cookie_auth {
                     $session->param('desk_id'),      $session->param('desk_name'),
                     $session->param('register_id'),  $session->param('register_name')
                 );
+                return ( "additional-auth-needed", $session )
+                    if $session->param('waiting-for-2FA');
+
                 return ( "ok", $session );
             } else {
                 $session->delete();
@@ -1792,7 +1880,7 @@ sub get_session {
         $session = CGI::Session->load( $params->{dsn}, $sessionID, $params->{dsn_args} );
     } else {
         $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
-        # $session->flush;
+        # no need to flush here
     }
     return $session;
 }
@@ -1803,7 +1891,7 @@ sub get_session {
 # Currently it's only passed from C4::SIP::ILS::Patron::check_password, but
 # not having a userenv defined could cause a crash.
 sub checkpw {
-    my ( $dbh, $userid, $password, $query, $type, $no_set_userenv ) = @_;
+    my ( $userid, $password, $query, $type, $no_set_userenv ) = @_;
     $type = 'opac' unless $type;
 
     # Get shibboleth login attribute
@@ -1823,7 +1911,7 @@ sub checkpw {
     # 0 if auth is nok
     # -1 if user bind failed (LDAP only)
 
-    if ( $patron and $patron->account_locked ) {
+    if ( $patron and ( $patron->account_locked )  ) {
         # Nothing to check, account is locked
     } elsif ($ldap && defined($password)) {
         my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_);    # EXTERNAL AUTH
@@ -1838,7 +1926,7 @@ sub checkpw {
         # In case of a CAS authentication, we use the ticket instead of the password
         my $ticket = $query->param('ticket');
         $query->delete('ticket');                                   # remove ticket to come back to original URL
-        my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $dbh, $ticket, $query, $type );    # EXTERNAL AUTH
+        my ( $retval, $retcard, $retuserid, $cas_ticket ) = checkpw_cas( $ticket, $query, $type );    # EXTERNAL AUTH
         if ( $retval ) {
             @return = ( $retval, $retcard, $retuserid, $cas_ticket );
         } else {
@@ -1870,13 +1958,16 @@ sub checkpw {
 
     # INTERNAL AUTH
     if ( $check_internal_as_fallback ) {
-        @return = checkpw_internal( $dbh, $userid, $password, $no_set_userenv);
+        @return = checkpw_internal( $userid, $password, $no_set_userenv);
         $passwd_ok = 1 if $return[0] > 0; # 1 or 2
     }
 
     if( $patron ) {
         if ( $passwd_ok ) {
             $patron->update({ login_attempts => 0 });
+            if( $patron->password_expired ){
+                @return = (-2);
+            }
         } elsif( !$patron->account_locked ) {
             $patron->update({ login_attempts => $patron->login_attempts + 1 });
         }
@@ -1893,11 +1984,12 @@ sub checkpw {
 }
 
 sub checkpw_internal {
-    my ( $dbh, $userid, $password, $no_set_userenv ) = @_;
+    my ( $userid, $password, $no_set_userenv ) = @_;
 
     $password = Encode::encode( 'UTF-8', $password )
       if Encode::is_utf8($password);
 
+    my $dbh = C4::Context->dbh;
     my $sth =
       $dbh->prepare(
         "select password,cardnumber,borrowernumber,userid,firstname,surname,borrowers.branchcode,branches.branchname,flags from borrowers join branches on borrowers.branchcode=branches.branchcode where userid=?"