Bugzilla – Attachment 137391 Details for
Bug 28787
Send a notice with the TOTP token
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 28787: Send a notice with the TOTP token
Bug-28787-Send-a-notice-with-the-TOTP-token.patch (text/plain), 18.80 KB, created by
Marcel de Rooy
on 2022-07-08 09:44:00 UTC
(
hide
)
Description:
Bug 28787: Send a notice with the TOTP token
Filename:
MIME Type:
Creator:
Marcel de Rooy
Created:
2022-07-08 09:44:00 UTC
Size:
18.80 KB
patch
obsolete
>From ab027e86346f7659daeff0c3cbdc95d894851d12 Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Mon, 23 May 2022 22:51:26 +0200 >Subject: [PATCH] Bug 28787: Send a notice with the TOTP token >Content-Type: text/plain; charset=utf-8 > >Bug 28786 let librarians enable a Two-factor authentication but force them to use >an application to generate the TOTP token. > >This new enhancement add the ability to send an email containing the token to the >patron once it's authenticaed > >The new notice template has the code '2FA_OTP_TOKEN' > >Test plan: >- Setup the two-factor authentication (you need the config entry and the >syspref ON) >- Enable it for your logged in patron >- Logout >- Login and notice the new link "Send the code by email" >- Click on it and confirm that you received an email with the code >- Use the code to be fully logged in > >QA question: Is 400 the correct error code to tell the email has not >been sent? > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >--- > C4/Letters.pm | 6 + > Koha/REST/V1/Auth.pm | 13 ++- > Koha/REST/V1/TwoFactorAuth.pm | 79 +++++++++++++ > api/v1/swagger/paths/auth.yaml | 38 +++++++ > api/v1/swagger/swagger.yaml | 2 + > .../data/mysql/atomicupdate/bug_28787.pl | 15 +++ > .../mysql/en/mandatory/sample_notices.yml | 14 +++ > .../intranet-tmpl/prog/en/modules/auth.tt | 23 ++++ > t/db_dependent/api/v1/two_factor_auth.t | 106 ++++++++++++++++++ > t/db_dependent/selenium/authentication_2fa.t | 53 ++++++++- > 10 files changed, 346 insertions(+), 3 deletions(-) > create mode 100644 Koha/REST/V1/TwoFactorAuth.pm > create mode 100644 api/v1/swagger/paths/auth.yaml > create mode 100755 installer/data/mysql/atomicupdate/bug_28787.pl > create mode 100755 t/db_dependent/api/v1/two_factor_auth.t > >diff --git a/C4/Letters.pm b/C4/Letters.pm >index 8ab0bb727e..4eef7cd2c0 100644 >--- a/C4/Letters.pm >+++ b/C4/Letters.pm >@@ -36,6 +36,7 @@ use Koha::Email; > use Koha::Notice::Messages; > use Koha::Notice::Templates; > use Koha::DateUtils qw( dt_from_string output_pref ); >+use Koha::Auth::TwoFactorAuth; > use Koha::Patrons; > use Koha::SMTP::Servers; > use Koha::Subscriptions; >@@ -1603,6 +1604,11 @@ sub _process_tt { > $content = add_tt_filters( $content ); > $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|; > >+ if ( $content =~ m|\[% otp_token %\]| ) { >+ my $patron = Koha::Patrons->find(C4::Context->userenv->{number}); >+ $tt_params->{otp_token} = Koha::Auth::TwoFactorAuth->new({patron => $patron})->code; >+ } >+ > my $output; > $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error(); > >diff --git a/Koha/REST/V1/Auth.pm b/Koha/REST/V1/Auth.pm >index 182db518f6..4fac9237c6 100644 >--- a/Koha/REST/V1/Auth.pm >+++ b/Koha/REST/V1/Auth.pm >@@ -82,6 +82,7 @@ sub under { > } > > if ( $c->req->url->to_abs->path eq '/api/v1/oauth/token' ) { >+ #|| $c->req->url->to_abs->path eq '/api/v1/auth/send_otp_token' ) { > # Requesting a token shouldn't go through the API authenticaction chain > $status = 1; > } >@@ -161,6 +162,7 @@ sub authenticate_api_request { > $c->stash_overrides(); > > my $cookie_auth = 0; >+ my $pending_auth; > > my $authorization = $spec->{'x-koha-authorization'}; > >@@ -229,6 +231,13 @@ sub authenticate_api_request { > elsif ($status eq "anon") { > $cookie_auth = 1; > } >+ elsif ($status eq "additional-auth-needed") { >+ if ( $c->req->url->to_abs->path eq '/api/v1/auth/send_otp_token' ) { >+ $user = Koha::Patrons->find( $session->param('number') ); >+ $cookie_auth = 1; >+ $pending_auth = 1; >+ } >+ } > elsif ($status eq "maintenance") { > Koha::Exceptions::UnderMaintenance->throw( > error => 'System is under maintenance.' >@@ -261,7 +270,9 @@ sub authenticate_api_request { > if ( !$authorization and > ( $params->{is_public} and > ( C4::Context->preference('RESTPublicAnonymousRequests') or >- $user) or $params->{is_plugin} ) ) { >+ $user) or $params->{is_plugin} ) >+ or $pending_auth >+ ) { > # We do not need any authorization > # Check the parameters > validate_query_parameters( $c, $spec ); >diff --git a/Koha/REST/V1/TwoFactorAuth.pm b/Koha/REST/V1/TwoFactorAuth.pm >new file mode 100644 >index 0000000000..35479228e5 >--- /dev/null >+++ b/Koha/REST/V1/TwoFactorAuth.pm >@@ -0,0 +1,79 @@ >+package Koha::REST::V1::TwoFactorAuth; >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Mojo::Base 'Mojolicious::Controller'; >+use Try::Tiny; >+ >+use C4::Letters qw( GetPreparedLetter ); >+ >+=head1 NAME >+ >+Koha::REST::V1::TwoFactorAuth >+ >+=head1 API >+ >+=head2 Methods >+ >+=head3 send_otp_token >+ >+Will send an email with the OTP token needed to complete the second authentication step. >+ >+=cut >+ >+ >+sub send_otp_token { >+ >+ my $c = shift->openapi->valid_input or return; >+ >+ my $patron = Koha::Patrons->find( $c->stash('koha.user')->borrowernumber ); >+ >+ return try { >+ >+ my $letter = C4::Letters::GetPreparedLetter( >+ module => 'members', >+ letter_code => '2FA_OTP_TOKEN', >+ branchcode => $patron->branchcode, >+ tables => { >+ borrowers => $patron->unblessed, >+ } >+ ); >+ my $message_id = C4::Letters::EnqueueLetter( >+ { >+ letter => $letter, >+ borrowernumber => $patron->borrowernumber, >+ message_transport_type => 'email' >+ } >+ ); >+ C4::Letters::SendQueuedMessages({message_id => $message_id}); >+ >+ my $message = C4::Letters::GetMessage($message_id); >+ >+ if ( $message->{status} eq 'sent' ) { >+ return $c->render(status => 200, openapi => {}); >+ } elsif ( $message->{status} eq 'failed' ) { >+ return $c->render(status => 400, openapi => { error => 'email_not_sent'}); >+ } >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+ >+} >+ >+1; >diff --git a/api/v1/swagger/paths/auth.yaml b/api/v1/swagger/paths/auth.yaml >new file mode 100644 >index 0000000000..3e0ac45083 >--- /dev/null >+++ b/api/v1/swagger/paths/auth.yaml >@@ -0,0 +1,38 @@ >+--- >+/auth/send_otp_token: >+ post: >+ x-mojo-to: TwoFactorAuth#send_otp_token >+ operationId: send_otp_token >+ tags: >+ - 2fa >+ summary: Send OTP token for second step authentication >+ produces: >+ - application/json >+ responses: >+ "200": >+ description: OK >+ schema: >+ type: object >+ properties: >+ access_token: >+ type: string >+ token_type: >+ type: string >+ expires_in: >+ type: integer >+ additionalProperties: false >+ "400": >+ description: Bad Request >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "403": >+ description: Access forbidden >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >+ "500": >+ description: | >+ Internal server error. Possible `error_code` attribute values: >+ >+ * `internal_server_error` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" >diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml >index f020ee427a..1b16b96003 100644 >--- a/api/v1/swagger/swagger.yaml >+++ b/api/v1/swagger/swagger.yaml >@@ -97,6 +97,8 @@ paths: > $ref: "./paths/advancededitormacros.yaml#/~1advanced_editor~1macros~1{advancededitormacro_id}" > "/article_requests/{article_request_id}": > $ref: "./paths/article_requests.yaml#/~1article_requests~1{article_request_id}" >+ /auth/send_otp_token: >+ $ref: paths/auth.yaml#/~1auth~1send_otp_token > "/biblios/{biblio_id}": > $ref: "./paths/biblios.yaml#/~1biblios~1{biblio_id}" > "/biblios/{biblio_id}/checkouts": >diff --git a/installer/data/mysql/atomicupdate/bug_28787.pl b/installer/data/mysql/atomicupdate/bug_28787.pl >new file mode 100755 >index 0000000000..bce0671836 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_28787.pl >@@ -0,0 +1,15 @@ >+use Modern::Perl; >+ >+return { >+ bug_number => "28787", >+ description => "Add new letter 2FA_OTP_TOKEN", >+ up => sub { >+ my ($args) = @_; >+ my ($dbh, $out) = @$args{qw(dbh out)}; >+ $dbh->do(q{ >+ INSERT IGNORE INTO `letter` (`module`, `code`, `branchcode`, `name`, `is_html`, `title`, `content`, `message_transport_type`) VALUES >+ ('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') >+ }); >+ >+ }, >+}; >diff --git a/installer/data/mysql/en/mandatory/sample_notices.yml b/installer/data/mysql/en/mandatory/sample_notices.yml >index 34ffab80df..16d84b9533 100644 >--- a/installer/data/mysql/en/mandatory/sample_notices.yml >+++ b/installer/data/mysql/en/mandatory/sample_notices.yml >@@ -1110,6 +1110,20 @@ tables: > - "" > - "If you have any problems or questions regarding your account, please contact the library." > >+ - module: members >+ code: 2FA_OTP_TOKEN >+ branchcode: "" >+ name: "two-authentication step token" >+ is_html: 1 >+ title: "Two-authentication token" >+ message_transport_type: email >+ lang: default >+ content: >+ - "Dear [% borrower.firstname %] [% borrower.surname %] ([% borrower.cardnumber %])" >+ - "" >+ - "Your authentication token is [% otp_token %]." >+ - "It is valid one minute." >+ > - module: orderacquisition > code: ACQORDER > branchcode: "" >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >index 47e135312c..e188d5f268 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >@@ -155,12 +155,15 @@ > <div id="login_error">Invalid two-factor code</div> > [% END %] > >+ <div id="email_error" class="dialog alert" style="display: none;"></div> >+ <div id="email_success" class="dialog message" style="display: none;"></div> > <p> > <label for="otp_token">Two-factor authentication code:</label> > <input type="text" name="otp_token" id="otp_token" class="input focus" value="" size="20" tabindex="1" /> > </p> > <p> > <input id="submit-button" type="submit" value="Verify code" /> >+ <a class="send_otp" id="send_otp" href="#">Send the code by email</a> > <a class="cancel" id="logout" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Cancel</a> > </p> > >@@ -189,6 +192,26 @@ > } > // Clear last borrowers, rememberd sql reports, carts, etc. > logOut(); >+ >+ $("#send_otp").on("click", function(e){ >+ e.preventDefault(); >+ $("#email_success").hide(); >+ $("#email_error").hide(); >+ $.ajax({ >+ url: '/api/v1/auth/send_otp_token', >+ type: 'POST', >+ success: function(data){ >+ let message = _("The code has been sent by email, please check your inbox.") >+ $("#email_success").show().html(message); >+ }, >+ error: function(data){ >+ let error = data.responseJSON && data.responseJSON.error == "email_not_sent" >+ ? _("Email not sent, maybe you don't have an email address defined?") >+ : _("Email not sent"); >+ $("#email_error").show().html(error); >+ } >+ }); >+ }); > }); > </script> > [% END %] >diff --git a/t/db_dependent/api/v1/two_factor_auth.t b/t/db_dependent/api/v1/two_factor_auth.t >new file mode 100755 >index 0000000000..c22fe819f8 >--- /dev/null >+++ b/t/db_dependent/api/v1/two_factor_auth.t >@@ -0,0 +1,106 @@ >+#!/usr/bin/env perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Test::More tests => 1; >+use Test::Mojo; >+use Test::MockModule; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+use Koha::Database; >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+# FIXME: sessionStorage defaults to mysql, but it seems to break transaction handling >+# this affects the other REST api tests >+t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); >+ >+my $remote_address = '127.0.0.1'; >+my $t = Test::Mojo->new('Koha::REST::V1'); >+ >+subtest 'send_otp_token' => sub { >+ >+ plan tests => 7; >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ flags => 16 >+ } >+ } >+ ); >+ >+ my $session = C4::Auth::get_session(''); >+ $session->param( 'number', $patron->borrowernumber ); >+ $session->param( 'id', $patron->userid ); >+ $session->param( 'ip', '127.0.0.1' ); >+ $session->param( 'lasttime', time() ); >+ $session->flush; >+ >+ my $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" ); >+ $tx->req->cookies( { name => 'CGISESSID', value => $session->id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ >+ # Patron is not authenticated yet >+ $t->request_ok($tx)->status_is(403); >+ >+ $session->param('waiting-for-2FA', 1); >+ $session->flush; >+ >+ $session = C4::Auth::get_session($session->id); >+ >+ my $auth = Test::MockModule->new("C4::Auth"); >+ $auth->mock('check_cookie_auth', sub { return 'additional-auth-needed'}); >+ >+ $patron->library->set( >+ { >+ branchemail => 'from@example.org', >+ branchreturnpath => undef, >+ branchreplyto => undef, >+ } >+ )->store; >+ $patron->auth_method('two-factor'); >+ $patron->encode_secret("nv4v65dpobpxgzldojsxiii"); >+ $patron->email(undef); >+ $patron->store; >+ >+ $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" ); >+ $tx->req->cookies( { name => 'CGISESSID', value => $session->id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ >+ # Invalid email >+ $t->request_ok($tx)->status_is(400)->json_is({ error => 'email_not_sent' }); >+ >+ $patron->email('to@example.org')->store; >+ $tx = $t->ua->build_tx( POST => "/api/v1/auth/send_otp_token" ); >+ $tx->req->cookies( { name => 'CGISESSID', value => $session->id } ); >+ $tx->req->env( { REMOTE_ADDR => $remote_address } ); >+ >+ # Everything is ok, the email will be sent >+ $t->request_ok($tx)->status_is(200); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+1; >diff --git a/t/db_dependent/selenium/authentication_2fa.t b/t/db_dependent/selenium/authentication_2fa.t >index 2b8f888219..d00cdfda53 100755 >--- a/t/db_dependent/selenium/authentication_2fa.t >+++ b/t/db_dependent/selenium/authentication_2fa.t >@@ -16,7 +16,7 @@ > # along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; >-use Test::More tests => 3; >+use Test::More tests => 4; > > use C4::Context; > use Koha::AuthUtils; >@@ -94,7 +94,6 @@ SKIP: { > $driver->get($mainpage . q|?logout.x=1|); > $driver->get($s->base_url . q|circ/circulation.pl?borrowernumber=|.$patron->borrowernumber); > like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' ); >- $driver->capture_screenshot('selenium_failure_2.png'); > fill_login_form($s); > like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' ); > is( login_error($s), undef ); >@@ -150,6 +149,56 @@ SKIP: { > } > }; > >+ subtest "Send OTP code" => sub { >+ plan tests => 4; >+ >+ # Make sure the send won't fail because of invalid email addresses >+ $patron->library->set( >+ { >+ branchemail => 'from@example.org', >+ branchreturnpath => undef, >+ branchreplyto => undef, >+ } >+ )->store; >+ $patron->auth_method('two-factor'); >+ $patron->email(undef); >+ $patron->store; >+ >+ my $mainpage = $s->base_url . q|mainpage.pl|; >+ $driver->get( $mainpage . q|?logout.x=1| ); >+ like( >+ $driver->get_title, >+ qr(Log in to Koha), >+ 'Must be on the first auth screen' >+ ); >+ fill_login_form($s); >+ like( >+ $driver->get_title, >+ qr(Two-factor authentication), >+ 'Must be on the second auth screen' >+ ); >+ $driver->find_element('//a[@id="send_otp"]')->click; >+ $s->wait_for_ajax; >+ my $error = $driver->find_element('//div[@id="email_error"]')->get_text; >+ like( >+ $error, >+ qr{Email not sent}, >+ 'Email not sent will display an error' >+ ); >+ >+ $patron->email('test@example.org'); >+ $patron->store; >+ $driver->find_element('//a[@id="send_otp"]')->click; >+ $s->wait_for_ajax; >+ my $message = >+ $driver->find_element('//div[@id="email_success"]')->get_text; >+ is( >+ $message, >+ "The code has been sent by email, please check your inbox.", >+ 'The email must have been sent correctly' >+ ); >+ }; >+ > subtest "Disable" => sub { > plan tests => 4; > >-- >2.20.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 28787
:
135287
|
136571
|
136572
|
136573
|
137251
|
137330
|
137331
|
137369
|
137370
|
137371
|
137379
|
137380
|
137381
|
137391
|
137392
|
137393
|
137394
|
137395
|
137396
|
137397
|
137398
|
137399
|
137400
|
138146
|
138149
|
138150
|
138151
|
138152
|
138153
|
138154
|
138155
|
138156
|
138157
|
138158
|
138159
|
138689
|
138696
|
138697
|
138698
|
138699
|
138700
|
138701
|
138702
|
138703
|
138704
|
138705
|
138706
|
138707
|
138931
|
138932