Bug 28786: Two-factor authentication for staff client - TOTP
[koha-ffzg.git] / C4 / Auth.pm
index a58b0a8..9ebc3ae 100644 (file)
@@ -19,38 +19,42 @@ package C4::Auth;
 
 use strict;
 use warnings;
-use Carp qw/croak/;
+use Carp qw( croak );
 
-use Digest::MD5 qw(md5_base64);
-use JSON qw/encode_json/;
-use URI::Escape;
+use Digest::MD5 qw( md5_base64 );
 use CGI::Session;
+use CGI::Session::ErrorHandler;
+use URI;
+use URI::QueryParam;
 
-require Exporter;
 use C4::Context;
 use C4::Templates;    # to get the template
 use C4::Languages;
 use C4::Search::History;
 use Koha;
+use Koha::Logger;
 use Koha::Caches;
-use Koha::AuthUtils qw(get_script_name hash_password);
+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::DateUtils qw( dt_from_string );
 use Koha::Library::Groups;
 use Koha::Libraries;
+use Koha::Cash::Registers;
 use Koha::Desks;
 use Koha::Patrons;
 use Koha::Patron::Consents;
-use POSIX qw/strftime/;
-use List::MoreUtils qw/ any /;
-use Encode qw( encode is_utf8);
-use C4::Auth_with_shibboleth;
+use List::MoreUtils qw( any );
+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 C4::Log qw( logaction );
+use Koha::CookieManager;
 
 # use utf8;
-use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $debug $ldap $cas $caslogout);
 
