3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19 use Test::More tests => 3;
23 use Koha::Auth::TwoFactorAuth;
26 use t::lib::TestBuilder;
29 my $pref_value = C4::Context->preference('TwoFactorAuthentication');
32 eval { require Selenium::Remote::Driver; };
33 skip "Selenium::Remote::Driver is needed for selenium tests.", 2 if $@;
35 my $builder = t::lib::TestBuilder->new;
37 my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 }});
38 $patron->flags(1)->store; # superlibrarian permission
39 my $password = Koha::AuthUtils::generate_password($patron->category);
40 t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
41 $patron->set_password({ password => $password });
43 push @data_to_cleanup, $patron, $patron->category, $patron->library;
45 my $s = t::lib::Selenium->new({ login => $patron->userid, password => $password });
46 my $driver = $s->driver;
48 subtest 'Setup' => sub {
51 my $mainpage = $s->base_url . q|mainpage.pl|;
52 $driver->get($mainpage);
53 like( $driver->get_title, qr(Log in to Koha), 'Hitting the main page should redirect to the login form');
56 like( $driver->get_title, qr(Koha staff interface), 'Patron with flags superlibrarian should be able to login' );
58 C4::Context->set_preference('TwoFactorAuthentication', 0);
59 $driver->get($s->base_url . q|members/two_factor_auth.pl|);
60 like( $driver->get_title, qr(Error 404), 'Must be redirected to 404 is the pref is off' );
62 C4::Context->set_preference('TwoFactorAuthentication', 1);
63 $driver->get($s->base_url . q|members/two_factor_auth.pl|);
64 like( $driver->get_title, qr(Two-factor authentication), 'Must be on the page with the pref on' );
66 is( $driver->find_element('//div[@class="two-factor-status"]')->get_text(), 'Status: Disabled', '2FA is disabled' );
68 $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click;
69 ok($driver->find_element('//img[@id="qr_code"]'), 'There is a QR code');
71 $s->fill_form({pin_code => 'wrong_code'});
73 ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid pin code")]'));
74 is( $patron->get_from_storage->secret, undef, 'secret is not set in DB yet' );
76 my $secret32 = $driver->find_element('//form[@id="two-factor-auth"]//input[@name="secret32"]')->get_value();
77 my $auth = Koha::Auth::TwoFactorAuth->new({patron => $patron, secret32 => $secret32});
78 my $code = $auth->code();
79 $s->fill_form({pin_code => $code});
81 is( $driver->find_element('//div[@class="two-factor-status"]')->get_text(), 'Status: Enabled', '2FA is enabled' );
82 $patron = $patron->get_from_storage;
83 is( $patron->secret, $secret32, 'secret is set in DB' );
87 subtest 'Login' => sub {
90 my $mainpage = $s->base_url . q|mainpage.pl|;
93 $driver->get($mainpage . q|?logout.x=1|);
94 $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
95 like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
96 $driver->capture_screenshot('selenium_failure_2.png');
98 like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
99 is( login_error($s), undef );
101 my $auth = Koha::Auth::TwoFactorAuth->new({patron => $patron});
102 my $code = $auth->code();
104 $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
105 $driver->find_element('//input[@type="submit"]')->click;
106 like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
109 { # second try and logout
110 $driver->get($mainpage . q|?logout.x=1|);
111 $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
112 like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
114 like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
115 is( login_error($s), undef );
116 $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
117 $driver->find_element('//input[@type="submit"]')->click;
118 is( login_error($s), "Invalid two-factor code" );
120 $driver->get($mainpage);
121 like( $driver->get_title, qr(Two-factor authentication), 'Must still be on the second auth screen' );
122 is( login_error($s), undef );
123 $driver->find_element('//a[@id="logout"]')->click();
124 like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
125 is( login_error($s), undef );
128 { # second try and success
130 $driver->get($mainpage . q|?logout.x=1|);
131 $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
132 like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
133 like( login_error($s), qr(Session timed out) );
135 like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
136 is( login_error($s), undef );
137 $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
138 $driver->find_element('//input[@type="submit"]')->click;
139 is( login_error($s), "Invalid two-factor code" );
141 my $auth = Koha::Auth::TwoFactorAuth->new({patron => $patron});
142 my $code = $auth->code();
144 $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
145 $driver->find_element('//input[@type="submit"]')->click;
146 like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
150 subtest "Disable" => sub {
153 my $mainpage = $s->base_url . q|mainpage.pl|;
154 $driver->get( $mainpage . q|?logout.x=1| );
156 my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
157 my $code = $auth->code();
159 $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')
161 $driver->find_element('//input[@type="submit"]')->click;
163 $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
166 $driver->find_element('//div[@class="two-factor-status"]')->get_text(),
171 $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click;
174 $driver->find_element('//div[@class="two-factor-status"]')->get_text(),
176 '2FA has been disabled'
179 $patron = $patron->get_from_storage;
180 is( $patron->secret, undef, "Secret has been cleared" );
181 is( $patron->auth_method(), 'password', 'auth_method has been reset to "password"' );
188 $_->delete for @data_to_cleanup;
189 C4::Context->set_preference('TwoFactorAuthentication', $pref_value);
195 my $driver = $s->driver;
197 $s->remove_error_handler;
198 my $login_error = eval {
199 my $elt = $driver->find_element('//div[@id="login_error"]');
200 return $elt->get_text if $elt && $elt->id;
202 $s->add_error_handler;
206 # Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect
207 sub fill_login_form {
209 $s->fill_form({ userid => $s->login, password => $s->password });
210 $s->driver->find_element('//input[@id="submit-button"]')->click;