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

(-)a/C4/Letters.pm (+6 lines)
Lines 36-41 use Koha::Email; Link Here
36
use Koha::Notice::Messages;
36
use Koha::Notice::Messages;
37
use Koha::Notice::Templates;
37
use Koha::Notice::Templates;
38
use Koha::DateUtils qw( dt_from_string output_pref );
38
use Koha::DateUtils qw( dt_from_string output_pref );
39
use Koha::Auth::TwoFactorAuth;
39
use Koha::Patrons;
40
use Koha::Patrons;
40
use Koha::SMTP::Servers;
41
use Koha::SMTP::Servers;
41
use Koha::Subscriptions;
42
use Koha::Subscriptions;
Lines 1609-1614 sub _process_tt { Link Here
1609
    $content = add_tt_filters( $content );
1610
    $content = add_tt_filters( $content );
1610
    $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1611
    $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1611
1612
1613
    if ( $content =~ m|\[% otp_token %\]| ) {
1614
        my $patron = Koha::Patrons->find(C4::Context->userenv->{number});
1615
        $tt_params->{otp_token} = Koha::Auth::TwoFactorAuth->new({patron => $patron})->code;
1616
    }
1617
1612
    my $output;
1618
    my $output;
1613
    my $schema = Koha::Database->new->schema;
1619
    my $schema = Koha::Database->new->schema;
1614
    $schema->txn_begin;
1620
    $schema->txn_begin;
(-)a/Koha/REST/V1/Auth.pm (-1 / +12 lines)
Lines 82-87 sub under { Link Here
82
        }
82
        }
83
83
84
        if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) {
84
        if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) {
85
            #|| $c->req->url->to_abs->path eq '/api/v1/auth/send_otp_token' ) {
85
            # Requesting a token shouldn't go through the API authenticaction chain
86
            # Requesting a token shouldn't go through the API authenticaction chain
86
            $status = 1;
87
            $status = 1;
87
        }
88
        }
