Bug 30588: Adjust existing occurrences of TwoFactorAuthentication
[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 Test::More tests => 4;
20
21 use C4::Context;
22 use Koha::AuthUtils;
23 use Koha::Auth::TwoFactorAuth;
24 use t::lib::Mocks;
25 use t::lib::Selenium;
26 use t::lib::TestBuilder;
27
28 my @data_to_cleanup;
29 my $pref_value = C4::Context->preference('TwoFactorAuthentication');
30
31 SKIP: {
32     eval { require Selenium::Remote::Driver; };
33     skip "Selenium::Remote::Driver is needed for selenium tests.", 2 if $@;
34
35     my $builder  = t::lib::TestBuilder->new;
36
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 });
42
43     push @data_to_cleanup, $patron, $patron->category, $patron->library;
44
45     my $s        = t::lib::Selenium->new({ login => $patron->userid, password => $password });
46     my $driver   = $s->driver;
47
48     subtest 'Setup' => sub {
49         plan tests => 10;
50
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');
54
55         fill_login_form($s);
56         like( $driver->get_title, qr(Koha staff interface), 'Patron with flags superlibrarian should be able to login' );
57
58         C4::Context->set_preference('TwoFactorAuthentication', 'disabled');
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' );
61
62         C4::Context->set_preference('TwoFactorAuthentication', 'enabled');
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' );
65
66         is( $driver->find_element('//div[@class="two-factor-status"]')->get_text(), 'Status: Disabled', '2FA is disabled' );
67
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');
70
71         $s->fill_form({pin_code => 'wrong_code'});
72         $s->submit_form;
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' );
75
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});
80         $s->submit_form;
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->decoded_secret, $secret32, 'encrypted secret is set in DB' );
84
85     };
86
87     subtest 'Login' => sub {
88         plan tests => 18;
89
90         my $mainpage = $s->base_url . q|mainpage.pl|;
91
92         my $secret32 = $patron->decoded_secret;
93         { # ok first try
94             $driver->get($mainpage . q|?logout.x=1|);
95             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
96             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
97             fill_login_form($s);
98             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
99             is( login_error($s), undef );
100
101             my $auth = Koha::Auth::TwoFactorAuth->new(
102                 { patron => $patron, secret32 => $secret32 } );
103             my $code = $auth->code();
104             $auth->clear;
105             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
106             $driver->find_element('//input[@type="submit"]')->click;
107             like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
108         }
109
110         { # second try and logout
111             $driver->get($mainpage . q|?logout.x=1|);
112             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
113             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
114             fill_login_form($s);
115             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
116             is( login_error($s), undef );
117             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
118             $driver->find_element('//input[@type="submit"]')->click;
119             is( login_error($s), "Invalid two-factor code" );
120
121             $driver->get($mainpage);
122             like( $driver->get_title, qr(Two-factor authentication), 'Must still be on the second auth screen' );
123             is( login_error($s), undef );
124             $driver->find_element('//a[@id="logout"]')->click();
125             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
126             is( login_error($s), undef );
127         }
128
129         { # second try and success
130
131             $driver->get($mainpage . q|?logout.x=1|);
132             $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
133             like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
134             like( login_error($s), qr(Session timed out) );
135             fill_login_form($s);
136             like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
137             is( login_error($s), undef );
138             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
139             $driver->find_element('//input[@type="submit"]')->click;
140             is( login_error($s), "Invalid two-factor code" );
141
142             my $auth = Koha::Auth::TwoFactorAuth->new(
143                 { patron => $patron, secret32 => $secret32 } );
144             my $code = $auth->code();
145             $auth->clear;
146             $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
147             $driver->find_element('//input[@type="submit"]')->click;
148             like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
149         }
150     };
151
152     subtest "Send OTP code" => sub {
153         plan tests => 3;
154
155         # Make sure the send won't fail because of invalid email addresses
156         $patron->library->set(
157             {
158                 branchemail      => 'from@example.org',
159                 branchreturnpath => undef,
160                 branchreplyto    => undef,
161             }
162         )->store;
163         $patron->auth_method('two-factor');
164         $patron->email(undef);
165         $patron->store;
166
167         my $mainpage = $s->base_url . q|mainpage.pl|;
168         $driver->get( $mainpage . q|?logout.x=1| );
169         like(
170             $driver->get_title,
171             qr(Log in to Koha),
172             'Must be on the first auth screen'
173         );
174         fill_login_form($s);
175         like(
176             $driver->get_title,
177             qr(Two-factor authentication),
178             'Must be on the second auth screen'
179         );
180         $driver->find_element('//a[@id="send_otp"]')->click;
181         $s->wait_for_ajax;
182         my $error = $driver->find_element('//div[@id="email_error"]')->get_text;
183         like(
184             $error,
185             qr{Email not sent},
186             'Email not sent will display an error'
187         );
188
189         # This test will only pass if an SMTP server is defined
190         # It cannot be mocked from selenium tests
191         #$patron->email('test@example.org');
192         #$patron->store;
193         #$driver->find_element('//a[@id="send_otp"]')->click;
194         #$s->wait_for_ajax;
195         #my $message =
196         #  $driver->find_element('//div[@id="email_success"]')->get_text;
197         #is(
198         #    $message,
199         #    "The code has been sent by email, please check your inbox.",
200         #    'The email must have been sent correctly'
201         #);
202     };
203
204     subtest "Disable" => sub {
205         plan tests => 4;
206
207         my $mainpage = $s->base_url . q|mainpage.pl|;
208         $driver->get( $mainpage . q|?logout.x=1| );
209         fill_login_form($s);
210         my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
211         my $code = $auth->code();
212         $auth->clear;
213         $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')
214           ->send_keys($code);
215         $driver->find_element('//input[@type="submit"]')->click;
216
217         $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
218
219         is(
220             $driver->find_element('//div[@class="two-factor-status"]')->get_text(),
221             'Status: Enabled',
222             '2FA is enabled'
223         );
224
225         $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click;
226
227         is(
228             $driver->find_element('//div[@class="two-factor-status"]')->get_text(),
229             'Status: Disabled',
230             '2FA has been disabled'
231         );
232
233         $patron = $patron->get_from_storage;
234         is( $patron->secret, undef, "Secret has been cleared" );
235         is( $patron->auth_method(), 'password', 'auth_method has been reset to "password"' );
236     };
237
238     $driver->quit();
239 };
240
241 END {
242     $_->delete for @data_to_cleanup;
243     C4::Context->set_preference('TwoFactorAuthentication', $pref_value);
244 };
245
246
247 sub login_error {
248     my ( $s ) = @_;
249     my $driver   = $s->driver;
250
251     $s->remove_error_handler;
252     my $login_error = eval {
253         my $elt = $driver->find_element('//div[@id="login_error"]');
254         return $elt->get_text if $elt && $elt->id;
255     };
256     $s->add_error_handler;
257     return $login_error;
258 }
259
260 # Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect
261 sub fill_login_form {
262     my ( $s ) = @_;
263     $s->fill_form({ userid => $s->login, password => $s->password });
264     $s->driver->find_element('//input[@id="submit-button"]')->click;
265 }