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;
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;
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();
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,
# 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 (?, ?, ?, ?, ?, ?, ?)
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"),
$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"),
# 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 );
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;
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,
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) {
}
elsif ( $sessionID = $query->cookie("CGISESSID") ) { # assignment, not comparison
my ( $return, $more_info );
+ # NOTE: $flags in the following call is still undefined !
( $return, $session, $more_info ) = check_cookie_auth( $sessionID, $flags,
{ remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
);
- if ( $return eq 'ok' ) {
- Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
+ if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
+ $userid = $session->param('id');
+ }
+
+ $auth_state =
+ $return eq 'ok' ? 'completed'
+ : $return eq 'additional-auth-needed' ? 'additional-auth-needed'
+ : 'failed';
+
+ # We are at the second screen if the waiting-for-2FA is set in session
+ # and otp_token param has been passed
+ if ( $require_2FA
+ && $auth_state eq 'additional-auth-needed'
+ && ( my $otp_token = $query->param('otp_token') ) )
+ {
+ my $patron = Koha::Patrons->find( { userid => $userid } );
+ my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
+ my $verified = $auth->verify($otp_token, 1);
+ $auth->clear;
+ if ( $verified ) {
+ # The token is correct, the user is fully logged in!
+ $auth_state = 'completed';
+ $session->param( 'waiting-for-2FA', 0 );
+
+ # This is an ugly trick to pass the test
+ # $query->param('koha_login_context') && ( $q_userid ne $userid )
+ # few lines later
+ $q_userid = $userid;
+ }
+ else {
+ $invalid_otp_token = 1;
+ }
+ }
- my $s_userid = $session->param('id');
- $userid = $s_userid;
+ if ( $auth_state eq 'completed' ) {
+ Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
- if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
+ if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
|| ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
|| ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
) {
$anon_search_history = $session->param('search_history');
$session->delete();
$session->flush;
+ $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
C4::Context::_unset_userenv($sessionID);
$sessionID = undef;
- }
- elsif ($logout) {
-
- # voluntary logout the user
- # check wether the user was using their shibboleth session or a local one
- my $shibSuccess = C4::Context->userenv->{'shibboleth'};
- $session->delete();
- $session->flush;
- C4::Context::_unset_userenv($sessionID);
- $sessionID = undef;
+ } elsif (!$logout) {
- if ($cas and $caslogout) {
- logout_cas($query, $type);
- }
-
- # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
- if ( $shib and $shib_login and $shibSuccess) {
- logout_shib($query);
- }
- } else {
-
- $cookie = $query->cookie(
+ $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
-name => 'CGISESSID',
-value => $session->id,
-HttpOnly => 1,
-secure => ( C4::Context->https_enabled() ? 1 : 0 ),
- );
+ -sameSite => 'Lax',
+ ));
$flags = haspermission( $userid, $flagsrequired );
- if ($flags) {
- $loggedin = 1;
- } else {
+ unless ( $flags ) {
+ $auth_state = 'failed';
$info{'nopermission'} = 1;
}
}
$info{oldip} = $more_info->{old_ip};
$info{newip} = $more_info->{new_ip};
$info{different_ip} = 1;
+ } elsif ( $return eq 'password_expired' ) {
+ $info{password_has_expired} = 1;
}
}
}
- unless ( $loggedin ) {
+ if ( $auth_state eq 'failed' || $logout ) {
+ $sessionID = undef;
$userid = undef;
}
+ if ($logout) {
+
+ # voluntary logout the user
+ # check wether the user was using their shibboleth session or a local one
+ my $shibSuccess = C4::Context->userenv ? C4::Context->userenv->{'shibboleth'} : undef;
+ if ( $session ) {
+ $session->delete();
+ $session->flush;
+ }
+ C4::Context::_unset_userenv($sessionID);
+ $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
+
+ if ($cas and $caslogout) {
+ logout_cas($query, $type);
+ }
+
+ # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
+ if ( $shib and $shib_login and $shibSuccess) {
+ logout_shib($query);
+ }
+
+ $session = undef;
+ $auth_state = 'logout';
+ }
+
unless ( $userid ) {
#we initiate a session prior to checking for a username to allow for anonymous sessions...
- $session ||= get_session("") or die "Auth ERROR: Cannot get_session()";
+ if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
+ $session = get_session() or die "Auth ERROR: Cannot get_session()";
+ }
# Save anonymous search history in new session so it can be retrieved
# by get_template_and_user to store it in user's search history after
$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";
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);
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);
}
{
( $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);
}
}
# $return: 1 = valid user
- if ($return) {
+ if ($return > 0) {
if ( $flags = haspermission( $userid, $flagsrequired ) ) {
- $loggedin = 1;
+ $auth_state = "logged_in";
}
else {
$info{'nopermission'} = 1;
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 ) {
$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;
}
}
$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 );
#
#
+ 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;
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;
{ type => 'text/html',
charset => 'utf-8',
cookie => $cookie,
- 'X-Frame-Options' => 'SAMEORIGIN'
+ 'X-Frame-Options' => 'SAMEORIGIN',
+ -sameSite => 'Lax'
}
),
$template->output;
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
+=item "additional-auth-needed -- User is in an authentication process that is not finished
+
=back
=cut
my $query = shift;
my $flagsrequired = shift;
- my $dbh = C4::Context->dbh;
my $timeout = _timeout_syspref();
unless ( C4::Context->preference('Version') ) {
-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 {
# 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
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 ) ) {
-value => $sessionID,
-HttpOnly => 1,
-secure => ( C4::Context->https_enabled() ? 1 : 0 ),
+ -sameSite => 'Lax'
);
if ( $return == 1 ) {
my (
$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=?"
} 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);
$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 = CGI::Session->load( $params->{dsn}, $sessionID, $params->{dsn_args} );
} else {
$session = CGI::Session->new( $params->{dsn}, $sessionID, $params->{dsn_args} );
- # $session->flush;
+ # no need to flush here
}
return $session;
}
# 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
# 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
# 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 {
# 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 });
}
}
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=?"