View | Details | Raw Unified | Return to bug 28786
Collapse All | Expand All

(-)a/C4/Auth.pm (-32 / +93 lines)
Lines 35-40 use Koha; Link Here
35
use Koha::Logger;
35
use Koha::Logger;
36
use Koha::Caches;
36
use Koha::Caches;
37
use Koha::AuthUtils qw( get_script_name hash_password );
37
use Koha::AuthUtils qw( get_script_name hash_password );
38
use Koha::Auth::TwoFactorAuth;
38
use Koha::Checkouts;
39
use Koha::Checkouts;
39
use Koha::DateUtils qw( dt_from_string );
40
use Koha::DateUtils qw( dt_from_string );
40
use Koha::Library::Groups;
41
use Koha::Library::Groups;
Lines 856-861 sub checkauth { Link Here
856
    my $q_userid = $query->param('userid') // '';
857
    my $q_userid = $query->param('userid') // '';
857
858
858
    my $session;
859
    my $session;
860
    my $invalid_otp_token;
861
    my $require_2FA = ( C4::Context->preference('TwoFactorAuthentication') && $type ne "OPAC" ) ? 1 : 0;
862
    my $auth_challenge_complete;
859
863
860
    # Basic authentication is incompatible with the use of Shibboleth,
864
    # Basic authentication is incompatible with the use of Shibboleth,
861
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
865
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
Lines 888-900 sub checkauth { Link Here
888
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
892
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
889
        );
893
        );
890
894
895
        if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
896
            $userid = $session->param('id');
897
        }
898
899
        $additional_auth_needed = ( $return eq 'additional-auth-needed' ) ? 1 : 0;
900
901
        # We are at the second screen if the waiting-for-2FA is set in session
902
        # and otp_token param has been passed
903
        if (   $require_2FA
904
            && $additional_auth_needed
905
            && ( my $otp_token = $query->param('otp_token') ) )
906
        {
907
            my $patron    = Koha::Patrons->find( { userid => $userid } );
908
            my $auth      = Koha::Auth::TwoFactorAuth::get_auth( { patron => $patron } );
909
            my $verified = $auth->verify($otp_token);
910
            $auth->clear;
911
            if ( $verified ) {
912
                # The token is correct, the user is fully logged in!
913
                $additional_auth_needed = 0;
914
                $session->param( 'waiting-for-2FA', 0 );
915
                $return = "ok";
916
                $auth_challenge_complete = 1;
917
918
               # This is an ugly trick to pass the test
919
               # $query->param('koha_login_context') && ( $q_userid ne $userid )
920
               # few lines later
921
                $q_userid = $userid;
922
            }
923
            else {
924
                $invalid_otp_token = 1;
925
            }
926
        }
