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

(-)a/C4/Auth.pm (-30 / +92 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 858-863 sub checkauth { Link Here
858
    my $q_userid = $query->param('userid') // '';
859
    my $q_userid = $query->param('userid') // '';
859
860
860
    my $session;
861
    my $session;
862
    my $invalid_otp_token;
863
    my $require_2FA = ( C4::Context->preference('TwoFactorAuthentication') && $type ne "OPAC" ) ? 1 : 0;
864
    my $auth_challenge_complete;
861
865
862
    # Basic authentication is incompatible with the use of Shibboleth,
866
    # Basic authentication is incompatible with the use of Shibboleth,
863
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
867
    # as Shibboleth may return REMOTE_USER as a Shibboleth attribute,
Lines 889-901 sub checkauth { Link Here
889
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
893
            { remote_addr => $ENV{REMOTE_ADDR}, skip_version_check => 1 }
890
        );
894
        );
891
895
896
        if ( $return eq 'ok' || $return eq 'additional-auth-needed' ) {
897
            $userid = $session->param('id');
898
        }
899
900
        $additional_auth_needed = ( $return eq 'additional-auth-needed' ) ? 1 : 0;
901
902
        # We are at the second screen if the waiting-for-2FA is set in session
903
        # and otp_token param has been passed
904
        if (   $require_2FA
905
            && $additional_auth_needed
906
            && ( my $otp_token = $query->param('otp_token') ) )
907
        {
908
            my $patron    = Koha::Patrons->find( { userid => $userid } );
909
            my $auth      = Koha::Auth::TwoFactorAuth::get_auth( { patron => $patron } );
910
            my $verified = $auth->verify($otp_token);
911
            $auth->clear;
912
            if ( $verified ) {
913
                # The token is correct, the user is fully logged in!
914
                $additional_auth_needed = 0;
915
                $session->param( 'waiting-for-2FA', 0 );
916
                $return = "ok";
917
                $auth_challenge_complete = 1;
918
919
               # This is an ugly trick to pass the test
920
               # $query->param('koha_login_context') && ( $q_userid ne $userid )
921
               # few lines later
922
                $q_userid = $userid;
923
            }
924
            else {
925
                $invalid_otp_token = 1;
926
            }
927
        }
928
892
        if ( $return eq 'ok' ) {
929
        if ( $return eq 'ok' ) {
893
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
930
            Koha::Logger->get->debug(sprintf "AUTH_SESSION: (%s)\t%s %s - %s", map { $session->param($_) || q{} } qw(cardnumber firstname surname branch));
894
931
895
            my $s_userid = $session->param('id');
932
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $userid ) )
896
            $userid      = $s_userid;
897
898
            if ( ( $query->param('koha_login_context') && ( $q_userid ne $s_userid ) )
899
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
933
                || ( $cas && $query->param('ticket') && !C4::Context->userenv->{'id'} )
900
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
934
                || ( $shib && $shib_login && !$logout && !C4::Context->userenv->{'id'} )
901
            ) {
935
            ) {
Lines 905-933 sub checkauth { Link Here
905
                $anon_search_history = $session->param('search_history');
939
                $anon_search_history = $session->param('search_history');
906
                $session->delete();
940
                $session->delete();
907
                $session->flush;
941
                $session->flush;
908
                C4::Context::_unset_userenv($sessionID);
909
                $sessionID = undef;
910
            }
911
            elsif ($logout) {
912
913
                # voluntary logout the user
914
                # check wether the user was using their shibboleth session or a local one
915
                my $shibSuccess = C4::Context->userenv->{'shibboleth'};
916
                $session->delete();
917
                $session->flush;
918
                $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
942
                $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
919
                C4::Context::_unset_userenv($sessionID);
943
                C4::Context::_unset_userenv($sessionID);
920
                $sessionID = undef;
944
                $sessionID = undef;
921
945
            } elsif (!$logout) {
922
                if ($cas and $caslogout) {
923
                    logout_cas($query, $type);
924
                }
925
926
                # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
927
                if ( $shib and $shib_login and $shibSuccess) {
928
                    logout_shib($query);
929
                }
930
            } else {
931
946
932
                $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
947
                $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
933
                    -name     => 'CGISESSID',
948
                    -name     => 'CGISESSID',
Lines 954-963 sub checkauth { Link Here
954
        }
969
        }
955
    }
970
    }
956
971
957
    unless ( $loggedin ) {
972
    if ( ( !$loggedin && !$additional_auth_needed ) || $logout ) {
973
        $sessionID = undef;
958
        $userid    = undef;
974
        $userid    = undef;
959
    }
975
    }
960
976
977
    if ($logout) {
978
979
        # voluntary logout the user
980
        # check wether the user was using their shibboleth session or a local one
981
        my $shibSuccess = C4::Context->userenv->{'shibboleth'};
982
        if ( $session ) {
983
            $session->delete();
984
            $session->flush;
985
        }
986
        C4::Context::_unset_userenv($sessionID);
987
        $cookie = $cookie_mgr->clear_unless( $query->cookie, @$cookie );
988
989
        if ($cas and $caslogout) {
990
            logout_cas($query, $type);
991
        }
992
993
        # If we are in a shibboleth session (shibboleth is enabled, a shibboleth match attribute is set and matches koha matchpoint)
994
        if ( $shib and $shib_login and $shibSuccess) {
995
            logout_shib($query);
996
        }
997
998
        $session   = undef;
999
        $additional_auth_needed = 0;
1000
    }
