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

(-)a/C4/Auth.pm (-30 / +95 lines)
Lines 34-39 use Koha; Link Here
34
use Koha::Logger;
34
use Koha::Logger;
35
use Koha::Caches;
35
use Koha::Caches;
36
use Koha::AuthUtils qw( get_script_name hash_password );
36
use Koha::AuthUtils qw( get_script_name hash_password );
37
use Koha::Auth::TwoFactorAuth;
37
use Koha::Checkouts;
38
use Koha::Checkouts;
38
use Koha::DateUtils qw( dt_from_string );
39
use Koha::DateUtils qw( dt_from_string );
39
use Koha::Library::Groups;
40
use Koha::Library::Groups;
Lines 838-843 sub checkauth { Link Here
838
    my $q_userid = $query->param('userid') // '';
839
    my $q_userid = $query->param('userid') // '';
839
840
840
    my $session;
841
    my $session;
842
    my $invalid_otp_token;
843
    my $require_2FA = ( C4::Context->preference('TwoFactorAuthentication') && $type ne "OPAC" ) ? 1 : 0;
844
    my $auth_challenge_complete;
841
845
842
    # Basic authentication is incompatible with the use of Shibboleth,
846
    # Basic authentication is incompatible with the use of Shibboleth,
843
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
847
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
Lines 869-881 sub checkauth { Link Here
869
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
873
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
870
        );
874
        );
871
875
876
        if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
877
            $userid = $session->param('id');
878
        }
879
880
        $additional_auth_needed = ( $return eq 'additional-auth-needed' ) ? 1 : 0;
881
882
        # We are at the second screen if the waiting-for-2FA is set in session
883
        # and otp_token param has been passed
884
        if (   $require_2FA
885
            && $additional_auth_needed
886
            && ( my $otp_token = $query->param('otp_token') ) )
887
        {
888
            my $patron    = Koha::Patrons->find( { userid => $userid } );
889
            my $auth      = Koha::Auth::TwoFactorAuth::get_auth( { patron => $patron } );
890
            my $verified = $auth->verify($otp_token);
891
            $auth->clear;
892
            if ( $verified ) {
893
                # The token is correct, the user is fully logged in!
894
                $additional_auth_needed = 0;
895
                $session->param( 'waiting-for-2FA', 0 );
896
                $return = "ok";
897
                $auth_challenge_complete = 1;
898
899
               # This is an ugly trick to pass the test
900
               # $query->param('koha_login_context') && ( $q_userid ne $userid )
901
               # few lines later
902
                $q_userid = $userid;
903
            }
904
            else {
905
                $invalid_otp_token = 1;
906
            }
907
        }