927
891
        if ( $return eq 'ok' ) {
928
        if ( $return eq 'ok' ) {
892
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
929
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
893
930
894
            my $s_userid = $session->param('id');
931
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
895
            $userid      = $s_userid;
896
897
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
898
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
932
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
899
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
933
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
900
            ) {
934
            ) {
Lines 904-931 sub checkauth { Link Here
904
                $anon_search_history = $session->param('search_history');
938
                $anon_search_history = $session->param('search_history');
905
                $session->delete();
939
                $session->delete();
906
                $session->flush;
940
                $session->flush;
907
                C4::Context::_unset_userenv($sessionID);
941
                C4::Context->_unset_userenv($sessionID);
908
                $sessionID = undef;
909
            }
910
            elsif ($logout) {
911
912
                # voluntary logout the user
913
                # check wether the user was using their shibboleth session or a local one
914
                my $shibSuccess = C4::Context->userenv->{'shibboleth'};
915
                $session->delete();
916
                $session->flush;
917
                C4::Context::_unset_userenv($sessionID);
918
                $sessionID = undef;
942
                $sessionID = undef;
919
943
            } elsif (!$logout) {
920
                if ($cas and $caslogout) {
921
                    logout_cas($query, $type);
922
                }
923
924
                # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
925
                if ( $shib and $shib_login and $shibSuccess) {
926
                    logout_shib($query);
927
                }
928
            } else {
929
944
930
                $cookie = $query->cookie(
945
                $cookie = $query->cookie(
931
                    -name     => 'CGISESSID',
946
                    -name     => 'CGISESSID',
Lines 952-964 sub checkauth { Link Here
952
        }
967
        }
953
    }
968
    }
954
969
955
    unless ( $loggedin ) {
970
    if ( ( !$loggedin && !$additional_auth_needed ) || $logout ) {
971
        $sessionID = undef;
956
        $userid    = undef;
972
        $userid    = undef;
957
    }
973
    }
958
974
975
    if ($logout) {
976
977
        # voluntary logout the user
978
        # check wether the user was using their shibboleth session or a local one
979
        my $shibSuccess = C4::Context->userenv->{'shibboleth'};
980
        if ( $session ) {
981
            $session->delete();
982
            $session->flush;
983
        }
984
        C4::Context->_unset_userenv($sessionID);
985
986
        if ($cas and $caslogout) {
987
            logout_cas($query, $type);
988
        }
989
990
        # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
991
        if ( $shib and $shib_login and $shibSuccess) {
992
            logout_shib($query);
993
        }
994
995
        $session   = undef;
996
        $additional_auth_needed = 0;
997
    }
998
959
    unless ( $userid ) {
999
    unless ( $userid ) {
960
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
1000
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
961
        $session ||= get_session("") or die "Auth ERROR: Cannot get_session()";
1001
        $session = get_session("") or die "Auth ERROR: Cannot get_session()";
962
1002
963
        # Save anonymous search history in new session so it can be retrieved
1003
        # Save anonymous search history in new session so it can be retrieved
964
        # by get_template_and_user to store it in user's search history after
1004
        # by get_template_and_user to store it in user's search history after
Lines 1253-1261 sub checkauth { Link Here
1253
        $session->flush;
1293
        $session->flush;
1254
    }    # END unless ($userid)
1294
    }    # END unless ($userid)
1255
1295
1296
    if ( $require_2FA && ( $loggedin && !$auth_challenge_complete)) {
1297
        my $patron = Koha::Patrons->find({userid => $userid});
1298
        if ( $patron->auth_method eq 'two-factor' ) {
1299
            # Ask for the OTP token
1300
            $additional_auth_needed = 1;
1301
            $session->param('waiting-for-2FA', 1);
1302
            %info = ();# We remove the warnings/errors we may have set incorrectly before
1303
        }
1304
    }
1305
1256
    # finished authentification, now respond
1306
    # finished authentification, now respond
1257
    if ( $loggedin || $authnotrequired )
1307
    if ( ( $loggedin || $authnotrequired ) && !$additional_auth_needed ) {
1258
    {
1259
        # successful login
1308
        # successful login
1260
        unless ($cookie) {
1309
        unless ($cookie) {
1261
            $cookie = $query->cookie(
1310
            $cookie = $query->cookie(
Lines 1289-1304 sub checkauth { Link Here
1289
    #
1338
    #
1290
    #
1339
    #
1291
1340
1341
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1342
1292
    # get the inputs from the incoming query
1343
    # get the inputs from the incoming query
1293
    my @inputs = ();
1344
    my @inputs = ();
1345
    my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1294
    foreach my $name ( param $query) {
1346
    foreach my $name ( param $query) {
1295
        (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
1347
        next if grep { $name eq $_ } @inputs_to_clean;
1296
        my @value = $query->multi_param($name);
1348
        my @value = $query->multi_param($name);
1297
        push @inputs, { name => $name, value => $_ } for @value;
1349
        push @inputs, { name => $name, value => $_ } for @value;
1298
    }
1350
    }
1299
1351
1300
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1301
1302
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1352
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1303
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1353
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1304
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1354
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
Lines 1346-1351 sub checkauth { Link Here
1346
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1396
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1347
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1397
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1348
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1398
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1399
    if ( $additional_auth_needed ) {
1400
        $template->param(
1401
            TwoFA_prompt => 1,
1402
            invalid_otp_token => $invalid_otp_token,
1403
        );
1404
    }
1349
1405
1350
    if ( $type eq 'opac' ) {
1406
    if ( $type eq 'opac' ) {
1351
        require Koha::Virtualshelves;
1407
        require Koha::Virtualshelves;
Lines 1454-1459 Possible return values in C<$status> are: Link Here
1454
1510
1455
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1511
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1456
1512
1513
=item "additional-auth-needed -- User is in an authentication process that is not finished
1514
1457
=back
1515
=back
1458
1516
1459
=cut
1517
=cut
Lines 1729-1734 sub check_cookie_auth { Link Here
1729
                    $session->param('desk_id'),      $session->param('desk_name'),
1787
                    $session->param('desk_id'),      $session->param('desk_name'),
1730
                    $session->param('register_id'),  $session->param('register_name')
1788
                    $session->param('register_id'),  $session->param('register_name')
1731
                );
1789
                );
1790
                return ( "additional-auth-needed", $session )
1791
                    if $session->param('waiting-for-2FA');
1792
1732
                return ( "ok", $session );
1793
                return ( "ok", $session );
1733
            } else {
1794
            } else {
1734
                $session->delete();
1795
                $session->delete();
(-)a/Koha/Auth/TwoFactorAuth.pm (+65 lines)
Line 0 Link Here
1
package Koha::Auth::TwoFactorAuth;
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 Auth::GoogleAuth;
20
21
use base qw( Auth::GoogleAuth );
22
23
=head1 NAME
24
25
Koha::Auth::TwoFactorAuth- Koha class deal with Two factor authentication
26
27
=head1 SYNOPSIS
28
29
use Koha::Auth::TwoFactorAuth;
30
31
my $secret = Koha::AuthUtils::generate_salt( 'weak', 16 );
32
my $auth = Koha::Auth::TwoFactorAuth->new(
33
    { patron => $patron, secret => $secret } );
34
my $secret32 = $auth->generate_secret32;
35
my $ok = $auth->verify($pin_code, 1, $secret32);
36
37
It's based on Auth::GoogleAuth
38
39
=cut
40
41
sub get_auth {
42
    my ($params) = @_;
43
    my $patron   = $params->{patron};
44
    my $secret   = $params->{secret};
45
    my $secret32 = $params->{secret32};
46
47
    if (!$secret && !$secret32){
48
        $secret32 = $patron->secret;
49
    }
50
51
    my $issuer = $patron->library->branchname;
52
    my $key_id = sprintf "%s_%s",
53
      $issuer, ( $patron->email || $patron->userid );
54
55
    return Auth::GoogleAuth->new(
56
        {
57
            ( $secret   ? ( secret   => $secret )   : () ),
58
            ( $secret32 ? ( secret32 => $secret32 ) : () ),
59
            issuer => $issuer,
60
            key_id => $key_id,
61
        }
62
    );
63
}
64
65
1;
(-)a/cpanfile (+1 lines)
Lines 1-6 Link Here
1
requires 'Algorithm::CheckDigits', '0.5';
1
requires 'Algorithm::CheckDigits', '0.5';
2
requires 'Array::Utils', '0.5';
2
requires 'Array::Utils', '0.5';
3
requires 'Authen::CAS::Client', '0.05';
3
requires 'Authen::CAS::Client', '0.05';
4
requires 'Auth::GoogleAuth', '1.02';
4
requires 'Biblio::EndnoteStyle', '0.05';
5
requires 'Biblio::EndnoteStyle', '0.05';
5
requires 'Business::ISBN', '2.05';
6
requires 'Business::ISBN', '2.05';
6
requires 'Business::ISSN', '0.91';
7
requires 'Business::ISSN', '0.91';
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc (-1 / +1 lines)
Lines 53-59 Link Here
53
                [% END %]
53
                [% END %]
54
54
55
                [% IF Koha.Preference('TwoFactorAuthentication') && logged_in_user.borrowernumber == patron.borrowernumber %]
55
                [% IF Koha.Preference('TwoFactorAuthentication') && logged_in_user.borrowernumber == patron.borrowernumber %]
56
                    <li><a id="twofa" href="/cgi-bin/koha/members/two_factor_auth.pl">Enable Two-Factor Authentication</a></li>
56
                    <li><a id="twofa" href="/cgi-bin/koha/members/two_factor_auth.pl">Manage two-factor authentication</a></li>
57
                [% END %]
57
                [% END %]
58
58
59
                [% IF CAN_user_borrowers_edit_borrowers && useDischarge %]
59
                [% IF CAN_user_borrowers_edit_borrowers && useDischarge %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt (-1 / +18 lines)
Lines 8-13 Link Here
8
[% SET footerjs = 1 %]
8
[% SET footerjs = 1 %]
9
[% INCLUDE 'doc-head-open.inc' %]
9
[% INCLUDE 'doc-head-open.inc' %]
10
<title>
10
<title>
11
    [% IF TwoFA_prompt %]Two-factor authentication[% END %]
11
    [% IF ( loginprompt ) %]Log in to Koha[% END %]
12
    [% IF ( loginprompt ) %]Log in to Koha[% END %]
12
    [% IF too_many_login_attempts %]This account has been locked.
13
    [% IF too_many_login_attempts %]This account has been locked.
13
    [% ELSIF invalid_username_or_password %]Invalid username or password[% END %]
14
    [% ELSIF invalid_username_or_password %]Invalid username or password[% END %]
Lines 61-67 Link Here
61
<p>If you have a shibboleth account, please <a href="[% shibbolethLoginUrl | $raw %]">click here</a> to login.</p>
62
<p>If you have a shibboleth account, please <a href="[% shibbolethLoginUrl | $raw %]">click here</a> to login.</p>
62
[% END %]
63
[% END %]
63
64
64
[% UNLESS Koha.Preference('staffShibOnly') %]
65
[% IF !TwoFA_prompt && !Koha.Preference('staffShibOnly') %]
65
    <!-- login prompt time-->
66
    <!-- login prompt time-->
66
    <form action="[% script_name | html %]" method="post" name="loginform" id="loginform">
67
    <form action="[% script_name | html %]" method="post" name="loginform" id="loginform">
67
        <input type="hidden" name="koha_login_context" value="intranet" />
68
        <input type="hidden" name="koha_login_context" value="intranet" />
Lines 135-140 Link Here
135
            [% END %]
136
            [% END %]
136
        [% END %]
137
        [% END %]
137
    [% END %]
138
    [% END %]
139
[% ELSIF TwoFA_prompt %]
140
    <form action="[% script_name | html %]" method="post" name="loginform" id="loginform">
141
        <input type="hidden" name="koha_login_context" value="intranet" />
142
        [% FOREACH INPUT IN INPUTS %]
143
            <input type="hidden" name="[% INPUT.name | html %]" value="[% INPUT.value | html %]" />
144
        [% END %]
145
        [% IF invalid_otp_token %]
146
            <div class="dialog error">Invalid two-factor code</div>
147
        [% END %]
148
149
        <p><label for="otp_token">Two-factor authentication code:</label>
150
        <input type="text" name="otp_token" id="otp_token" class="input focus" value="" size="20" tabindex="1" />
151
        <p class="submit"><input id="submit-button" type="submit" value="Verify code" /></p>
152
        <a class="logout" id="logout" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Log out</a>
153
154
    </form>
138
[% END %]
155
[% END %]
139
156
140
[% IF ( nopermission ) %]
157
[% IF ( nopermission ) %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/members/two_factor_auth.tt (-5 / +5 lines)
Lines 3-9 Link Here
3
[% USE Asset %]
3
[% USE Asset %]
4
[% SET footerjs = 1 %]
4
[% SET footerjs = 1 %]
5
[% INCLUDE 'doc-head-open.inc' %]
5
[% INCLUDE 'doc-head-open.inc' %]
6
<title>Two-Factor Authentication &rsaquo; Patrons &rsaquo; Koha</title>
6
<title>Two-factor authentication &rsaquo; Patrons &rsaquo; Koha</title>
7
[% INCLUDE 'doc-head-close.inc' %]
7
[% INCLUDE 'doc-head-close.inc' %]
8
</head>
8
</head>
9
<body id="pat_two_factor_auth" class="pat">
9
<body id="pat_two_factor_auth" class="pat">
Lines 34-40 Link Here
34
                [% INCLUDE 'members-toolbar.inc' %]
34
                [% INCLUDE 'members-toolbar.inc' %]
35
35
36
                [% IF op == 'register' %]
36
                [% IF op == 'register' %]
37
                    <h1>Register Two-Factor Authenticator</h1>
37
                    <h1>Register two-factor authenticator</h1>
38
                    <div class="dialog message">
38
                    <div class="dialog message">
39
                        <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>
39
                        <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>
40
                        <p>Can't scan the code?</p>
40
                        <p>Can't scan the code?</p>
Lines 69-82 Link Here
69
                        </fieldset>
69
                        </fieldset>
70
                    </form>
70
                    </form>
71
                [% ELSE %]
71
                [% ELSE %]
72
                    <h1>Two-Factor Authentication</h1>
72
                    <h1>Two-factor authentication</h1>
73
                    [% IF patron.auth_method == "two-factor" %]
73
                    [% IF patron.auth_method == "two-factor" %]
74
                        <div class="two-factor-status">Status: Enabled</div>
74
                        <div class="two-factor-status">Status: Enabled</div>
75
75
76
                        <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
76
                        <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
77
                            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
77
                            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
78
                            <input type="hidden" name="op" value="disable-2FA" />
78
                            <input type="hidden" name="op" value="disable-2FA" />
79
                            <input type="submit" value="Disable Two-Factor Authentication" />
79
                            <input type="submit" value="Disable two-factor authentication" />
80
                        </form>
80
                        </form>
81
                    [% ELSE %]
81
                    [% ELSE %]
82
                        <div class="two-factor-status">Status: Disabled</div>
82
                        <div class="two-factor-status">Status: Disabled</div>
Lines 84-90 Link Here
84
                        <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
84
                        <form id="two-factor-auth" action="/cgi-bin/koha/members/two_factor_auth.pl" method="post">
85
                            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
85
                            <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
86
                            <input type="hidden" name="op" value="enable-2FA" />
86
                            <input type="hidden" name="op" value="enable-2FA" />
87
                            <input type="submit" value="Enable Two-Factor Authentication" />
87
                            <input type="submit" value="Enable two-factor authentication" />
88
                        </form>
88
                        </form>
89
89
90
                    [% END %]
90
                    [% END %]
(-)a/t/db_dependent/selenium/authentication_2fa.t (+178 lines)
Line 0 Link Here
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 => 2;
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', 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' );
61
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' );
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->secret, $secret32, 'secret is set in DB' );
84
85
    };
86
87
    subtest 'Login' => sub {
88
        plan tests => 19;
89
90
        my $mainpage = $s->base_url . q|mainpage.pl|;
91
92
        { # ok first try
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');
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({patron => $patron});
102
            my $code = $auth->code();
103
            $auth->clear;
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' );
107
        }
108
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' );
113
            fill_login_form($s);
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
            ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid two-factor code")]'));
119
            is( login_error($s), undef );
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
            ok($driver->find_element('//div[@class="dialog error"][contains(text(), "Invalid two-factor code")]'));
141
142
            my $auth = Koha::Auth::TwoFactorAuth->new({patron => $patron});
143
            my $code = $auth->code();
144
            $auth->clear;
145
            $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
146
            $driver->find_element('//input[@type="submit"]')->click;
147
            like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
148
        }
149
    };
150
151
    $driver->quit();
152
};
153
154
END {
155
    $_->delete for @data_to_cleanup;
156
    C4::Context->set_preference('TwoFactorAuthentication', $pref_value);
157
};
158
159
160
sub login_error {
161
    my ( $s ) = @_;
162
    my $driver   = $s->driver;
163
164
    $s->remove_error_handler;
165
    my $login_error = eval {
166
        my $elt = $driver->find_element('//div[@id="login_error"]');
167
        return $elt->get_text if $elt && $elt->id;
168
    };
169
    $s->add_error_handler;
170
    return $login_error;
171
}
172
173
# Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect
174
sub fill_login_form {
175
    my ( $s ) = @_;
176
    $s->fill_form({ userid => $s->login, password => $s->password });
177
    $s->driver->find_element('//input[@id="submit-button"]')->click;
178
}
(-)a/t/lib/TestBuilder.pm (-1 / +1 lines)
Lines 553-558 sub _gen_default_values { Link Here
553
            lost           => undef,
553
            lost           => undef,
554
            debarred       => undef,
554
            debarred       => undef,
555
            borrowernotes  => '',
555
            borrowernotes  => '',
556
            secret         => undef,
556
        },
557
        },
557
        Item => {
558
        Item => {
558
            notforloan         => 0,
559
            notforloan         => 0,
559
- 

Return to bug 28786