From 2427d773a901926727888a43c471f90f1beb4bf8 Mon Sep 17 00:00:00 2001 From: Marcel de Rooy Date: Mon, 14 Nov 2022 11:07:09 +0000 Subject: [PATCH] Bug 31908: Add a test to show issue Test plan: Without next patch, run Auth.t. Should fail now before next patch resolves problem: not ok 2 - Login of patron2 approved ok 3 - Did not return previous session ID not ok 4 - New session ID not empty Signed-off-by: Marcel de Rooy Signed-off-by: David Cook Signed-off-by: Martin Renvoize Signed-off-by: Wainui Witika-Park --- t/db_dependent/Auth.t | 194 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/t/db_dependent/Auth.t b/t/db_dependent/Auth.t index b9b413842e..9e013bb1c4 100755 --- a/t/db_dependent/Auth.t +++ b/t/db_dependent/Auth.t @@ -38,7 +38,7 @@ $schema->storage->txn_begin; subtest 'checkauth() tests' => sub { - plan tests => 4; + plan tests => 7; my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => undef } }); @@ -108,6 +108,198 @@ subtest 'checkauth() tests' => sub { is( $userid, undef, 'If librarian user is used and password with GET, they should not be logged in' ); }; + subtest 'Template params tests (password_expired)' => sub { + + plan tests => 1; + + my $password_expired; + + my $patron_class = Test::MockModule->new('Koha::Patron'); + $patron_class->mock( 'password_expired', sub { return $password_expired; } ); + + my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 } }); + my $password = 'password'; + t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 ); + $patron->set_password( { password => $password } ); + + my $cgi_mock = Test::MockModule->new('CGI')->mock( 'request_method', 'POST' ); + my $cgi = CGI->new; + $cgi->param( -name => 'userid', -value => $patron->userid ); + $cgi->param( -name => 'password', -value => $password ); + + my $auth = Test::MockModule->new( 'C4::Auth' ); + # Tests will fail if we hit safe_exit + $auth->mock( 'safe_exit', sub { return } ); + + my ( $userid, $cookie, $sessionID, $flags ); + + { + t::lib::Mocks::mock_preference( 'DumpTemplateVarsOpac', 1 ); + # checkauth will redirect and safe_exit if not authenticated and not authorized + local *STDOUT; + my $stdout; + open STDOUT, '>', \$stdout; + + # Password has expired + $password_expired = 1; + C4::Auth::checkauth( $cgi, 0, { catalogue => 1 } ); + like( $stdout, qr{'password_has_expired' => 1}, 'password_has_expired is set to 1' ); + + close STDOUT; + }; + }; + + subtest 'While still logged in, relogin with another user' => sub { + plan tests => 4; + my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {} }); + my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => {} }); + # Create 'former' session + my $session = C4::Auth::get_session(); + $session->param( 'number', $patron->id ); + $session->param( 'id', $patron->userid ); + $session->param( 'ip', '1.2.3.4' ); + $session->param( 'lasttime', time() ); + $session->param( 'interface', 'opac' ); + $session->flush; + my $sessionID = $session->id; + C4::Context->_new_userenv($sessionID); + + my ( $return ) = C4::Auth::check_cookie_auth( $sessionID, undef, { skip_version_check => 1, remote_addr => '1.2.3.4' } ); + is( $return, 'ok', 'Former session in shape now' ); + + my $mock1 = Test::MockModule->new('C4::Auth')->mock( 'safe_exit', sub {} ); + my $mock2 = Test::MockModule->new('CGI') ->mock( 'request_method', 'POST' ) + ->mock( 'cookie', sub { return $sessionID; } ); # oversimplified.. + my $cgi = CGI->new; + my $password = 'Incr3d1blyZtr@ng93$'; + $patron2->set_password({ password => $password }); + $cgi->param( -name => 'userid', -value => $patron2->userid ); + $cgi->param( -name => 'password', -value => $password ); + $cgi->param( -name => 'koha_login_context', -value => 1 ); + my @return; + { + local *STDOUT; + local %ENV; + $ENV{REMOTE_ADDR} = '1.2.3.4'; + my $stdout; + open STDOUT, '>', \$stdout; + @return = C4::Auth::checkauth( $cgi, 0, {} ); + close STDOUT; + } + # Note: We can test return values from checkauth here since we mocked the safe_exit after the Redirect 303 + is( $return[0], $patron2->userid, 'Login of patron2 approved' ); + isnt( $return[2], $sessionID, 'Did not return previous session ID' ); + ok( $return[2], 'New session ID not empty' ); + }; + + subtest 'Two-factor authentication' => sub { + plan tests => 18; + + my $patron = $builder->build_object( + { class => 'Koha::Patrons', value => { flags => 1 } } ); + my $password = 'password'; + $patron->set_password( { password => $password } ); + $cgi = Test::MockObject->new(); + + my $otp_token; + our ( $logout, $sessionID, $verified ); + $cgi->mock( + 'param', + sub { + my ( $self, $param ) = @_; + if ( $param eq 'userid' ) { return $patron->userid; } + elsif ( $param eq 'password' ) { return $password; } + elsif ( $param eq 'otp_token' ) { return $otp_token; } + elsif ( $param eq 'logout.x' ) { return $logout; } + else { return; } + } + ); + $cgi->mock( 'request_method', sub { return 'POST' } ); + $cgi->mock( 'cookie', sub { return $sessionID } ); + + my $two_factor_auth = Test::MockModule->new( 'Koha::Auth::TwoFactorAuth' ); + $two_factor_auth->mock( 'verify', sub {$verified} ); + + my ( $userid, $cookie, $flags ); + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + + sub logout { + my $cgi = shift; + $logout = 1; + undef $sessionID; + C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + $logout = 0; + } + + t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'disabled' ); + $patron->auth_method('password')->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' ); + logout($cgi); + + $patron->auth_method('two-factor')->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' ); + logout($cgi); + + t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'enabled' ); + t::lib::Mocks::mock_config('encryption_key', '1234tH1s=t&st'); + $patron->auth_method('password')->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' ); + logout($cgi); + + $patron->encode_secret('one_secret'); + $patron->auth_method('two-factor'); + $patron->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + my $session = C4::Auth::get_session($sessionID); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth required' ); + + # Wrong OTP token + $otp_token = "wrong"; + $verified = 0; + $patron->auth_method('two-factor')->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth still required after wrong OTP token' ); + + $otp_token = "good"; + $verified = 1; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 0, 'Second auth no longer required if OTP token has been verified' ); + logout($cgi); + + t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'enforced' ); + $patron->auth_method('password')->store; + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' ); + is( $userid, $patron->userid, 'Succesful login' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA-setup'), 1, 'Setup 2FA required' ); + logout($cgi); + + ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'opac' ); + is( $userid, $patron->userid, 'Succesful login at the OPAC' ); + is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'No second auth required at the OPAC' ); + + # + t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 'disabled' ); + $session = C4::Auth::get_session($sessionID); + $session->param('waiting-for-2FA', 1); + $session->flush; + my ($auth_status, undef ) = C4::Auth::check_cookie_auth($sessionID, undef ); + is( $auth_status, 'ok', 'User authenticated, pref was disabled, access OK' ); + $session->param('waiting-for-2FA', 0); + $session->param('waiting-for-2FA-setup', 1); + $session->flush; + ($auth_status, undef ) = C4::Auth::check_cookie_auth($sessionID, undef ); + is( $auth_status, 'ok', 'User waiting for 2FA setup, pref was disabled, access OK' ); + }; + C4::Context->_new_userenv; # For next tests }; -- 2.39.5