X-Git-Url: http://koha-dev.rot13.org:8081/gitweb/?a=blobdiff_plain;f=C4%2FAuth.pm;h=a2b1b84aea004a4cd191f7f9b031ee1fa457fff2;hb=eded6edacc5e3bf8dd0be21ed05842c3b78aadce;hp=284bc4ed908d5a0ee9779a1e41c3087415d869ce;hpb=1a216186ff97c292ef6292cd98572015cc7c5a32;p=koha-ffzg.git diff --git a/C4/Auth.pm b/C4/Auth.pm index 284bc4ed90..a2b1b84aea 100644 --- a/C4/Auth.pm +++ b/C4/Auth.pm @@ -23,6 +23,9 @@ use Carp qw( croak ); use Digest::MD5 qw( md5_base64 ); use CGI::Session; +use CGI::Session::ErrorHandler; +use URI; +use URI::QueryParam; use C4::Context; use C4::Templates; # to get the template @@ -32,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; @@ -45,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; @@ -72,14 +77,14 @@ BEGIN { $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); } } @@ -151,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(); @@ -196,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; @@ -228,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, @@ -287,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( @@ -333,6 +353,7 @@ sub get_template_and_user { $template->param( CAN_user_stockrotation => 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} } ) { @@ -383,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 (?, ?, ?, ?, ?, ?, ?) @@ -431,12 +451,15 @@ 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}; } } @@ -479,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"), @@ -501,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"), @@ -526,7 +547,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 { @@ -546,9 +567,10 @@ sub get_template_and_user { 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; } } @@ -568,7 +590,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, @@ -588,14 +610,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'} : '', @@ -639,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 ); @@ -808,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; @@ -839,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, @@ -852,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) { @@ -866,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'); + } - my $s_userid = $session->param('id'); - $userid = $s_userid; + $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; + } + } + + 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'} ) ) { @@ -886,42 +939,23 @@ sub checkauth { $anon_search_history = $session->param('search_history'); $session->delete(); $session->flush; - C4::Context->_unset_userenv($sessionID); - } - 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); - - if ($cas and $caslogout) { - logout_cas($query, $type); - } + $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie ); + C4::Context::_unset_userenv($sessionID); + $sessionID = undef; + } elsif (!$logout) { - # 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', + )); - my $sessiontype = $session->param('sessiontype') || ''; - 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; - } else { - $info{'nopermission'} = 1; - } + $flags = haspermission( $userid, $flagsrequired ); + unless ( $flags ) { + $auth_state = 'failed'; + $info{'nopermission'} = 1; } } } elsif ( !$logout ) { @@ -931,18 +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... - 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 @@ -953,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"; @@ -979,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); @@ -990,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); } @@ -1059,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); } @@ -1084,14 +1148,14 @@ 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; - C4::Context->_unset_userenv($sessionID); + C4::Context::_unset_userenv($sessionID); } my ( $borrowernumber, $firstname, $surname, $userflags, $branchcode, $branchname, $emailaddress, $desk_id, @@ -1104,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 ) { @@ -1145,7 +1210,7 @@ sub checkauth { $register_id = $register->id if ($register); $register_name = $register->name if ($register); } - my $branches = { map { $_->branchcode => $_->unblessed } Koha::Libraries->search }; + 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 @@ -1153,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; } } @@ -1174,6 +1240,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 ); @@ -1191,6 +1263,7 @@ sub checkauth { $session->param( 'shibboleth', $shibSuccess ); $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( @@ -1209,7 +1282,7 @@ sub checkauth { else { 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() ); @@ -1227,19 +1300,36 @@ sub checkauth { $session->param( 'sessiontype', 'anon' ); $session->param( 'interface', $type); } + $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 ); @@ -1265,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; @@ -1288,13 +1379,10 @@ sub checkauth { casAuthentication => C4::Context->preference("casAuthentication"), shibbolethAuthentication => $shib, suggestion => C4::Context->preference("suggestion"), - SessionRestrictionByIP => C4::Context->preference("SessionRestrictionByIP"), 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"), @@ -1318,19 +1406,28 @@ 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; my $some_public_shelves = Koha::Virtualshelves->get_some_shelves( { - category => 2, + public => 1, } ); $template->param( @@ -1341,8 +1438,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" }; @@ -1393,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; @@ -1432,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 @@ -1440,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') ) { @@ -1476,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 { @@ -1492,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 @@ -1502,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 ) ) { @@ -1516,6 +1617,7 @@ sub check_api_auth { -value => $sessionID, -HttpOnly => 1, -secure => ( C4::Context->https_enabled() ? 1 : 0 ), + -sameSite => 'Lax' ); if ( $return == 1 ) { my ( @@ -1523,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=?" @@ -1562,7 +1665,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 @@ -1606,7 +1709,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> @@ -1625,6 +1728,8 @@ 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 @@ -1667,53 +1772,62 @@ sub check_cookie_auth { # however, if a userid parameter is present (i.e., from # a form submission, assume that any current cookie # is to be ignored - unless ( defined $sessionID and $sessionID ) { + unless ( $sessionID ) { return ( "failed", undef ); } + 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'), - $session->param('register_id'), $session->param('register_name') - ); - my $userid = $session->param('id'); my $ip = $session->param('ip'); my $lasttime = $session->param('lasttime'); my $timeout = _timeout_syspref(); if ( !$lasttime || ( $lasttime < time() - $timeout ) ) { - # time out $session->delete(); $session->flush; - C4::Context->_unset_userenv($sessionID); 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); return ( "restricted", undef, { old_ip => $ip, new_ip => $remote_addr}); - } else { + + } 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); + 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); 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 ); @@ -1760,9 +1874,13 @@ sub _get_session_params { sub get_session { my $sessionID = shift; my $params = _get_session_params(); - my $session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} ); - if ( ! $session ){ - die CGI::Session->errstr(); + 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; } @@ -1773,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 @@ -1793,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 @@ -1808,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 { @@ -1840,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 }); } @@ -1863,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=?"