+use vars qw($ldap $cas $caslogout);
+our (@ISA, @EXPORT_OK);
 BEGIN {
     sub psgi_env { any { /^psgi\./ } keys %ENV }
 
@@ -61,24 +65,26 @@ BEGIN {
 
     C4::Context->set_remote_address;
 
-    $debug     = $ENV{DEBUG};
-    @ISA       = qw(Exporter);
-    @EXPORT    = qw(&checkauth &get_template_and_user &haspermission &get_user_subpermissions);
-    @EXPORT_OK = qw(&check_api_auth &get_session &check_cookie_auth &checkpw &checkpw_internal &checkpw_hash
-      &get_all_subpermissions &get_user_subpermissions track_login_daily &in_iprange
+    require Exporter;
+    @ISA = qw(Exporter);
+
+    @EXPORT_OK = qw(
+      checkauth check_api_auth get_session check_cookie_auth checkpw checkpw_internal checkpw_hash
+      get_all_subpermissions get_user_subpermissions track_login_daily in_iprange
+      get_template_and_user haspermission
     );
-    %EXPORT_TAGS = ( EditPermissions => [qw(get_all_subpermissions get_user_subpermissions)] );
+
     $ldap      = C4::Context->config('useldapserver') || 0;
     $cas       = C4::Context->preference('casAuthentication');
     $caslogout = C4::Context->preference('casLogout');
-    require C4::Auth_with_cas;    # no import
 
     if ($ldap) {
         require C4::Auth_with_ldap;
         import C4::Auth_with_ldap qw(checkpw_ldap);
     }
     if ($cas) {
-        import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url logout_if_required);
+        require C4::Auth_with_cas;    # no import
+        import C4::Auth_with_cas qw(check_api_auth_cas checkpw_cas login_cas logout_cas login_cas_url logout_if_required multipleAuth getMultipleAuth);
     }
 
 }
@@ -93,7 +99,7 @@ C4::Auth - Authenticates Koha users
   use C4::Auth;
   use C4::Output;
 
-  my $query = new CGI;
+  my $query = CGI->new;
 
   my ($template, $borrowernumber, $cookie)
     = get_template_and_user(
@@ -150,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();
@@ -171,7 +180,9 @@ sub get_template_and_user {
             $in->{'query'},
             $in->{'authnotrequired'},
             $in->{'flagsrequired'},
-            $in->{'type'}
+            $in->{'type'},
+            undef,
+            $in->{template_name},
         );
     }
 
@@ -193,14 +204,26 @@ sub get_template_and_user {
     }
 
     if ( $in->{type} eq 'opac' && $user ) {
+        my $is_sco_user;
+        if ($sessionID){
+            my $session = get_session($sessionID);
+            if ($session){
+                $is_sco_user = $session->param('sco_user');
+            }
+        }
         my $kick_out;
 
         if (
 # If the user logged in is the SCO user and they try to go out of the SCO module,
 # log the user out removing the CGISESSID cookie
             $in->{template_name} !~ m|sco/| && $in->{template_name} !~ m|errors/errorpage.tt|
-            && C4::Context->preference('AutoSelfCheckID')
-            && $user eq C4::Context->preference('AutoSelfCheckID')
+            && (
+                $is_sco_user ||
+                (
+                    C4::Context->preference('AutoSelfCheckID')
+                    && $user eq C4::Context->preference('AutoSelfCheckID')
+                )
+            )
           )
         {
             $kick_out = 1;
@@ -225,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,
@@ -284,12 +307,12 @@ sub get_template_and_user {
             my $some_private_shelves = Koha::Virtualshelves->get_some_shelves(
                 {
                     borrowernumber => $borrowernumber,
-                    category       => 1,
+                    public         => 0,
                 }
             );
             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
                 {
-                    category       => 2,
+                    public => 1,
                 }
             );
             $template->param(
@@ -316,17 +339,21 @@ sub get_template_and_user {
             $template->param( CAN_user_editcatalogue    => 1 );
             $template->param( CAN_user_updatecharges    => 1 );
             $template->param( CAN_user_acquisition      => 1 );
+            $template->param( CAN_user_suggestions      => 1 );
             $template->param( CAN_user_tools            => 1 );
             $template->param( CAN_user_editauthorities  => 1 );
             $template->param( CAN_user_serials          => 1 );
             $template->param( CAN_user_reports          => 1 );
             $template->param( CAN_user_staffaccess      => 1 );
-            $template->param( CAN_user_plugins          => 1 );
             $template->param( CAN_user_coursereserves   => 1 );
+            $template->param( CAN_user_plugins          => 1 );
+            $template->param( CAN_user_lists            => 1 );
             $template->param( CAN_user_clubs            => 1 );
             $template->param( CAN_user_ill              => 1 );
             $template->param( CAN_user_stockrotation    => 1 );
-            $template->param( CAN_user_problem_reports   => 1 );
+            $template->param( CAN_user_cash_management  => 1 );
+            $template->param( CAN_user_problem_reports  => 1 );
+            $template->param( CAN_user_recalls          => 1 );
 
             foreach my $module ( keys %$all_perms ) {
                 foreach my $subperm ( keys %{ $all_perms->{$module} } ) {
@@ -425,15 +452,37 @@ sub get_template_and_user {
             require Koha::Virtualshelves;
             my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
                 {
-                    category       => 2,
+                    public => 1,
                 }
             );
             $template->param(
                 some_public_shelves  => $some_public_shelves,
             );
+
+            # Set default branch if one has been passed by the environment.
+            $template->param( default_branch => $ENV{OPAC_BRANCH_DEFAULT} ) if $ENV{OPAC_BRANCH_DEFAULT};
         }
     }
 
+    # Sysprefs disabled via URL param
+    # Note that value must be defined in order to override via ENV
+    foreach my $syspref (
+        qw(
+            OPACUserCSS
+            OPACUserJS
+            IntranetUserCSS
+            IntranetUserJS
+            OpacAdditionalStylesheet
+            opaclayoutstylesheet
+            intranetcolorstylesheet
+            intranetstylesheet
+        )
+      )
+    {
+        $ENV{"OVERRIDE_SYSPREF_$syspref"} = q{}
+          if $in->{'query'}->param("DISABLE_SYSPREF_$syspref");
+    }
+
     # Anonymous opac search history
     # If opac search history is enabled and at least one search has already been performed
     if ( C4::Context->preference('EnableOpacSearchHistory') ) {
@@ -454,7 +503,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"),
@@ -467,8 +515,6 @@ sub get_template_and_user {
         item_level_itypes  => C4::Context->preference('item-level_itypes'),
         patronimages       => C4::Context->preference("patronimages"),
         singleBranchMode   => ( Koha::Libraries->search->count == 1 ),
-        XSLTDetailsDisplay => C4::Context->preference("XSLTDetailsDisplay"),
-        XSLTResultsDisplay => C4::Context->preference("XSLTResultsDisplay"),
         noItemTypeImages   => C4::Context->preference("noItemTypeImages"),
         marcflavour        => C4::Context->preference("marcflavour"),
         OPACBaseURL        => C4::Context->preference('OPACBaseURL'),
@@ -478,7 +524,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"),
@@ -493,7 +538,6 @@ sub get_template_and_user {
             intranetstylesheet                                                         => C4::Context->preference("intranetstylesheet"),
             IntranetUserCSS                                                            => C4::Context->preference("IntranetUserCSS"),
             IntranetUserJS                                                             => C4::Context->preference("IntranetUserJS"),
-            intranetbookbag                                                            => C4::Context->preference("intranetbookbag"),
             suggestion                                                                 => C4::Context->preference("suggestion"),
             virtualshelves                                                             => C4::Context->preference("virtualshelves"),
             StaffSerialIssueDisplayCount                                               => C4::Context->preference("StaffSerialIssueDisplayCount"),
@@ -504,7 +548,7 @@ sub get_template_and_user {
             EnableBorrowerFiles                                                        => C4::Context->preference('EnableBorrowerFiles'),
             UseCourseReserves                                                          => C4::Context->preference("UseCourseReserves"),
             useDischarge                                                               => C4::Context->preference('useDischarge'),
-            pending_checkout_notes                                                     => scalar Koha::Checkouts->search({ noteseen => 0 }),
+            pending_checkout_notes                                                     => Koha::Checkouts->search({ noteseen => 0 }),
         );
     }
     else {
@@ -522,10 +566,12 @@ sub get_template_and_user {
             && $in->{'template_name'} =~ /opac-(.+)\.(?:tt|tmpl)$/ ) {
             my $pagename = $1;
             unless ( $pagename =~ /^(?:MARC|ISBD)?detail$/
+                or $pagename =~ /^showmarc$/
                 or $pagename =~ /^addbybiblionumber$/
-                or $pagename =~ /^review$/ ) {
-                my $sessionSearch = get_session( $sessionID || $in->{'query'}->cookie("CGISESSID") );
-                $sessionSearch->clear( ["busc"] ) if ( $sessionSearch->param("busc") );
+                or $pagename =~ /^review$/ )
+            {
+                my $sessionSearch = get_session( $sessionID );
+                $sessionSearch->clear( ["busc"] ) if $sessionSearch;
             }
         }
 
@@ -536,7 +582,7 @@ sub get_template_and_user {
         if (
             ( $opac_limit_override && $opac_search_limit && $opac_search_limit =~ /branch:([\w-]+)/ ) ||
             ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /branch:([\w-]+)/ ) ||
-            ( $in->{'query'}->param('multibranchlimit') && $in->{'query'}->param('multibranchlimit') =~ /multibranchlimit-(\w+)/ )
+            ( $in->{'query'}->param('limit') && $in->{'query'}->param('limit') =~ /multibranchlimit:(\w+)/ )
           ) {
             $opac_name = $1;    # opac_search_limit is a branch, so we use it.
         } elsif ( $in->{'query'}->param('multibranchlimit') ) {
@@ -545,7 +591,7 @@ sub get_template_and_user {
             $opac_name = C4::Context->userenv->{'branch'};
         }
 
-        my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' });
+        my @search_groups = Koha::Library::Groups->get_search_groups({ interface => 'opac' })->as_list;
         $template->param(
             AnonSuggestions                       => "" . C4::Context->preference("AnonSuggestions"),
             LibrarySearchGroups                   => \@search_groups,
@@ -565,14 +611,11 @@ sub get_template_and_user {
             OpacBrowser                           => C4::Context->preference("OpacBrowser"),
             OpacCloud                             => C4::Context->preference("OpacCloud"),
             OpacKohaUrl                           => C4::Context->preference("OpacKohaUrl"),
-            OpacNav                               => "" . C4::Context->preference("OpacNav"),
-            OpacNavBottom                         => "" . C4::Context->preference("OpacNavBottom"),
             OpacPasswordChange                    => C4::Context->preference("OpacPasswordChange"),
             OPACPatronDetails                     => C4::Context->preference("OPACPatronDetails"),
             OPACPrivacy                           => C4::Context->preference("OPACPrivacy"),
             OPACFinesTab                          => C4::Context->preference("OPACFinesTab"),
             OpacTopissue                          => C4::Context->preference("OpacTopissue"),
-            RequestOnOpac                         => C4::Context->preference("RequestOnOpac"),
             'Version'                             => C4::Context->preference('Version'),
             hidelostitems                         => C4::Context->preference("hidelostitems"),
             mylibraryfirst                        => ( C4::Context->preference("SearchMyLibraryFirst") && C4::Context->userenv ) ? C4::Context->userenv->{'branch'} : '',
@@ -588,8 +631,6 @@ sub get_template_and_user {
             suggestion                            => "" . C4::Context->preference("suggestion"),
             virtualshelves                        => "" . C4::Context->preference("virtualshelves"),
             OPACSerialIssueDisplayCount           => C4::Context->preference("OPACSerialIssueDisplayCount"),
-            OPACXSLTDetailsDisplay                => C4::Context->preference("OPACXSLTDetailsDisplay"),
-            OPACXSLTResultsDisplay                => C4::Context->preference("OPACXSLTResultsDisplay"),
             SyndeticsClientCode                   => C4::Context->preference("SyndeticsClientCode"),
             SyndeticsEnabled                      => C4::Context->preference("SyndeticsEnabled"),
             SyndeticsCoverImages                  => C4::Context->preference("SyndeticsCoverImages"),
@@ -618,11 +659,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 );
@@ -738,7 +775,7 @@ sub _version_check {
 
     # remove the 3 last . to have a Perl number
     $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
-    $debug and print STDERR "kohaversion : $kohaversion\n";
+    Koha::Logger->get->debug("kohaversion : $kohaversion");
     if ( $version < $kohaversion ) {
         my $warning = "Database update needed, redirecting to %s. Database is $version and Koha is $kohaversion";
         if ( $type ne 'opac' ) {
@@ -752,26 +789,28 @@ sub _version_check {
     }
 }
 
-sub _session_log {
-    (@_) or return 0;
-    open my $fh, '>>', "/tmp/sessionlog" or warn "ERROR: Cannot append to /tmp/sessionlog";
-    printf $fh join( "\n", @_ );
-    close $fh;
-}
-
 sub _timeout_syspref {
-    my $timeout = C4::Context->preference('timeout') || 600;
+    my $default_timeout = 600;
+    my $timeout = C4::Context->preference('timeout') || $default_timeout;
 
     # value in days, convert in seconds
-    if ( $timeout =~ /(\d+)[dD]/ ) {
+    if ( $timeout =~ /^(\d+)[dD]$/ ) {
         $timeout = $1 * 86400;
     }
+    # value in hours, convert in seconds
+    elsif ( $timeout =~ /^(\d+)[hH]$/ ) {
+        $timeout = $1 * 3600;
+    }
+    elsif ( $timeout !~ m/^\d+$/ ) {
+        warn "The value of the system preference 'timeout' is not correct, defaulting to $default_timeout";
+        $timeout = $default_timeout;
+    }
+
     return $timeout;
 }
 
 sub checkauth {
     my $query = shift;
-    $debug and warn "Checking Auth";
 
     # Get shibboleth login attribute
     my $shib = C4::Context->config('useshibboleth') && shib_ok();
@@ -782,18 +821,33 @@ sub checkauth {
     my $flagsrequired   = shift;
     my $type            = shift;
     my $emailaddress    = shift;
+    my $template_name   = shift;
     $type = 'opac' unless $type;
 
-    $authnotrequired = 0 unless C4::Context->preference("OpacPublic");
+    unless ( 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
+        );
+        $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 %info;
     my ( $userid, $cookie, $sessionID, $flags );
+    $cookie = [];
     my $logout = $query->param('logout.x');
 
     my $anon_search_history;
@@ -804,6 +858,9 @@ 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;
+    my $auth_challenge_complete;
 
     # Basic authentication is incompatible with the use of Shibboleth,
     # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
@@ -817,117 +874,84 @@ 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) {
         # the Google OpenID Connect passes an email address
     }
-    elsif ( $sessionID = $query->cookie("CGISESSID") )
-    {    # assignment, not comparison
-        $session = get_session($sessionID);
-        C4::Context->_new_userenv($sessionID);
-        my ( $ip, $lasttime, $sessiontype );
-        my $s_userid = '';
-        if ($session) {
-            $s_userid = $session->param('id') // '';
-            C4::Context->set_userenv(
-                $session->param('number'),       $s_userid,
-                $session->param('cardnumber'),   $session->param('firstname'),
-                $session->param('surname'),      $session->param('branch'),
-                $session->param('branchname'),   $session->param('flags'),
-                $session->param('emailaddress'), $session->param('shibboleth'),
-                $session->param('desk_id'),      $session->param('desk_name')
-            );
-            C4::Context::set_shelves_userenv( 'bar', $session->param('barshelves') );
-            C4::Context::set_shelves_userenv( 'pub', $session->param('pubshelves') );
-            C4::Context::set_shelves_userenv( 'tot', $session->param('totshelves') );
-            $debug and printf STDERR "AUTH_SESSION: (%s)\t%s %s - %s\n", map { $session->param($_) } qw(cardnumber firstname surname branch);
-            $ip          = $session->param('ip');
-            $lasttime    = $session->param('lasttime');
-            $userid      = $s_userid;
-            $sessiontype = $session->param('sessiontype') || '';
-        }
-        if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
-            || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
-            || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
-        ) {
-
-            #if a user enters an id ne to the id in the current session, we need to log them in...
-            #first we need to clear the anonymous session...
-            $debug and warn "query id = $q_userid but session id = $s_userid";
-            $anon_search_history = $session->param('search_history');
-            $session->delete();
-            $session->flush;
-            C4::Context->_unset_userenv($sessionID);
-            $sessionID = undef;
-            $userid    = undef;
-        }
-        elsif ($logout) {
+    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 }
+        );
 
-            # 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);
+        if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
+            $userid = $session->param('id');
+        }
 
-            #_session_log(sprintf "%20s from %16s logged out at %30s (manually).\n", $userid,$ip,(strftime "%c",localtime));
-            $sessionID = undef;
-            $userid    = undef;
+        $additional_auth_needed = ( $return eq 'additional-auth-needed' ) ? 1 : 0;
 
-            if ($cas and $caslogout) {
-                logout_cas($query, $type);
+        # 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
+            && $additional_auth_needed
+            && ( my $otp_token = $query->param('otp_token') ) )
+        {
+            my $patron    = Koha::Patrons->find( { userid => $userid } );
+            my $auth      = Koha::Auth::TwoFactorAuth::get_auth( { patron => $patron } );
+            my $verified = $auth->verify($otp_token);
+            $auth->clear;
+            if ( $verified ) {
+                # The token is correct, the user is fully logged in!
+                $additional_auth_needed = 0;
+                $session->param( 'waiting-for-2FA', 0 );
+                $return = "ok";
+                $auth_challenge_complete = 1;
+
+               # This is an ugly trick to pass the test
+               # $query->param('koha_login_context') && ( $q_userid ne $userid )
+               # few lines later
+                $q_userid = $userid;
             }
-
-            # 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 {
+                $invalid_otp_token = 1;
             }
         }
-        elsif ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
 
-            # timed logout
-            $info{'timed_out'} = 1;
-            if ($session) {
+        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 ( ( $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'} )
+            ) {
+
+                #if a user enters an id ne to the id in the current session, we need to log them in...
+                #first we need to clear the anonymous session...
+                $anon_search_history = $session->param('search_history');
                 $session->delete();
                 $session->flush;
-            }
-            C4::Context->_unset_userenv($sessionID);
-
-            #_session_log(sprintf "%20s from %16s logged out at %30s (inactivity).\n", $userid,$ip,(strftime "%c",localtime));
-            $userid    = undef;
-            $sessionID = undef;
-        }
-        elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $ENV{'REMOTE_ADDR'} ) {
+                $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
+                C4::Context::_unset_userenv($sessionID);
+                $sessionID = undef;
+            } elsif (!$logout) {
 
-            # Different ip than originally logged in from
-            $info{'oldip'}        = $ip;
-            $info{'newip'}        = $ENV{'REMOTE_ADDR'};
-            $info{'different_ip'} = 1;
-            $session->delete();
-            $session->flush;
-            C4::Context->_unset_userenv($sessionID);
+                $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',
+                ));
 
-            #_session_log(sprintf "%20s from %16s logged out at %30s (ip changed to %16s).\n", $userid,$ip,(strftime "%c",localtime), $info{'newip'});
-            $sessionID = undef;
-            $userid    = undef;
-        }
-        else {
-            $cookie = $query->cookie(
-                -name     => 'CGISESSID',
-                -value    => $session->id,
-                -HttpOnly => 1,
-                -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-            );
-            $session->param( 'lasttime', time() );
-            unless ( $sessiontype && $sessiontype eq 'anon' ) {    #if this is an anonymous session, we want to update the session, but not behave as if they are logged in...
                 $flags = haspermission( $userid, $flagsrequired );
                 if ($flags) {
                     $loggedin = 1;
@@ -935,11 +959,52 @@ sub checkauth {
                     $info{'nopermission'} = 1;
                 }
             }
+        } elsif ( !$logout ) {
+            if ( $return eq 'expired' ) {
+                $info{timed_out} = 1;
+            } elsif ( $return eq 'restricted' ) {
+                $info{oldip}        = $more_info->{old_ip};
+                $info{newip}        = $more_info->{new_ip};
+                $info{different_ip} = 1;
+            }
         }
     }
-    unless ( $userid || $sessionID ) {
+
+    if ( ( !$loggedin && !$additional_auth_needed ) || $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->{'shibboleth'};
+        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;
+        $additional_auth_needed = 0;
+    }
+
+    unless ( $userid ) {
         #we initiate a session prior to checking for a username to allow for anonymous sessions...
-        my $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
@@ -950,12 +1015,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    => $session->id,
+            -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";
@@ -1046,26 +1112,53 @@ sub checkauth {
                 }
                 else {
                     my $retuserid;
-                    ( $return, $cardnumber, $retuserid, $cas_ticket ) =
-                      checkpw( $dbh, $q_userid, $password, $query, $type );
-                    $userid = $retuserid if ($retuserid);
-                    $info{'invalid_username_or_password'} = 1 unless ($return);
+                    my $request_method = $query->request_method();
+
+                    if (
+                        $request_method eq 'POST'
+                        || ( C4::Context->preference('AutoSelfCheckID')
+                            && $q_userid eq C4::Context->preference('AutoSelfCheckID') )
+                      )
+                    {
+
+                        ( $return, $cardnumber, $retuserid, $cas_ticket ) =
+                          checkpw( $dbh, $q_userid, $password, $query, $type );
+                        $userid = $retuserid if ($retuserid);
+                        $info{'invalid_username_or_password'} = 1 unless ($return);
+                    }
                 }
             }
 
+            # If shib configured and shibOnly enabled, we should ignore anything other than a shibboleth type login.
+            if (
+                   $shib
+                && !$shibSuccess
+                && (
+                    (
+                        ( $type eq 'opac' )
+                        && C4::Context->preference('OPACShibOnly')
+                    )
+                    || ( ( $type ne 'opac' )
+                        && C4::Context->preference('staffShibOnly') )
+                )
+              )
+            {
+                $return = 0;
+            }
+
             # $return: 1 = valid user
             if ($return) {
 
-                #_session_log(sprintf "%20s from %16s logged in  at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
                 if ( $flags = haspermission( $userid, $flagsrequired ) ) {
                     $loggedin = 1;
                 }
                 else {
                     $info{'nopermission'} = 1;
-                    C4::Context->_unset_userenv($sessionID);
+                    C4::Context::_unset_userenv($sessionID);
                 }
                 my ( $borrowernumber, $firstname, $surname, $userflags,
-                    $branchcode, $branchname, $emailaddress, $desk_id, $desk_name );
+                    $branchcode, $branchname, $emailaddress, $desk_id,
+                    $desk_name, $register_id, $register_name );
 
                 if ( $return == 1 ) {
                     my $select = "
@@ -1077,25 +1170,16 @@ sub checkauth {
                     my $sth = $dbh->prepare("$select where userid=?");
                     $sth->execute($userid);
                     unless ( $sth->rows ) {
-                        $debug and print STDERR "AUTH_1: no rows for userid='$userid'\n";
                         $sth = $dbh->prepare("$select where cardnumber=?");
                         $sth->execute($cardnumber);
 
                         unless ( $sth->rows ) {
-                            $debug and print STDERR "AUTH_2a: no rows for cardnumber='$cardnumber'\n";
                             $sth->execute($userid);
-                            unless ( $sth->rows ) {
-                                $debug and print STDERR "AUTH_2b: no rows for userid='$userid' AS cardnumber\n";
-                            }
                         }
                     }
                     if ( $sth->rows ) {
                         ( $borrowernumber, $firstname, $surname, $userflags,
                             $branchcode, $branchname, $emailaddress ) = $sth->fetchrow;
-                        $debug and print STDERR "AUTH_3 results: " .
-                          "$cardnumber,$borrowernumber,$userid,$firstname,$surname,$userflags,$branchcode,$emailaddress\n";
-                    } else {
-                        print STDERR "AUTH_3: no results for userid='$userid', cardnumber='$cardnumber'.\n";
                     }
 
                     # launch a sequence to check if we have a ip for the branch, i
@@ -1114,20 +1198,31 @@ sub checkauth {
                         my $desk = Koha::Desks->find($desk_id);
                         $desk_name = $desk ? $desk->desk_name : '';
                     }
-                    my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search };
-                    if ( $type ne 'opac' and C4::Context->boolean_preference('AutoLocation') ) {
+                    if ( C4::Context->preference('UseCashRegisters') ) {
+                        my $register =
+                          $query->param('register_id')
+                          ? Koha::Cash::Registers->find($query->param('register_id'))
+                          : Koha::Cash::Registers->search(
+                            { branch => $branchcode, branch_default => 1 },
+                            { rows   => 1 } )->single;
+                        $register_id   = $register->id   if ($register);
+                        $register_name = $register->name if ($register);
+                    }
+                    my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
+                    if ( $type ne 'opac' and C4::Context->preference('AutoLocation') ) {
 
                         # we have to check they are coming from the right ip range
                         my $domain = $branches->{$branchcode}->{'branchip'};
                         $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;
                         }
                     }
@@ -1143,6 +1238,12 @@ sub checkauth {
                             $branchname    = $branches->{$br}->{'branchname'};
                         }
                     }
+
+                    my $is_sco_user = 0;
+                    if ( $query->param('sco_user_login') && ( $query->param('sco_user_login') eq '1' ) ){
+                        $is_sco_user = 1;
+                    }
+
                     $session->param( 'number',       $borrowernumber );
                     $session->param( 'id',           $userid );
                     $session->param( 'cardnumber',   $cardnumber );
@@ -1158,7 +1259,9 @@ sub checkauth {
                     $session->param( 'lasttime',     time() );
                     $session->param( 'interface',    $type);
                     $session->param( 'shibboleth',   $shibSuccess );
-                    $debug and printf STDERR "AUTH_4: (%s)\t%s %s - %s\n", map { $session->param($_) } qw(cardnumber firstname surname branch);
+                    $session->param( 'register_id',  $register_id );
+                    $session->param( 'register_name',  $register_name );
+                    $session->param( 'sco_user', $is_sco_user );
                 }
                 $session->param('cas_ticket', $cas_ticket) if $cas_ticket;
                 C4::Context->set_userenv(
@@ -1167,17 +1270,17 @@ sub checkauth {
                     $session->param('surname'),      $session->param('branch'),
                     $session->param('branchname'),   $session->param('flags'),
                     $session->param('emailaddress'), $session->param('shibboleth'),
-                    $session->param('desk_id'),      $session->param('desk_name')
+                    $session->param('desk_id'),      $session->param('desk_name'),
+                    $session->param('register_id'),  $session->param('register_name')
                 );
 
             }
             # $return: 0 = invalid user
             # reset to anonymous session
             else {
-                $debug and warn "Login failed, resetting anonymous session...";
                 if ($userid) {
                     $info{'invalid_username_or_password'} = 1;
-                    C4::Context->_unset_userenv($sessionID);
+                    C4::Context::_unset_userenv($sessionID);
                 }
                 $session->param( 'lasttime', time() );
                 $session->param( 'ip',       $session->remote_addr() );
@@ -1187,9 +1290,7 @@ sub checkauth {
         }    # END if ( $q_userid
         elsif ( $type eq "opac" ) {
 
-            # if we are here this is an anonymous session; add public lists to it and a few other items...
             # anonymous sessions are created only for the OPAC
-            $debug and warn "Initiating an anonymous session...";
 
             # setting a couple of other session vars...
             $session->param( 'ip',          $session->remote_addr() );
@@ -1197,21 +1298,34 @@ sub checkauth {
             $session->param( 'sessiontype', 'anon' );
             $session->param( 'interface', $type);
         }
+        $session->flush;
     }    # END unless ($userid)
 
+    if ( $require_2FA && ( $loggedin && !$auth_challenge_complete)) {
+        my $patron = Koha::Patrons->find({userid => $userid});
+        if ( $patron->auth_method eq 'two-factor' ) {
+            # Ask for the OTP token
+            $additional_auth_needed = 1;
+            $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 ( ( $loggedin || $authnotrequired ) && !$additional_auth_needed ) {
         # 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 );
+
         # In case, that this request was a login attempt, we want to prevent that users can repost the opac login
         # request. We therefore redirect the user to the requested page again without the login parameters.
         # See Post/Redirect/Get (PRG) design pattern: https://en.wikipedia.org/wiki/Post/Redirect/Get
@@ -1224,8 +1338,6 @@ sub checkauth {
             exit;
         }
 
-        track_login_daily( $userid );
-
         return ( $userid, $cookie, $sessionID, $flags );
     }
 
@@ -1235,36 +1347,34 @@ 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;
 
-    my $template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
-    my $template = C4::Templates::gettemplate( $template_name, $type, $query );
+    my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt';
+    my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query );
     $template->param(
         login                                 => 1,
         INPUTS                                => \@inputs,
         script_name                           => get_script_name(),
         casAuthentication                     => C4::Context->preference("casAuthentication"),
         shibbolethAuthentication              => $shib,
-        SessionRestrictionByIP                => C4::Context->preference("SessionRestrictionByIP"),
         suggestion                            => C4::Context->preference("suggestion"),
         virtualshelves                        => C4::Context->preference("virtualshelves"),
         LibraryName                           => "" . C4::Context->preference("LibraryName"),
         LibraryNameTitle                      => "" . $LibraryNameTitle,
         opacuserlogin                         => C4::Context->preference("opacuserlogin"),
-        OpacNav                               => C4::Context->preference("OpacNav"),
-        OpacNavBottom                         => C4::Context->preference("OpacNavBottom"),
         OpacFavicon                           => C4::Context->preference("OpacFavicon"),
         opacreadinghistory                    => C4::Context->preference("opacreadinghistory"),
         opaclanguagesdisplay                  => C4::Context->preference("opaclanguagesdisplay"),
@@ -1278,7 +1388,6 @@ sub checkauth {
         OPACUserCSS                           => C4::Context->preference("OPACUserCSS"),
         intranetcolorstylesheet               => C4::Context->preference("intranetcolorstylesheet"),
         intranetstylesheet                    => C4::Context->preference("intranetstylesheet"),
-        intranetbookbag                       => C4::Context->preference("intranetbookbag"),
         IntranetNav                           => C4::Context->preference("IntranetNav"),
         IntranetFavicon                       => C4::Context->preference("IntranetFavicon"),
         IntranetUserCSS                       => C4::Context->preference("IntranetUserCSS"),
@@ -1296,12 +1405,18 @@ sub checkauth {
     $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 ( $additional_auth_needed ) {
+        $template->param(
+            TwoFA_prompt => 1,
+            invalid_otp_token => $invalid_otp_token,
+        );
+    }
 
     if ( $type eq 'opac' ) {
         require Koha::Virtualshelves;
         my $some_public_shelves = Koha::Virtualshelves->get_some_shelves(
             {
-                category       => 2,
+                public => 1,
             }
         );
         $template->param(
@@ -1312,8 +1427,9 @@ sub checkauth {
     if ($cas) {
 
         # Is authentication against multiple CAS servers enabled?
-        if ( C4::Auth_with_cas::multipleAuth && !$casparam ) {
-            my $casservers = C4::Auth_with_cas::getMultipleAuth();
+        require C4::Auth_with_cas;
+        if ( multipleAuth() && !$casparam ) {
+            my $casservers = getMultipleAuth();
             my @tmplservers;
             foreach my $key ( keys %$casservers ) {
                 push @tmplservers, { name => $key, value => login_cas_url( $query, $key, $type ) . "?cas=$key" };
@@ -1333,6 +1449,13 @@ sub checkauth {
     }
 
     if ($shib) {
+        #If shibOnly is enabled just go ahead and redirect directly
+        if ( (($type eq 'opac') && C4::Context->preference('OPACShibOnly')) || (($type ne 'opac') && C4::Context->preference('staffShibOnly')) ) {
+            my $redirect_url = login_shib_url( $query );
+            print $query->redirect( -uri => "$redirect_url", -status => 303 );
+            safe_exit;
+        }
+
         $template->param(
             shibbolethAuthentication => $shib,
             shibbolethLoginUrl       => login_shib_url($query),
@@ -1357,7 +1480,8 @@ sub checkauth {
         {   type              => 'text/html',
             charset           => 'utf-8',
             cookie            => $cookie,
-            'X-Frame-Options' => 'SAMEORIGIN'
+            'X-Frame-Options' => 'SAMEORIGIN',
+            -sameSite => 'Lax'
         }
       ),
       $template->output;
@@ -1394,6 +1518,10 @@ Possible return values in C<$status> are:
 
 =item "expired -- session cookie has expired; API user should resubmit userid and password
 
+=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
+
+=item "additional-auth-needed -- User is in an authentication process that is not finished
+
 =back
 
 =cut
@@ -1420,79 +1548,28 @@ sub check_api_auth {
         return ( "maintenance", undef, undef );
     }
 
-    # FIXME -- most of what follows is a copy-and-paste
-    # of code from checkauth.  There is an obvious need
-    # for refactoring to separate the various parts of
-    # the authentication code, but as of 2007-11-19 this
-    # is deferred so as to not introduce bugs into the
-    # regular authentication code for Koha 3.0.
-
-    # see if we have a valid session cookie already
-    # however, if a userid parameter is present (i.e., from
-    # a form submission, assume that any current cookie
-    # is to be ignored
-    my $sessionID = undef;
+    my ( $sessionID, $session );
     unless ( $query->param('userid') ) {
         $sessionID = $query->cookie("CGISESSID");
     }
     if ( $sessionID && not( $cas && $query->param('PT') ) ) {
-        my $session = get_session($sessionID);
-        C4::Context->_new_userenv($sessionID);
-        if ($session) {
-            C4::Context->interface($session->param('interface'));
-            C4::Context->set_userenv(
-                $session->param('number'),       $session->param('id'),
-                $session->param('cardnumber'),   $session->param('firstname'),
-                $session->param('surname'),      $session->param('branch'),
-                $session->param('branchname'),   $session->param('flags'),
-                $session->param('emailaddress'), $session->param('shibboleth'),
-                $session->param('desk_id'),      $session->param('desk_name')
-            );
 
-            my $ip       = $session->param('ip');
-            my $lasttime = $session->param('lasttime');
-            my $userid   = $session->param('id');
-            if ( $lasttime < time() - $timeout ) {
+        my $return;
+        ( $return, $session, undef ) = check_cookie_auth(
+            $sessionID, $flagsrequired, { remote_addr => $ENV{REMOTE_ADDR} } );
 
-                # time out
-                $session->delete();
-                $session->flush;
-                C4::Context->_unset_userenv($sessionID);
-                $userid    = undef;
-                $sessionID = undef;
-                return ( "expired", undef, undef );
-            } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $ENV{'REMOTE_ADDR'} ) {
+        return ( $return, undef, undef ) # Cookie auth failed
+            if $return ne "ok";
+
+        my $cookie = $query->cookie(
+            -name     => 'CGISESSID',
+            -value    => $session->id,
+            -HttpOnly => 1,
+            -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
+            -sameSite => 'Lax'
+        );
+        return ( $return, $cookie, $session ); # return == 'ok' here
 
-                # IP address changed
-                $session->delete();
-                $session->flush;
-                C4::Context->_unset_userenv($sessionID);
-                $userid    = undef;
-                $sessionID = undef;
-                return ( "expired", undef, undef );
-            } else {
-                my $cookie = $query->cookie(
-                    -name     => 'CGISESSID',
-                    -value    => $session->id,
-                    -HttpOnly => 1,
-                    -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
-                );
-                $session->param( 'lasttime', time() );
-                my $flags = haspermission( $userid, $flagsrequired );
-                if ($flags) {
-                    return ( "ok", $cookie, $sessionID );
-                } else {
-                    $session->delete();
-                    $session->flush;
-                    C4::Context->_unset_userenv($sessionID);
-                    $userid    = undef;
-                    $sessionID = undef;
-                    return ( "failed", undef, undef );
-                }
-            }
-        } else {
-            return ( "expired", undef, undef );
-        }
     } else {
 
         # new login
@@ -1503,7 +1580,6 @@ sub check_api_auth {
         # Proxy CAS auth
         if ( $cas && $query->param('PT') ) {
             my $retuserid;
-            $debug and print STDERR "## check_api_auth - checking CAS\n";
 
             # In case of a CAS authentication, we use the ticket instead of the password
             my $PT = $query->param('PT');
@@ -1531,6 +1607,7 @@ sub check_api_auth {
                 -value    => $sessionID,
                 -HttpOnly => 1,
                 -secure => ( C4::Context->https_enabled() ? 1 : 0 ),
+                -sameSite => 'Lax'
             );
             if ( $return == 1 ) {
                 my (
@@ -1577,7 +1654,7 @@ sub check_api_auth {
                     my $library = Koha::Libraries->find($branchcode);
                     $branchname = $library? $library->branchname: '';
                 }
-                my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search };
+                my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search->as_list };
                 foreach my $br ( keys %$branches ) {
 
                     #     now we work with the treatment of ip
@@ -1607,8 +1684,10 @@ sub check_api_auth {
                 $session->param('number'),       $session->param('id'),
                 $session->param('cardnumber'),   $session->param('firstname'),
                 $session->param('surname'),      $session->param('branch'),
+                $session->param('branchname'),   $session->param('flags'),
                 $session->param('emailaddress'), $session->param('shibboleth'),
-                $session->param('desk_id'),      $session->param('desk_name')
+                $session->param('desk_id'),      $session->param('desk_name'),
+                $session->param('register_id'),  $session->param('register_name')
             );
             return ( "ok", $cookie, $sessionID );
         } else {
@@ -1619,7 +1698,7 @@ sub check_api_auth {
 
 =head2 check_cookie_auth
 
-  ($status, $sessionId) = check_api_auth($cookie, $userflags);
+  ($status, $sessionId) = check_cookie_auth($cookie, $userflags);
 
 Given a CGISESSID cookie set during a previous login to Koha, determine
 if the user has the privileges specified by C<$userflags>. C<$userflags>
@@ -1638,102 +1717,103 @@ Possible return values in C<$status> are:
 
 =item "ok" -- user authenticated; C<$sessionID> have valid values.
 
+=item "anon" -- user not authenticated but valid for anonymous session.
+
 =item "failed" -- credentials are not correct; C<$sessionid> are undef
 
 =item "maintenance" -- DB is in maintenance mode; no login possible at the moment
 
 =item "expired -- session cookie has expired; API user should resubmit userid and password
 
+=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
+
 =back
 
 =cut
 
 sub check_cookie_auth {
-    my $cookie        = shift;
+    my $sessionID     = shift;
     my $flagsrequired = shift;
     my $params        = shift;
 
     my $remote_addr = $params->{remote_addr} || $ENV{REMOTE_ADDR};
-    my $dbh     = C4::Context->dbh;
-    my $timeout = _timeout_syspref();
 
-    unless ( C4::Context->preference('Version') ) {
+    my $skip_version_check = $params->{skip_version_check}; # Only for checkauth
 
-        # database has not been installed yet
-        return ( "maintenance", undef );
-    }
-    my $kohaversion = Koha::version();
-    $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
-    if ( C4::Context->preference('Version') < $kohaversion ) {
+    unless ( $skip_version_check ) {
+        unless ( C4::Context->preference('Version') ) {
 
-        # database in need of version update; assume that
-        # no API should be called while databsae is in
-        # this condition.
-        return ( "maintenance", undef );
+            # database has not been installed yet
+            return ( "maintenance", undef );
+        }
+        my $kohaversion = Koha::version();
+        $kohaversion =~ s/(.*\..*)\.(.*)\.(.*)/$1$2$3/;
+        if ( C4::Context->preference('Version') < $kohaversion ) {
+
+            # database in need of version update; assume that
+            # no API should be called while databsae is in
+            # this condition.
+            return ( "maintenance", undef );
+        }
     }
 
-    # FIXME -- most of what follows is a copy-and-paste
-    # of code from checkauth.  There is an obvious need
-    # for refactoring to separate the various parts of
-    # the authentication code, but as of 2007-11-23 this
-    # is deferred so as to not introduce bugs into the
-    # regular authentication code for Koha 3.0.
-
     # see if we have a valid session cookie already
     # however, if a userid parameter is present (i.e., from
     # a form submission, assume that any current cookie
     # is to be ignored
-    unless ( defined $cookie and $cookie ) {
+    unless ( $sessionID ) {
         return ( "failed", undef );
     }
-    my $sessionID = $cookie;
+    C4::Context::_unset_userenv($sessionID); # remove old userenv first
     my $session   = get_session($sessionID);
-    C4::Context->_new_userenv($sessionID);
     if ($session) {
-        C4::Context->interface($session->param('interface'));
-        C4::Context->set_userenv(
-            $session->param('number'),       $session->param('id'),
-            $session->param('cardnumber'),   $session->param('firstname'),
-            $session->param('surname'),      $session->param('branch'),
-            $session->param('branchname'),   $session->param('flags'),
-            $session->param('emailaddress'), $session->param('shibboleth'),
-            $session->param('desk_id'),      $session->param('desk_name')
-        );
-
+        my $userid   = $session->param('id');
         my $ip       = $session->param('ip');
         my $lasttime = $session->param('lasttime');
-        my $userid   = $session->param('id');
-        if ( $lasttime < time() - $timeout ) {
+        my $timeout = _timeout_syspref();
 
+        if ( !$lasttime || ( $lasttime < time() - $timeout ) ) {
             # time out
             $session->delete();
             $session->flush;
-            C4::Context->_unset_userenv($sessionID);
-            $userid    = undef;
-            $sessionID = undef;
             return ("expired", undef);
-        } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
 
+        } elsif ( C4::Context->preference('SessionRestrictionByIP') && $ip ne $remote_addr ) {
             # IP address changed
             $session->delete();
             $session->flush;
-            C4::Context->_unset_userenv($sessionID);
-            $userid    = undef;
-            $sessionID = undef;
-            return ( "expired", undef );
-        } else {
+            return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr});
+
+        } elsif ( $userid ) {
             $session->param( 'lasttime', time() );
             my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
             if ($flags) {
-                return ( "ok", $sessionID );
+                C4::Context->_new_userenv($sessionID);
+                C4::Context->interface($session->param('interface'));
+                C4::Context->set_userenv(
+                    $session->param('number'),       $session->param('id') // '',
+                    $session->param('cardnumber'),   $session->param('firstname'),
+                    $session->param('surname'),      $session->param('branch'),
+                    $session->param('branchname'),   $session->param('flags'),
+                    $session->param('emailaddress'), $session->param('shibboleth'),
+                    $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();
                 $session->flush;
-                C4::Context->_unset_userenv($sessionID);
-                $userid    = undef;
-                $sessionID = undef;
                 return ( "failed", undef );
             }
+
+        } else {
+            C4::Context->_new_userenv($sessionID);
+            C4::Context->interface($session->param('interface'));
+            C4::Context->set_userenv( undef, q{} );
+            return ( "anon", $session );
         }
     } else {
         return ( "expired", undef );
@@ -1759,28 +1839,36 @@ sub _get_session_params {
     my $storage_method = C4::Context->preference('SessionStorage');
     if ( $storage_method eq 'mysql' ) {
         my $dbh = C4::Context->dbh;
-        return { dsn => "driver:MySQL;serializer:yaml;id:md5", dsn_args => { Handle => $dbh } };
+        return { dsn => "serializer:yamlxs;driver:MySQL;id:md5", dsn_args => { Handle => $dbh } };
     }
     elsif ( $storage_method eq 'Pg' ) {
         my $dbh = C4::Context->dbh;
-        return { dsn => "driver:PostgreSQL;serializer:yaml;id:md5", dsn_args => { Handle => $dbh } };
+        return { dsn => "serializer:yamlxs;driver:PostgreSQL;id:md5", dsn_args => { Handle => $dbh } };
     }
     elsif ( $storage_method eq 'memcached' && Koha::Caches->get_instance->memcached_cache ) {
         my $memcached = Koha::Caches->get_instance()->memcached_cache;
-        return { dsn => "driver:memcached;serializer:yaml;id:md5", dsn_args => { Memcached => $memcached } };
+        return { dsn => "serializer:yamlxs;driver:memcached;id:md5", dsn_args => { Memcached => $memcached } };
     }
     else {
         # catch all defaults to tmp should work on all systems
         my $dir = C4::Context::temporary_directory;
         my $instance = C4::Context->config( 'database' ); #actually for packages not exactly the instance name, but generally safer to leave it as it is
-        return { dsn => "driver:File;serializer:yaml;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
+        return { dsn => "serializer:yamlxs;driver:File;id:md5", dsn_args => { Directory => "$dir/cgisess_$instance" } };
     }
 }
 
 sub get_session {
     my $sessionID      = shift;
     my $params = _get_session_params();
-    return new CGI::Session( $params->{dsn}, $sessionID, $params->{dsn_args} );
+    my $session;
+    if( $sessionID ) { # find existing
+        CGI::Session::ErrorHandler->set_error( q{} ); # clear error, cpan issue #111463
+        $session = CGI::Session->load( $params->{dsn}, $sessionID, $params->{dsn_args} );
+    } else {
+        $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
+        # no need to flush here
+    }
+    return $session;
 }
 
 
@@ -1812,7 +1900,6 @@ sub checkpw {
     if ( $patron and $patron->account_locked ) {
         # Nothing to check, account is locked
     } elsif ($ldap && defined($password)) {
-        $debug and print STDERR "## checkpw - checking LDAP\n";
         my ( $retval, $retcard, $retuserid ) = checkpw_ldap(@_);    # EXTERNAL AUTH
         if ( $retval == 1 ) {
             @return = ( $retval, $retcard, $retuserid );
@@ -1821,7 +1908,6 @@ sub checkpw {
         $check_internal_as_fallback = 1 if $retval == 0;
 
     } elsif ( $cas && $query && $query->param('ticket') ) {
-        $debug and print STDERR "## checkpw - checking CAS\n";
 
         # In case of a CAS authentication, we use the ticket instead of the password
         my $ticket = $query->param('ticket');
@@ -1840,8 +1926,6 @@ sub checkpw {
     # time around.
     elsif ( $shib && $shib_login && !$password ) {
 
-        $debug and print STDERR "## checkpw - checking Shibboleth\n";
-
         # In case of a Shibboleth authentication, we expect a shibboleth user attribute
         # (defined under shibboleth mapping in koha-conf.xml) to contain the login of the
         # shibboleth-authenticated user
@@ -2147,7 +2231,7 @@ sub in_iprange {
     if (scalar @allowedipranges > 0) {
         my @rangelist;
         eval { @rangelist = Net::CIDR::range2cidr(@allowedipranges); }; return 0 if $@;
-        eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || ( $ENV{DEBUG} && warn 'cidrlookup failed for ' . join(' ',@rangelist) );
+        eval { $result = Net::CIDR::cidrlookup($ENV{'REMOTE_ADDR'}, @rangelist) } || Koha::Logger->get->warn('cidrlookup failed for ' . join(' ',@rangelist) );
      }
      return $result ? 1 : 0;
 }