Bug 27634: Fix self_registration.t
[koha.git] / t / db_dependent / selenium / authentication_2fa.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19 use utf8;
20 use Test::More tests => 5;
21
22 use C4::Context;
23 use Koha::AuthUtils;
24 use Koha::Auth::TwoFactorAuth;
25 use t::lib::Mocks;
26 use t::lib::Selenium;
27 use t::lib::TestBuilder;
28
29 my @data_to_cleanup;
30 my $pref_value = C4::Context->preference('TwoFactorAuthentication');
31
32 SKIP: {
33     eval { require Selenium::Remote::Driver; };
34     skip "Selenium::Remote::Driver is needed for selenium tests.", 2 if $@;
35
36     my $builder  = t::lib::TestBuilder->new;
37
38     my $library_name = 'my ❤ library';
39     my $library = $builder->build_object( { class => 'Koha::Libraries', value => { branchname => $library_name } } );
40     my $patron  = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 1, branchcode => $library->branchcode } } );
41     $patron->flags(1)->store; # superlibrarian permission
42     my $password = Koha::AuthUtils::generate_password($patron->category);
43     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
44     $patron->set_password({ password => $password });
45
46     push @data_to_cleanup, $patron, $patron->category, $patron->library;
47
48     my $s        = t::lib::Selenium->new({ login => $patron->userid, password => $password });
49     my $driver   = $s->driver;
50
51     subtest 'Setup' => sub {
52         plan tests => 13;
53
54         my $mainpage = $s->base_url . q|mainpage.pl|;
55         $driver->get($mainpage);
56         like( $driver->get_title, qr(Log in to Koha), 'Hitting the main page should redirect to the login form');
57
58         C4::Context->set_preference('TwoFactorAuthentication', 'disabled');
59
60         fill_login_form($s);
61         like( $driver->get_title, qr(Koha staff interface), 'Patron with flags superlibrarian should be able to login' );
62
63         $driver->get($s->base_url . q|members/two_factor_auth.pl|);
64         like( $driver->get_title, qr(Error 404), 'Must be redirected to 404 is the pref is off' );
65
66         C4::Context->set_preference('TwoFactorAuthentication', 'enabled');
67         $driver->get($s->base_url . q|members/two_factor_auth.pl|);
68         like( $driver->get_title, qr(Two-factor authentication), 'Must be on the page with the pref on' );
69
70         is(
71             $driver->find_element( '//div[@id="registration-status-disabled"]/div[@class="two-factor-status"]' )->get_text,
72             'Status: Disabled',
73             '2FA is disabled'
74         );
75
76         is(
77             $driver->find_element( '//div[@id="registration-status-enabled"]/div[@class="two-factor-status"]' )->get_text,
78             '', # 'Status: Enabled' is not shown
79             '2FA is disabled'
80         );
81
82         $driver->find_element('//*[@id="enable-2FA"]')->click;
83         $s->wait_for_ajax;
84         ok($driver->find_element('//img[@id="qr_code"]'), 'There is a QR code');
85         is($driver->find_element('//span[@id="issuer"]')->get_text, $library_name);
86
87         $driver->find_element('//*[@id="pin_code"]')->send_keys('wrong_code');
88         $driver->find_element('//*[@id="register-2FA"]')->click;
89         $s->wait_for_ajax;
90         ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid PIN code")]'));
91         is( $patron->get_from_storage->secret, undef, 'secret is not set in DB yet' );
92
93         my $secret32 = $driver->find_element('//*[@id="secret32"]')->get_value();
94         my $auth = Koha::Auth::TwoFactorAuth->new({patron => $patron, secret32 => $secret32});
95         my $code = $auth->code();
96         $driver->find_element('//*[@id="pin_code"]')->clear;
97         $driver->find_element('//*[@id="pin_code"]')->send_keys($code);
98         $driver->find_element('//*[@id="register-2FA"]')->click;
99         # Wait for the response then go to the page, don't wait for the redirect
100         $s->wait_for_ajax;
101         $driver->get($s->base_url . q|members/two_factor_auth.pl|);
102         is(
103             $driver->find_element( '//div[@id="registration-status-disabled"]/div[@class="two-factor-status"]' )->get_text,
104             '', # 'Status: Disabled' is not shown
105             '2FA is enabled'
106         );
107
108         is(
109             $driver->find_element( '//div[@id="registration-status-enabled"]/div[@class="two-factor-status"]' )->get_text,
110             'Status: Enabled',
111             '2FA is enabled'
112         );
113
114         $patron = $patron->get_from_storage;
115         is( $patron->decoded_secret, $secret32, 'encrypted secret is set in DB' );
116
117     };
118
119     subtest 'Login' => sub {
120         plan tests => 18;
121
122         my $mainpage = $s->base_url . q|mainpage.pl|;
123
124         my $secret32 = $patron->decoded_secret;
125         { # ok first try
126             $driver->get($mainpage . q|?logout.x=1|);
127             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
128             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
129             fill_login_form($s);
130             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
131             is( login_error($s), undef );
132
133             my $auth = Koha::Auth::TwoFactorAuth->new(
134                 { patron => $patron, secret32 => $secret32 } );
135             my $code = $auth->code();
136             $auth->clear;
137             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
138             $driver->find_element('//input[@type="submit"]')->click;
139             like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
140         }
141
142         { # second try and logout
143             $driver->get($mainpage . q|?logout.x=1|);
144             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
145             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
146             fill_login_form($s);
147             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
148             is( login_error($s), undef );
149             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
150             $driver->find_element('//input[@type="submit"]')->click;
151             is( login_error($s), "Invalid two-factor code" );
152
153             $driver->get($mainpage);
154             like( $driver->get_title, qr(Two-factor authentication), 'Must still be on the second auth screen' );
155             is( login_error($s), undef );
156             $driver->find_element('//a[@id="logout"]')->click();
157             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
158             is( login_error($s), undef );
159         }
160
161         { # second try and success
162
163             $driver->get($mainpage . q|?logout.x=1|);
164             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
165             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
166             like( login_error($s), qr(Session timed out) );
167             fill_login_form($s);
168             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
169             is( login_error($s), undef );
170             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
171             $driver->find_element('//input[@type="submit"]')->click;
172             is( login_error($s), "Invalid two-factor code" );
173
174             my $auth = Koha::Auth::TwoFactorAuth->new(
175                 { patron => $patron, secret32 => $secret32 } );
176             my $code = $auth->code();
177             $auth->clear;
178             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
179             $driver->find_element('//input[@type="submit"]')->click;
180             like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
181         }
182     };
183
184     subtest "Send OTP code" => sub {
185         plan tests => 3;
186
187         # Make sure the send won't fail because of invalid email addresses
188         $patron->library->set(
189             {
190                 branchemail      => 'from@example.org',
191                 branchreturnpath => undef,
192                 branchreplyto    => undef,
193             }
194         )->store;
195         $patron->auth_method('two-factor');
196         $patron->email(undef);
197         $patron->store;
198
199         my $mainpage = $s->base_url . q|mainpage.pl|;
200         $driver->get( $mainpage . q|?logout.x=1| );
201         like(
202             $driver->get_title,
203             qr(Log in to Koha),
204             'Must be on the first auth screen'
205         );
206         fill_login_form($s);
207         like(
208             $driver->get_title,
209             qr(Two-factor authentication),
210             'Must be on the second auth screen'
211         );
212         $driver->find_element('//a[@id="send_otp"]')->click;
213         $s->wait_for_ajax;
214         my $error = $driver->find_element('//div[@id="email_error"]')->get_text;
215         like(
216             $error,
217             qr{Email not sent},
218             'Email not sent will display an error'
219         );
220
221         # This test will only pass if an SMTP server is defined
222         # It cannot be mocked from selenium tests
223         #$patron->email('test@example.org');
224         #$patron->store;
225         #$driver->find_element('//a[@id="send_otp"]')->click;
226         #$s->wait_for_ajax;
227         #my $message =
228         #  $driver->find_element('//div[@id="email_success"]')->get_text;
229         #is(
230         #    $message,
231         #    "The code has been sent by email, please check your inbox.",
232         #    'The email must have been sent correctly'
233         #);
234     };
235
236     subtest "Enforce 2FA setup on first login" => sub {
237         plan tests => 7;
238
239         C4::Context->set_preference( 'TwoFactorAuthentication', 'enforced' );
240
241         # Make sure the send won't fail because of invalid email addresses
242         $patron->library->set(
243             {
244                 branchemail      => 'from@example.org',
245                 branchreturnpath => undef,
246                 branchreplyto    => undef,
247             }
248         )->store;
249         $patron->auth_method('password');
250         $patron->email(undef);
251         $patron->store;
252
253         my $mainpage = $s->base_url . q|mainpage.pl|;
254         $driver->get( $mainpage . q|?logout.x=1| );
255         like(
256             $driver->get_title,
257             qr(Log in to Koha),
258             'Must be on the first auth screen'
259         );
260         fill_login_form($s);
261         like(
262             $driver->get_title,
263             qr(Two-factor authentication setup),
264             'Must be on the 2FA auth setup screen'
265         );
266
267         $s->wait_for_ajax; # There is an ajax request to populate the qr_code and the secret
268
269         isnt( $driver->find_element('//*[@id="qr_code"]')->get_attribute("src"), "" );
270         my $secret32 = $driver->find_element('//*[@id="secret32"]')->get_value;
271
272         my $auth = Koha::Auth::TwoFactorAuth->new(
273             { patron => $patron, secret32 => $secret32 } );
274         my $pin_code = $auth->code;
275
276         $driver->find_element('//*[@id="pin_code"]')->send_keys("wrong code");
277         $driver->find_element('//*[@id="register-2FA"]')->click;
278         $s->wait_for_ajax;
279         is( $driver->find_element('//*[@id="errors"]')->get_text,
280             "Invalid PIN code" );
281
282         $driver->find_element('//*[@id="pin_code"]')->clear;
283         $driver->find_element('//*[@id="pin_code"]')->send_keys($pin_code);
284         $driver->find_element('//*[@id="register-2FA"]')->click;
285         is( $s->get_next_alert_text,
286             "Two-factor authentication correctly configured. You will be redirected to the login screen."
287         );
288         $driver->accept_alert;
289         # FIXME How to test the redirect to the mainpage here
290
291         $patron = $patron->get_from_storage;
292         is( $patron->auth_method, 'two-factor', );
293         isnt( $patron->secret, undef, );
294     };
295
296     subtest "Disable" => sub {
297         plan tests => 6;
298
299         my $mainpage = $s->base_url . q|mainpage.pl|;
300         $driver->get( $mainpage . q|?logout.x=1| );
301         fill_login_form($s);
302         my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
303         my $code = $auth->code();
304         $auth->clear;
305         $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')
306           ->send_keys($code);
307         $driver->find_element('//input[@type="submit"]')->click;
308
309         $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
310
311         is(
312             $driver->find_element( '//div[@id="registration-status-disabled"]/div[@class="two-factor-status"]' )->get_text,
313             '', # 'Status: Disabled' is not shown
314             '2FA is enabled'
315         );
316
317         is(
318             $driver->find_element( '//div[@id="registration-status-enabled"]/div[@class="two-factor-status"]' )->get_text,
319             'Status: Enabled',
320             '2FA is enabled'
321         );
322
323         $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click;
324
325         is(
326             $driver->find_element( '//div[@id="registration-status-disabled"]/div[@class="two-factor-status"]' )->get_text,
327             'Status: Disabled',
328             '2FA is disabled'
329         );
330
331         is(
332             $driver->find_element( '//div[@id="registration-status-enabled"]/div[@class="two-factor-status"]' )->get_text,
333             '', # 'Status: Enabled' is not shown
334             '2FA is disabled'
335         );
336
337         $patron = $patron->get_from_storage;
338         is( $patron->secret, undef, "Secret has been cleared" );
339         is( $patron->auth_method(), 'password', 'auth_method has been reset to "password"' );
340     };
341
342     $driver->quit();
343 };
344
345 END {
346     $_->delete for @data_to_cleanup;
347     C4::Context->set_preference('TwoFactorAuthentication', $pref_value);
348 };
349
350
351 sub login_error {
352     my ( $s ) = @_;
353     my $driver   = $s->driver;
354
355     $s->remove_error_handler;
356     my $login_error = eval {
357         my $elt = $driver->find_element('//div[@id="login_error"]');
358         return $elt->get_text if $elt && $elt->id;
359     };
360     $s->add_error_handler;
361     return $login_error;
362 }
363
364 # Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect
365 sub fill_login_form {
366     my ( $s ) = @_;
367     $s->fill_form({ userid => $s->login, password => $s->password });
368     $s->driver->find_element('//input[@id="submit-button"]')->click;
369 }