Bugzilla – Attachment 123379 Details for
Bug 28786
Two-factor authentication for staff client - TOTP
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 28786: Two-factor authentication for staff client - TOTP
Bug-28786-Two-factor-authentication-for-staff-clie.patch (text/plain), 25.98 KB, created by
Jonathan Druart
on 2021-08-02 13:14:30 UTC
(
hide
)
Description:
Bug 28786: Two-factor authentication for staff client - TOTP
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2021-08-02 13:14:30 UTC
Size:
25.98 KB
patch
obsolete
>From 0ca43288001e799193ab03069d3df4a5d5ae7ab8 Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Thu, 29 Jul 2021 16:08:25 +0200 >Subject: [PATCH] Bug 28786: Two-factor authentication for staff client - TOTP > >This patchset introduces the Two-factor authentication (2FA) idea in >Koha. > >It is far for complete, and only implement one way of doing it, but at >least it's a first step. >The idea here is to offer the librarian user the ability to >enable/disable 2FA when logging in to Koha. > >It will use time-based, one-time passwords (TOTP) as the second factor, >an application to handle that will be required. > >https://en.wikipedia.org/wiki/Time-based_One-Time_Password > >More developements are possible on top of this: >* Send a notice (sms or email) with the code >* Force 2FA for librarians >* Implementation for OPAC >* WebAuthn, FIDO2, etc. - https://fidoalliance.org/category/intro-fido/ > >Test plan: > 0. > a. % apt install -y libauth-googleauth-perl && updatedatabase && restart_all > b. To test this you will need an app to generate the TOTP token, you can > use FreeOTP that is open source and easy to use. > 1. Turn on TwoFactorAuthentication > 2. Go to your account, click 'More' > 'Manage Two-Factor authentication' > 3. Click Enable, scan the QR code with the app, insert the pin code and > register > 4. Your account now requires 2FA to login! > 5. Notice that you can browse until you logout > 6. Logout > 7. Enter the credential and the pincode provided by the app > 8. Logout > 9. Enter the credential, no pincode >10. Confirm that you are stuck on the second auth form (ie. you cannot >access other Koha pages) >11. Click logout => First login form >12. Enter the credential and the pincode provided by the app > >Sponsored-by: Orex Digital >--- > C4/Auth.pm | 125 +++++++++--- > Koha/Auth/TwoFactorAuth.pm | 31 +++ > cpanfile | 1 + > .../prog/en/includes/members-toolbar.inc | 2 +- > .../intranet-tmpl/prog/en/modules/auth.tt | 19 +- > .../en/modules/members/two_factor_auth.tt | 10 +- > t/db_dependent/selenium/authentication_2fa.t | 178 ++++++++++++++++++ > t/lib/TestBuilder.pm | 1 + > 8 files changed, 330 insertions(+), 37 deletions(-) > create mode 100644 Koha/Auth/TwoFactorAuth.pm > create mode 100755 t/db_dependent/selenium/authentication_2fa.t > >diff --git a/C4/Auth.pm b/C4/Auth.pm >index e732a338697..db56ecdedae 100644 >--- a/C4/Auth.pm >+++ b/C4/Auth.pm >@@ -32,6 +32,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; >@@ -843,6 +844,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, >@@ -874,13 +878,43 @@ sub checkauth { > { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 } > ); > >+ if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) { >+ $userid = $session->param('id'); >+ } >+ >+ $additional_auth_needed = ( $return eq 'additional-auth-needed' ) ? 1 : 0; >+ >+ # 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; >+ } >+ else { >+ $invalid_otp_token = 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)); > >- my $s_userid = $session->param('id'); >- $userid = $s_userid; >- >- 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'} ) > ) { >@@ -891,25 +925,7 @@ sub checkauth { > $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); >- } >- >- # 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 { >+ } elsif (!$logout) { > > $cookie = $query->cookie( > -name => 'CGISESSID', >@@ -939,14 +955,41 @@ sub checkauth { > } > } > >- unless ( $loggedin ) { >+ if ( ( !$loggedin && !$additional_auth_needed ) || $logout ) { > $sessionID = undef; >+ $session = 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); >+ >+ 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); >+ } >+ >+ $sessionID = undef; >+ $session = undef; >+ $userid = 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()"; >+ $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 >@@ -1233,9 +1276,18 @@ sub checkauth { > } > } # 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( >@@ -1269,16 +1321,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; >@@ -1328,6 +1381,12 @@ 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; >@@ -1435,6 +1494,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 >@@ -1710,6 +1771,10 @@ sub check_cookie_auth { > $session->param( 'lasttime', time() ); > my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1; > if ($flags) { >+ >+ return ( "additional-auth-needed", $session ) >+ if $session->param('waiting-for-2FA'); >+ > return ( "ok", $session ); > } else { > $session->delete(); >diff --git a/Koha/Auth/TwoFactorAuth.pm b/Koha/Auth/TwoFactorAuth.pm >new file mode 100644 >index 00000000000..d477d446bec >--- /dev/null >+++ b/Koha/Auth/TwoFactorAuth.pm >@@ -0,0 +1,31 @@ >+package Koha::Auth::TwoFactorAuth; >+ >+use Modern::Perl; >+use Auth::GoogleAuth; >+ >+ >+sub get_auth { >+ my ($params) = @_; >+ my $patron = $params->{patron}; >+ my $secret = $params->{secret}; >+ my $secret32 = $params->{secret32}; >+ >+ if (!$secret && !$secret32){ >+ $secret32 = $patron->secret; >+ } >+ >+ my $issuer = $patron->library->branchname; >+ my $key_id = sprintf "%s_%s", >+ $issuer, ( $patron->email || $patron->userid ); >+ >+ return Auth::GoogleAuth->new( >+ { >+ ( $secret ? ( secret => $secret ) : () ), >+ ( $secret32 ? ( secret32 => $secret32 ) : () ), >+ issuer => $issuer, >+ key_id => $key_id, >+ } >+ ); >+} >+ >+1; >diff --git a/cpanfile b/cpanfile >index 6e13f049320..eeccf40db61 100644 >--- a/cpanfile >+++ b/cpanfile >@@ -1,6 +1,7 @@ > requires 'Algorithm::CheckDigits', '0.5'; > requires 'Array::Utils', '0.5'; > requires 'Authen::CAS::Client', '0.05'; >+requires 'Auth::GoogleAuth', '1.02'; > requires 'Biblio::EndnoteStyle', '0.05'; > requires 'Business::ISBN', '2.05'; > requires 'Business::ISSN', '0.91'; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc >index 369746d1515..035b1b89533 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc >@@ -53,7 +53,7 @@ > [% END %] > > [% IF Koha.Preference('TwoFactorAuthentication') && logged_in_user.borrowernumber == patron.borrowernumber %] >- <li><a id="twofa" href="/cgi-bin/koha/members/two_factor_auth.pl">Enable Two-Factor Authentication</a></li> >+ <li><a id="twofa" href="/cgi-bin/koha/members/two_factor_auth.pl">Manage two-factor authentication</a></li> > [% END %] > > [% IF CAN_user_borrowers_edit_borrowers && useDischarge %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >index da9c0c14c49..e4663294bf2 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >@@ -8,6 +8,7 @@ > [% SET footerjs = 1 %] > [% INCLUDE 'doc-head-open.inc' %] > <title> >+ [% IF TwoFA_prompt %]Two-factor authentication[% END %] > [% IF ( loginprompt ) %]Log in to Koha[% END %] > [% IF too_many_login_attempts %]This account has been locked. > [% ELSIF invalid_username_or_password %]Invalid username or password[% END %] >@@ -61,7 +62,7 @@ > <p>If you have a shibboleth account, please <a href="[% shibbolethLoginUrl | $raw %]">click here</a> to login.</p> > [% END %] > >-[% UNLESS Koha.Preference('staffShibOnly') %] >+[% IF !TwoFA_prompt && !Koha.Preference('staffShibOnly') %] > <!-- login prompt time--> > <form action="[% script_name | html %]" method="post" name="loginform" id="loginform"> > <input type="hidden" name="koha_login_context" value="intranet" /> >@@ -135,6 +136,22 @@ > [% END %] > [% END %] > [% END %] >+[% ELSIF TwoFA_prompt %] >+ <form action="[% script_name | html %]" method="post" name="loginform" id="loginform"> >+ <input type="hidden" name="koha_login_context" value="intranet" /> >+ [% FOREACH INPUT IN INPUTS %] >+ <input type="hidden" name="[% INPUT.name | html %]" value="[% INPUT.value | html %]" /> >+ [% END %] >+ [% IF invalid_otp_token %] >+ <div class="dialog error">Invalid two-factor code</div> >+ [% END %] >+ >+ <p><label for="otp_token">Two-factor authentication code:</label> >+ <input type="text" name="otp_token" id="otp_token" class="input focus" value="" size="20" tabindex="1" /> >+ <p class="submit"><input id="submit-button" type="submit" value="Verify code" /></p> >+ <a class="logout" id="logout" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Log out</a> >+ >+ </form> > [% END %] > > [% IF ( nopermission ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/two_factor_auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/two_factor_auth.tt >index 999f6f9fb67..d067c2dcde8 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/two_factor_auth.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/two_factor_auth.tt >@@ -3,7 +3,7 @@ > [% USE Asset %] > [% SET footerjs = 1 %] > [% INCLUDE 'doc-head-open.inc' %] >-<title>Two-Factor Authentication › Patrons › Koha</title> >+<title>Two-factor authentication › Patrons › Koha</title> > [% INCLUDE 'doc-head-close.inc' %] > </head> > <body id="pat_two_factor_auth" class="pat"> >@@ -34,7 +34,7 @@ > [% INCLUDE 'members-toolbar.inc' %] > > [% IF op == 'register' %] >- <h1>Register Two-Factor Authenticator</h1> >+ <h1>Register two-factor authenticator</h1> > <div class="dialog message"> > <p>We recommend cloud-based mobile authenticator apps such as Authy, Duo Mobile, and LastPass. They can restore access if you lose your hardware device.</p> > <p>Can't scan the code?</p> >@@ -69,14 +69,14 @@ > </fieldset> > </form> > [% ELSE %] >- <h1>Two-Factor Authentication</h1> >+ <h1>Two-factor authentication</h1> > [% IF patron.auth_method == "two-factor" %] > <div class="two-factor-status">Status: Enabled</div> > > <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post"> > <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" /> > <input type="hidden" name="op" value="disable-2FA" /> >- <input type="submit" value="Disable Two-Factor Authentication" /> >+ <input type="submit" value="Disable two-factor authentication" /> > </form> > [% ELSE %] > <div class="two-factor-status">Status: Disabled</div> >@@ -84,7 +84,7 @@ > <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post"> > <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" /> > <input type="hidden" name="op" value="enable-2FA" /> >- <input type="submit" value="Enable Two-Factor Authentication" /> >+ <input type="submit" value="Enable two-factor authentication" /> > </form> > > [% END %] >diff --git a/t/db_dependent/selenium/authentication_2fa.t b/t/db_dependent/selenium/authentication_2fa.t >new file mode 100755 >index 00000000000..e7c761ef637 >--- /dev/null >+++ b/t/db_dependent/selenium/authentication_2fa.t >@@ -0,0 +1,178 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+use Test::More tests => 2; >+ >+use C4::Context; >+use Koha::AuthUtils; >+use Koha::Auth::TwoFactorAuth; >+use t::lib::Mocks; >+use t::lib::Selenium; >+use t::lib::TestBuilder; >+ >+my @data_to_cleanup; >+my $pref_value = C4::Context->preference('TwoFactorAuthentication'); >+ >+SKIP: { >+ eval { require Selenium::Remote::Driver; }; >+ skip "Selenium::Remote::Driver is needed for selenium tests.", 2 if $@; >+ >+ my $builder = t::lib::TestBuilder->new; >+ >+ my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 }}); >+ $patron->flags(1)->store; # superlibrarian permission >+ my $password = Koha::AuthUtils::generate_password($patron->category); >+ t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 ); >+ $patron->set_password({ password => $password }); >+ >+ push @data_to_cleanup, $patron, $patron->category, $patron->library; >+ >+ my $s = t::lib::Selenium->new({ login => $patron->userid, password => $password }); >+ my $driver = $s->driver; >+ >+ subtest 'Setup' => sub { >+ plan tests => 10; >+ >+ my $mainpage = $s->base_url . q|mainpage.pl|; >+ $driver->get($mainpage); >+ like( $driver->get_title, qr(Log in to Koha), 'Hitting the main page should redirect to the login form'); >+ >+ fill_login_form($s); >+ like( $driver->get_title, qr(Koha staff interface), 'Patron with flags superlibrarian should be able to login' ); >+ >+ C4::Context->set_preference('TwoFactorAuthentication', 0); >+ $driver->get($s->base_url . q|members/two_factor_auth.pl|); >+ like( $driver->get_title, qr(Error 404), 'Must be redirected to 404 is the pref is off' ); >+ >+ C4::Context->set_preference('TwoFactorAuthentication', 1); >+ $driver->get($s->base_url . q|members/two_factor_auth.pl|); >+ like( $driver->get_title, qr(Two-Factor Authentication), 'Must be on the page with the pref on' ); >+ >+ is( $driver->find_element('//div[@class="two-factor-status"]')->get_text(), 'Status: Disabled', '2FA is disabled' ); >+ >+ $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click; >+ ok($driver->find_element('//img[@id="qr_code"]'), 'There is a QR code'); >+ >+ $s->fill_form({pin_code => 'wrong_code'}); >+ $s->submit_form; >+ ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid pin code")]')); >+ is( $patron->get_from_storage->secret, undef, 'secret is not set in DB yet' ); >+ >+ my $secret32 = $driver->find_element('//form[@id="two-factor-auth"]//input[@name="secret32"]')->get_value(); >+ my $auth = Koha::Auth::TwoFactorAuth::get_auth({patron => $patron, secret32 => $secret32}); >+ my $code = $auth->code(); >+ $s->fill_form({pin_code => $code}); >+ $s->submit_form; >+ is( $driver->find_element('//div[@class="two-factor-status"]')->get_text(), 'Status: Enabled', '2FA is enabled' ); >+ $patron = $patron->get_from_storage; >+ is( $patron->secret, $secret32, 'secret is set in DB' ); >+ >+ }; >+ >+ subtest 'Login' => sub { >+ plan tests => 19; >+ >+ my $mainpage = $s->base_url . q|mainpage.pl|; >+ >+ { # ok first try >+ $driver->get($mainpage . q|?logout.x=1|); >+ $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber); >+ like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' ); >+ $driver->capture_screenshot('selenium_failure_2.png'); >+ fill_login_form($s); >+ like( $driver->get_title, qr(Two-Factor Authentication), 'Must be on the second auth screen' ); >+ is( login_error($s), undef ); >+ >+ my $auth = Koha::Auth::TwoFactorAuth::get_auth({patron => $patron}); >+ my $code = $auth->code(); >+ $auth->clear; >+ $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code); >+ $driver->find_element('//input[@type="submit"]')->click; >+ like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' ); >+ } >+ >+ { # second try and logout >+ $driver->get($mainpage . q|?logout.x=1|); >+ $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber); >+ like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' ); >+ fill_login_form($s); >+ like( $driver->get_title, qr(Two-Factor Authentication), 'Must be on the second auth screen' ); >+ is( login_error($s), undef ); >+ $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code'); >+ $driver->find_element('//input[@type="submit"]')->click; >+ ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid two-factor code")]')); >+ is( login_error($s), undef ); >+ >+ $driver->get($mainpage); >+ like( $driver->get_title, qr(Two-Factor Authentication), 'Must still be on the second auth screen' ); >+ is( login_error($s), undef ); >+ $driver->find_element('//a[@id="logout"]')->click(); >+ like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' ); >+ is( login_error($s), undef ); >+ } >+ >+ { # second try and success >+ >+ $driver->get($mainpage . q|?logout.x=1|); >+ $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber); >+ like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' ); >+ like( login_error($s), qr(Session timed out) ); >+ fill_login_form($s); >+ like( $driver->get_title, qr(Two-Factor Authentication), 'Must be on the second auth screen' ); >+ is( login_error($s), undef ); >+ $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code'); >+ $driver->find_element('//input[@type="submit"]')->click; >+ ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid two-factor code")]')); >+ >+ my $auth = Koha::Auth::TwoFactorAuth::get_auth({patron => $patron}); >+ my $code = $auth->code(); >+ $auth->clear; >+ $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code); >+ $driver->find_element('//input[@type="submit"]')->click; >+ like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' ); >+ } >+ }; >+ >+ $driver->quit(); >+}; >+ >+END { >+ $_->delete for @data_to_cleanup; >+ C4::Context->set_preference('TwoFactorAuthentication', $pref_value); >+}; >+ >+ >+sub login_error { >+ my ( $s ) = @_; >+ my $driver = $s->driver; >+ >+ $s->remove_error_handler; >+ my $login_error = eval { >+ my $elt = $driver->find_element('//div[@id="login_error"]'); >+ return $elt->get_text if $elt && $elt->id; >+ }; >+ $s->add_error_handler; >+ return $login_error; >+} >+ >+# Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect >+sub fill_login_form { >+ my ( $s ) = @_; >+ $s->fill_form({ userid => $s->login, password => $s->password }); >+ $s->driver->find_element('//input[@id="submit-button"]')->click; >+} >diff --git a/t/lib/TestBuilder.pm b/t/lib/TestBuilder.pm >index e8b0dbf1269..c48ba4cf4c0 100644 >--- a/t/lib/TestBuilder.pm >+++ b/t/lib/TestBuilder.pm >@@ -553,6 +553,7 @@ sub _gen_default_values { > lost => undef, > debarred => undef, > borrowernotes => '', >+ secret => undef, > }, > Item => { > notforloan => 0, >-- >2.20.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 28786
:
123375
|
123376
|
123377
|
123378
|
123379
|
123380
|
123381
|
123397
|
123398
|
123399
|
123400
|
123401
|
123402
|
123403
|
124569
|
124770
|
124771
|
124844
|
124845
|
124846
|
124847
|
128841
|
128842
|
129383
|
129384
|
129474
|
129521
|
129557
|
129558
|
129559
|
129560
|
129561
|
129562
|
129563
|
129564
|
129565
|
129566
|
129567
|
129568
|
129569
|
129654
|
129655
|
129656
|
129657
|
129658
|
129659
|
129660
|
129661
|
129662
|
129665
|
131940
|
131941
|
131942
|
131943
|
131944
|
131945
|
131946
|
131947
|
131948
|
131949
|
131950
|
131951
|
131952
|
131953
|
131954
|
131955
|
131956
|
131957
|
131958
|
132142
|
132143
|
132144
|
132145
|
132146
|
132147
|
132148
|
132149
|
132150
|
132153
|
132154
|
132155
|
132156
|
132157
|
133222
|
133516
|
133522
|
133531
|
133533
|
133541
|
133548
|
133643
|
133686