908
872
        if ( $return eq 'ok' ) {
909
        if ( $return eq 'ok' ) {
873
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
910
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
874
911
875
            my $s_userid = $session->param('id');
912
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
876
            $userid      = $s_userid;
877
878
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
879
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
913
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
880
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
914
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
881
            ) {
915
            ) {
Lines 886-910 sub checkauth { Link Here
886
                $session->delete();
920
                $session->delete();
887
                $session->flush;
921
                $session->flush;
888
                C4::Context->_unset_userenv($sessionID);
922
                C4::Context->_unset_userenv($sessionID);
889
            }
923
            } elsif (!$logout) {
890
            elsif ($logout) {
891
892
                # voluntary logout the user
893
                # check wether the user was using their shibboleth session or a local one
894
                my $shibSuccess = C4::Context->userenv->{'shibboleth'};
895
                $session->delete();
896
                $session->flush;
897
                C4::Context->_unset_userenv($sessionID);
898
899
                if ($cas and $caslogout) {
900
                    logout_cas($query, $type);
901
                }
902
903
                # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
904
                if ( $shib and $shib_login and $shibSuccess) {
905
                    logout_shib($query);
906
                }
907
            } else {
908
924
909
                $cookie = $query->cookie(
925
                $cookie = $query->cookie(
910
                    -name     => 'CGISESSID',
926
                    -name     => 'CGISESSID',
Lines 934-947 sub checkauth { Link Here
934
        }
950
        }
935
    }
951
    }
936
952
937
    unless ( $loggedin ) {
953
    if ( ( !$loggedin && !$additional_auth_needed ) || $logout ) {
938
        $sessionID = undef;
954
        $sessionID = undef;
955
        $session   = undef;
939
        $userid    = undef;
956
        $userid    = undef;
940
    }
957
    }
941
958
959
    if ($logout) {
960
961
        # voluntary logout the user
962
        # check wether the user was using their shibboleth session or a local one
963
        my $shibSuccess = C4::Context->userenv->{'shibboleth'};
964
        if ( $session ) {
965
            $session->delete();
966
            $session->flush;
967
        }
968
        C4::Context->_unset_userenv($sessionID);
969
970
        if ($cas and $caslogout) {
971
            logout_cas($query, $type);
972
        }
973
974
        # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
975
        if ( $shib and $shib_login and $shibSuccess) {
976
            logout_shib($query);
977
        }
978
979
        $sessionID = undef;
980
        $session   = undef;
981
        $userid    = undef;
982
        $additional_auth_needed = 0;
983
    }
984
942
    unless ( $userid ) {
985
    unless ( $userid ) {
943
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
986
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
944
        my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
987
        $session = get_session("") or die "Auth ERROR: Cannot get_session()";
945
988
946
        # Save anonymous search history in new session so it can be retrieved
989
        # Save anonymous search history in new session so it can be retrieved
947
        # by get_template_and_user to store it in user's search history after
990
        # by get_template_and_user to store it in user's search history after
Lines 1228-1236 sub checkauth { Link Here
1228
        }
1271
        }
1229
    }    # END unless ($userid)
1272
    }    # END unless ($userid)
1230
1273
1274
    if ( $require_2FA && ( $loggedin && !$auth_challenge_complete)) {
1275
        my $patron = Koha::Patrons->find({userid => $userid});
1276
        if ( $patron->auth_method eq 'two-factor' ) {
1277
            # Ask for the OTP token
1278
            $additional_auth_needed = 1;
1279
            $session->param('waiting-for-2FA', 1);
1280
            %info = ();# We remove the warnings/errors we may have set incorrectly before
1281
        }
1282
    }
1283
1231
    # finished authentification, now respond
1284
    # finished authentification, now respond
1232
    if ( $loggedin || $authnotrequired )
1285
    if ( ( $loggedin || $authnotrequired ) && !$additional_auth_needed ) {
1233
    {
1234
        # successful login
1286
        # successful login
1235
        unless ($cookie) {
1287
        unless ($cookie) {
1236
            $cookie = $query->cookie(
1288
            $cookie = $query->cookie(
Lines 1264-1279 sub checkauth { Link Here
1264
    #
1316
    #
1265
    #
1317
    #
1266
1318
1319
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1320
1267
    # get the inputs from the incoming query
1321
    # get the inputs from the incoming query
1268
    my @inputs = ();
1322
    my @inputs = ();
1323
    my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1269
    foreach my $name ( param $query) {
1324
    foreach my $name ( param $query) {
1270
        (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
1325
        next if grep { $name eq $_ } @inputs_to_clean;
1271
        my @value = $query->multi_param($name);
1326
        my @value = $query->multi_param($name);
1272
        push @inputs, { name => $name, value => $_ } for @value;
1327
        push @inputs, { name => $name, value => $_ } for @value;
1273
    }
1328
    }
1274
1329
1275
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1276
1277
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1330
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1278
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1331
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1279
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1332
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
Lines 1321-1326 sub checkauth { Link Here
1321
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1374
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1322
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1375
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1323
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1376
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1377
    if ( $additional_auth_needed ) {
1378
        $template->param(
1379
            TwoFA_prompt => 1,
1380
            invalid_otp_token => $invalid_otp_token,
1381
        );
1382
    }
1324
1383
1325
    if ( $type eq 'opac' ) {
1384
    if ( $type eq 'opac' ) {
1326
        require Koha::Virtualshelves;
1385
        require Koha::Virtualshelves;
Lines 1429-1434 Possible return values in C<$status> are: Link Here
1429
1488
1430
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1489
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1431
1490
1491
=item "additional-auth-needed -- User is in an authentication process that is not finished
1492
1432
=back
1493
=back
1433
1494
1434
=cut
1495
=cut
Lines 1704-1709 sub check_cookie_auth { Link Here
1704
            $session->param( 'lasttime', time() );
1765
            $session->param( 'lasttime', time() );
1705
            my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1766
            my $flags = defined($flagsrequired) ? haspermission( $userid, $flagsrequired ) : 1;
1706
            if ($flags) {
1767
            if ($flags) {
1768
1769
                return ( "additional-auth-needed", $session )
1770
                    if $session->param('waiting-for-2FA');
1771
1707
                return ( "ok", $session );
1772
                return ( "ok", $session );
1708
            } else {
1773
            } else {
1709
                $session->delete();
1774
                $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