Lines 161-166 sub authenticate_api_request { Link Here
161
    $c->stash_overrides();
162
    $c->stash_overrides();
162
163
163
    my $cookie_auth = 0;
164
    my $cookie_auth = 0;
165
    my $pending_auth;
164
166
165
    my $authorization = $spec->{'x-koha-authorization'};
167
    my $authorization = $spec->{'x-koha-authorization'};
166
168
Lines 229-234 sub authenticate_api_request { Link Here
229
        elsif ($status eq "anon") {
231
        elsif ($status eq "anon") {
230
            $cookie_auth = 1;
232
            $cookie_auth = 1;
231
        }
233
        }
234
        elsif ($status eq "additional-auth-needed") {
235
            if ( $c->req->url->to_abs->path eq '/api/v1/auth/send_otp_token' ) {
236
                $user = Koha::Patrons->find( $session->param('number') );
237
                $cookie_auth = 1;
238
                $pending_auth = 1;
239
            }
240
        }
232
        elsif ($status eq "maintenance") {
241
        elsif ($status eq "maintenance") {
233
            Koha::Exceptions::UnderMaintenance->throw(
242
            Koha::Exceptions::UnderMaintenance->throw(
234
                error => 'System is under maintenance.'
243
                error => 'System is under maintenance.'
Lines 261-267 sub authenticate_api_request { Link Here
261
    if ( !$authorization and
270
    if ( !$authorization and
262
         ( $params->{is_public} and
271
         ( $params->{is_public} and
263
          ( C4::Context->preference('RESTPublicAnonymousRequests') or
272
          ( C4::Context->preference('RESTPublicAnonymousRequests') or
264
            $user) or $params->{is_plugin} ) ) {
273
            $user) or $params->{is_plugin} )
274
        or $pending_auth
275
    ) {
265
        # We do not need any authorization
276
        # We do not need any authorization
266
        # Check the parameters
277
        # Check the parameters
267
        validate_query_parameters( $c, $spec );
278
        validate_query_parameters( $c, $spec );
(-)a/Koha/REST/V1/TwoFactorAuth.pm (+79 lines)
Line 0 Link Here
1
package Koha::REST::V1::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
20
use Mojo::Base 'Mojolicious::Controller';
21
use Try::Tiny;
22
23
use C4::Letters qw( GetPreparedLetter );
24
25
=head1 NAME
26
27
Koha::REST::V1::TwoFactorAuth
28
29
=head1 API
30
31
=head2 Methods
32
33
=head3 send_otp_token
34
35
Will send an email with the OTP token needed to complete the second authentication step.
36
37
=cut
38
39
40
sub send_otp_token {
41
42
    my $c = shift->openapi->valid_input or return;
43
44
    my $patron = Koha::Patrons->find( $c->stash('koha.user')->borrowernumber );
45
46
    return try {
47
48
        my $letter = C4::Letters::GetPreparedLetter(
49
            module      => 'members',
50
            letter_code => '2FA_OTP_TOKEN',
51
            branchcode  => $patron->branchcode,
52
            tables      => {
53
                borrowers => $patron->unblessed,
54
            }
55
        );
56
        my $message_id = C4::Letters::EnqueueLetter(
57
            {
58
                letter                 => $letter,
59
                borrowernumber         => $patron->borrowernumber,
60
                message_transport_type => 'email'
61
            }
62
        );
63
        C4::Letters::SendQueuedMessages({message_id => $message_id});
64
65
        my $message = C4::Letters::GetMessage($message_id);
66
67
        if ( $message->{status} eq 'sent' ) {
68
            return $c->render(status => 200, openapi => {});
69
        } elsif ( $message->{status} eq 'failed' ) {
70
            return $c->render(status => 400, openapi => { error => 'email_not_sent'});
71
        }
72
    }
73
    catch {
74
        $c->unhandled_exception($_);
75
    };
76
77
}
78
79
1;
(-)a/api/v1/swagger/paths/auth.yaml (+38 lines)
Line 0 Link Here
1
---
2
/auth/send_otp_token:
3
  post:
4
    x-mojo-to: TwoFactorAuth#send_otp_token
5
    operationId: send_otp_token
6
    tags:
7
      - 2fa
8
    summary: Send OTP token for second step authentication
9
    produces:
10
      - application/json
11
    responses:
12
      "200":
13
        description: OK
14
        schema:
15
          type: object
16
          properties:
17
            access_token:
18
              type: string
19
            token_type:
20
              type: string
21
            expires_in:
22
              type: integer
23
          additionalProperties: false
24
      "400":
25
        description: Bad Request
26
        schema:
27
          $ref: "../swagger.yaml#/definitions/error"
28
      "403":
29
        description: Access forbidden
30
        schema:
31
          $ref: "../swagger.yaml#/definitions/error"
32
      "500":
33
        description: |
34
          Internal server error. Possible `error_code` attribute values:
35
36
          * `internal_server_error`
37
        schema:
38
          $ref: "../swagger.yaml#/definitions/error"
(-)a/api/v1/swagger/swagger.yaml (+2 lines)
Lines 101-106 paths: Link Here
101
    $ref: "./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1{advancededitormacro_id}"
101
    $ref: "./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1{advancededitormacro_id}"
102
  "/article_requests/{article_request_id}":
102
  "/article_requests/{article_request_id}":
103
    $ref: "./paths/article_requests.yaml#/~1article_requests~1{article_request_id}"
103
    $ref: "./paths/article_requests.yaml#/~1article_requests~1{article_request_id}"
104
  /auth/send_otp_token:
105
    $ref: paths/auth.yaml#/~1auth~1send_otp_token
104
  "/biblios/{biblio_id}":
106
  "/biblios/{biblio_id}":
105
    $ref: "./paths/biblios.yaml#/~1biblios~1{biblio_id}"
107
    $ref: "./paths/biblios.yaml#/~1biblios~1{biblio_id}"
106
  "/biblios/{biblio_id}/checkouts":
108
  "/biblios/{biblio_id}/checkouts":
(-)a/installer/data/mysql/atomicupdate/bug_28787.pl (+15 lines)
Line 0 Link Here
1
use Modern::Perl;
2
3
return {
4
    bug_number => "28787",
5
    description => "Add new letter 2FA_OTP_TOKEN",
6
    up => sub {
7
        my ($args) = @_;
8
        my ($dbh, $out) = @$args{qw(dbh out)};
9
        $dbh->do(q{
10
            INSERT IGNORE INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`) VALUES
11
            ('members', '2FA_OTP_TOKEN', '', 'two-authentication step token', 0, 'Two-authentication step token', 'Dear [% borrower.firstname %] [% borrower.surname %] ([% borrower.cardnumber %])\r\n\r\nYour authentication token is [% otp_token %]. \r\nIt is valid one minute.', 'email')
12
        });
13
14
    },
15
};
(-)a/installer/data/mysql/en/mandatory/sample_notices.yml (+14 lines)
Lines 1110-1115 tables: Link Here
1110
            - ""
1110
            - ""
1111
            - "If you have any problems or questions regarding your account, please contact the library."
1111
            - "If you have any problems or questions regarding your account, please contact the library."
1112
1112
1113
        - module: members
1114
          code: 2FA_OTP_TOKEN
1115
          branchcode: ""
1116
          name: "two-authentication step token"
1117
          is_html: 1
1118
          title: "Two-authentication token"
1119
          message_transport_type: email
1120
          lang: default
1121
          content:
1122
            - "Dear [% borrower.firstname %] [% borrower.surname %] ([% borrower.cardnumber %])"
1123
            - ""
1124
            - "Your authentication token is [% otp_token %]."
1125
            - "It is valid one minute."
1126
1113
        - module: orderacquisition
1127
        - module: orderacquisition
1114
          code: ACQORDER
1128
          code: ACQORDER
1115
          branchcode: ""
1129
          branchcode: ""
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt (+23 lines)
Lines 155-166 Link Here
155
            <div id="login_error">Invalid two-factor code</div>
155
            <div id="login_error">Invalid two-factor code</div>
156
        [% END %]
156
        [% END %]
157
157
158
        <div id="email_error" class="dialog alert" style="display: none;"></div>
159
        <div id="email_success" class="dialog message" style="display: none;"></div>
158
        <p>
160
        <p>
159
            <label for="otp_token">Two-factor authentication code:</label>
161
            <label for="otp_token">Two-factor authentication code:</label>
160
            <input type="text" name="otp_token" id="otp_token" class="input focus" value="" size="20" tabindex="1" />
162
            <input type="text" name="otp_token" id="otp_token" class="input focus" value="" size="20" tabindex="1" />
161
        </p>
163
        </p>
162
        <p>
164
        <p>
163
            <input id="submit-button" type="submit" value="Verify code" />
165
            <input id="submit-button" type="submit" value="Verify code" />
166
            <a class="send_otp" id="send_otp" href="#">Send the code by email</a>
164
            <a class="cancel" id="logout" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Cancel</a>
167
            <a class="cancel" id="logout" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Cancel</a>
165
        </p>
168
        </p>
166
169
Lines 189-194 Link Here
189
            }
192
            }
190
            // Clear last borrowers, rememberd sql reports, carts, etc.
193
            // Clear last borrowers, rememberd sql reports, carts, etc.
191
            logOut();
194
            logOut();
195
196
            $("#send_otp").on("click", function(e){
197
                e.preventDefault();
198
                $("#email_success").hide();
199
                $("#email_error").hide();
200
                $.ajax({
201
                    url: '/api/v1/auth/send_otp_token',
202
                    type: 'POST',
203
                    success: function(data){
204
                        let message = _("The code has been sent by email, please check your inbox.")
205
                        $("#email_success").show().html(message);
206
                    },
207
                    error: function(data){
208
                        let error = data.responseJSON && data.responseJSON.error == "email_not_sent"
209
                            ? _("Email not sent, maybe you don't have an email address defined?")
210
                            : _("Email not sent");
211
                        $("#email_error").show().html(error);
212
                    }
213
                });
214
            });
192
        });
215
        });
193
    </script>
216
    </script>
194
[% END %]
217
[% END %]
(-)a/t/db_dependent/api/v1/two_factor_auth.t (+106 lines)
Line 0 Link Here
1
#!/usr/bin/env 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
20
use Test::More tests => 1;
21
use Test::Mojo;
22
use Test::MockModule;
23
24
use t::lib::TestBuilder;
25
use t::lib::Mocks;
26
27
use Koha::Database;
28
29
my $schema  = Koha::Database->new->schema;
30
my $builder = t::lib::TestBuilder->new;
31
32
# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling
33
# this affects the other REST api tests
34
t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
35
36
my $remote_address = '127.0.0.1';
37
my $t              = Test::Mojo->new('Koha::REST::V1');
38
39
subtest 'send_otp_token' => sub {
40
41
    plan tests => 7;
42
43
    $schema->storage->txn_begin;
44
45
    my $patron = $builder->build_object(
46
        {
47
            class => 'Koha::Patrons',
48
            value  => {
49
                flags => 16
50
            }
51
        }
52
    );
53
54
    my $session = C4::Auth::get_session('');
55
    $session->param( 'number',   $patron->borrowernumber );
56
    $session->param( 'id',       $patron->userid );
57
    $session->param( 'ip',       '127.0.0.1' );
58
    $session->param( 'lasttime', time() );
59
    $session->flush;
60
61
    my $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" );
62
    $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
63
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
64
65
    # Patron is not authenticated yet
66
    $t->request_ok($tx)->status_is(403);
67
68
    $session->param('waiting-for-2FA', 1);
69
    $session->flush;
70
71
    $session = C4::Auth::get_session($session->id);
72
73
    my $auth = Test::MockModule->new("C4::Auth");
74
    $auth->mock('check_cookie_auth', sub { return 'additional-auth-needed'});
75
76
    $patron->library->set(
77
        {
78
            branchemail      => 'from@example.org',
79
            branchreturnpath => undef,
80
            branchreplyto    => undef,
81
        }
82
    )->store;
83
    $patron->auth_method('two-factor');
84
    $patron->encode_secret("nv4v65dpobpxgzldojsxiii");
85
    $patron->email(undef);
86
    $patron->store;
87
88
    $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" );
89
    $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
90
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
91
92
    # Invalid email
93
    $t->request_ok($tx)->status_is(400)->json_is({ error => 'email_not_sent' });
94
95
    $patron->email('to@example.org')->store;
96
    $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" );
97
    $tx->req->cookies( { name => 'CGISESSID', value => $session->id } );
98
    $tx->req->env( { REMOTE_ADDR => $remote_address } );
99
100
    # Everything is ok, the email will be sent
101
    $t->request_ok($tx)->status_is(200);
102
103
    $schema->storage->txn_rollback;
104
};
105
106
1;
(-)a/t/db_dependent/selenium/authentication_2fa.t (-3 / +51 lines)
Lines 16-22 Link Here
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
17
18
use Modern::Perl;
18
use Modern::Perl;
19
use Test::More tests => 3;
19
use Test::More tests => 4;
20
20
21
use C4::Context;
21
use C4::Context;
22
use Koha::AuthUtils;
22
use Koha::AuthUtils;
Lines 94-100 SKIP: { Link Here
94
            $driver->get($mainpage . q|?logout.x=1|);
94
            $driver->get($mainpage . q|?logout.x=1|);
95
            $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
95
            $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber);
96
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
96
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
97
            $driver->capture_screenshot('selenium_failure_2.png');
98
            fill_login_form($s);
97
            fill_login_form($s);
99
            like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
98
            like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
100
            is( login_error($s), undef );
99
            is( login_error($s), undef );
Lines 150-155 SKIP: { Link Here
150
        }
149
        }
151
    };
150
    };
152
151
152
    subtest "Send OTP code" => sub {
153
        plan tests => 4;
154
155
        # Make sure the send won't fail because of invalid email addresses
156
        $patron->library->set(
157
            {
158
                branchemail      => 'from@example.org',
159
                branchreturnpath => undef,
160
                branchreplyto    => undef,
161
            }
162
        )->store;
163
        $patron->auth_method('two-factor');
164
        $patron->email(undef);
165
        $patron->store;
166
167
        my $mainpage = $s->base_url . q|mainpage.pl|;
168
        $driver->get( $mainpage . q|?logout.x=1| );
169
        like(
170
            $driver->get_title,
171
            qr(Log in to Koha),
172
            'Must be on the first auth screen'
173
        );
174
        fill_login_form($s);
175
        like(
176
            $driver->get_title,
177
            qr(Two-factor authentication),
178
            'Must be on the second auth screen'
179
        );
180
        $driver->find_element('//a[@id="send_otp"]')->click;
181
        $s->wait_for_ajax;
182
        my $error = $driver->find_element('//div[@id="email_error"]')->get_text;
183
        like(
184
            $error,
185
            qr{Email not sent},
186
            'Email not sent will display an error'
187
        );
188
189
        $patron->email('test@example.org');
190
        $patron->store;
191
        $driver->find_element('//a[@id="send_otp"]')->click;
192
        $s->wait_for_ajax;
193
        my $message =
194
          $driver->find_element('//div[@id="email_success"]')->get_text;
195
        is(
196
            $message,
197
            "The code has been sent by email, please check your inbox.",
198
            'The email must have been sent correctly'
199
        );
200
    };
201
153
    subtest "Disable" => sub {
202
    subtest "Disable" => sub {
154
        plan tests => 4;
203
        plan tests => 4;
155
204
156
- 

Return to bug 28787