1001
961
    unless ( $userid ) {
1002
    unless ( $userid ) {
962
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
1003
        #we initiate a session prior to checking for a username to allow for anonymous sessions...
963
        if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
1004
        if( !$session or !$sessionID ) { # if we cleared sessionID, we need a new session
Lines 1257-1265 sub checkauth { Link Here
1257
        $session->flush;
1298
        $session->flush;
1258
    }    # END unless ($userid)
1299
    }    # END unless ($userid)
1259
1300
1301
    if ( $require_2FA && ( $loggedin && !$auth_challenge_complete)) {
1302
        my $patron = Koha::Patrons->find({userid => $userid});
1303
        if ( $patron->auth_method eq 'two-factor' ) {
1304
            # Ask for the OTP token
1305
            $additional_auth_needed = 1;
1306
            $session->param('waiting-for-2FA', 1);
1307
            %info = ();# We remove the warnings/errors we may have set incorrectly before
1308
        }
1309
    }
1310
1260
    # finished authentification, now respond
1311
    # finished authentification, now respond
1261
    if ( $loggedin || $authnotrequired )
1312
    if ( ( $loggedin || $authnotrequired ) && !$additional_auth_needed ) {
1262
    {
1263
        # successful login
1313
        # successful login
1264
        unless (@$cookie) {
1314
        unless (@$cookie) {
1265
            $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
1315
            $cookie = $cookie_mgr->replace_in_list( $cookie, $query->cookie(
Lines 1293-1308 sub checkauth { Link Here
1293
    #
1343
    #
1294
    #
1344
    #
1295
1345
1346
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1347
1296
    # get the inputs from the incoming query
1348
    # get the inputs from the incoming query
1297
    my @inputs = ();
1349
    my @inputs = ();
1350
    my @inputs_to_clean = qw( userid password ticket logout.x otp_token );
1298
    foreach my $name ( param $query) {
1351
    foreach my $name ( param $query) {
1299
        (next) if ( $name eq 'userid' || $name eq 'password' || $name eq 'ticket' );
1352
        next if grep { $name eq $_ } @inputs_to_clean;
1300
        my @value = $query->multi_param($name);
1353
        my @value = $query->multi_param($name);
1301
        push @inputs, { name => $name, value => $_ } for @value;
1354
        push @inputs, { name => $name, value => $_ } for @value;
1302
    }
1355
    }
1303
1356
1304
    my $patron = Koha::Patrons->find({ userid => $q_userid }); # Not necessary logged in!
1305
1306
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1357
    my $LibraryNameTitle = C4::Context->preference("LibraryName");
1307
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1358
    $LibraryNameTitle =~ s/<(?:\/?)(?:br|p)\s*(?:\/?)>/ /sgi;
1308
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
1359
    $LibraryNameTitle =~ s/<(?:[^<>'"]|'(?:[^']*)'|"(?:[^"]*)")*>//sg;
Lines 1350-1355 sub checkauth { Link Here
1350
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1401
    $template->param( SCI_login => 1 ) if ( $query->param('sci_user_login') );
1351
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1402
    $template->param( OpacPublic => C4::Context->preference("OpacPublic") );
1352
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1403
    $template->param( loginprompt => 1 ) unless $info{'nopermission'};
1404
    if ( $additional_auth_needed ) {
1405
        $template->param(
1406
            TwoFA_prompt => 1,
1407
            invalid_otp_token => $invalid_otp_token,
1408
        );
1409
    }
1353
1410
1354
    if ( $type eq 'opac' ) {
1411
    if ( $type eq 'opac' ) {
1355
        require Koha::Virtualshelves;
1412
        require Koha::Virtualshelves;
Lines 1458-1463 Possible return values in C<$status> are: Link Here
1458
1515
1459
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1516
=item "restricted" -- The IP has changed (if SessionRestrictionByIP)
1460
1517
1518
=item "additional-auth-needed -- User is in an authentication process that is not finished
1519
1461
=back
1520
=back
1462
1521
1463
=cut
1522
=cut
Lines 1733-1738 sub check_cookie_auth { Link Here
1733
                    $session->param('desk_id'),      $session->param('desk_name'),
1792
                    $session->param('desk_id'),      $session->param('desk_name'),
1734
                    $session->param('register_id'),  $session->param('register_name')
1793
                    $session->param('register_id'),  $session->param('register_name')
1735
                );
1794
                );
1795
                return ( "additional-auth-needed", $session )
1796
                    if $session->param('waiting-for-2FA');
1797
1736
                return ( "ok", $session );
1798
                return ( "ok", $session );
1737
            } else {
1799
            } else {
1738
                $session->delete();
1800
                $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