From bcb9403dcc3028ca233a9ccc6cd8ea2b876472e9 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Thu, 17 Aug 2017 16:41:20 +0300 Subject: [PATCH] Bug 19133: Add an ability to link patron to third party service to reset their password Adds additional functionality to POST /public/patrons/password/recovery. Pass a new parameter "custom_link" to send patron a recovery email that contains this link instead of Koha OPAC's opac-password-recovery.pl page. This way third party services can link Koha patrons to their own service to have passwords recovered. By default, forbids all custom links. To enable specific services, use the new system preference OpacResetPasswordHostWhitelist. This preference contains a list of allowed host names to which it is allowed to link the patron in recovery email. Use case in steps (to understand the feature better): 1. Library uses a discovery software (e.g. VuFind, from now on VF) and has disabled Koha's OPAC 2. VF uses Koha REST API 3. Patrons can login to VF with their Koha userid and password 4. Patron goes to VF login screen, but has forgotten their password 5. Patron requests their password to be reset in VF 6. VF sends a POST request to /api/v1/patrons/password/recovery with patron's userid and email as parameters, as well as custom_link to a password reset page in VF 7. Koha sends patron an email containing a link to VF password reset page (instead of Koha OPAC's opac-password-recovery.pl) 8. Patron reads their email and resets password in VF To test: 1. Apply patch and run updatedatabase.pl 2. prove t/db_dependent/api/v1/patrons_password_recovery.t 3. Modify OpacResetPasswordHostWhitelist system preference. Add "anything" to its value. 4. Send a POST request to /api/v1/public/atrons/password/recovery with parameters "email", "userid" and "custom_link" (custom_link value e.g. "https://anything/reset-password.pl?token={uuid}") 5. You should get a HTTP 201 response 6. Check your message_queue. Your password recovery email should now contain the link you provided in step 4 7. Repeat step 4 but this time use something else as custom_link value 8. You should get a response that this host name is not accepted Sponsored-by: Koha-Suomi Oy --- Koha/Patron/Password/Recovery.pm | 31 +++++++++ Koha/REST/V1/Patrons/Password.pm | 9 ++- api/v1/swagger/paths/patrons.json | 4 ++ ...ssword-recovery-custom-link-whitelist.perl | 11 ++++ installer/data/mysql/sysprefs.sql | 1 + .../en/modules/admin/preferences/opac.pref | 5 ++ .../api/v1/patrons_password_recovery.t | 66 ++++++++++++++++++- 7 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl diff --git a/Koha/Patron/Password/Recovery.pm b/Koha/Patron/Password/Recovery.pm index 0d09923bf1..4a71d68595 100644 --- a/Koha/Patron/Password/Recovery.pm +++ b/Koha/Patron/Password/Recovery.pm @@ -22,6 +22,8 @@ use C4::Context; use C4::Letters; use Crypt::Eksblowfish::Bcrypt qw(en_base64); +use Koha::Exceptions; + use vars qw(@ISA @EXPORT); BEGIN { @@ -98,12 +100,19 @@ sub GetValidLinkInfo { It creates an email using the templates and sends it to the user, using the specified email + Parameters: + C<$borrower> Koha::Patron + C<$userEmail> to_address (the one specified in the request) + C<$update> Update existing password recovery request + C<$params> Additional parameters as follows: + url: Custom password reset link (third party integrations) =cut sub SendPasswordRecoveryEmail { my $borrower = shift; # Koha::Patron my $userEmail = shift; #to_address (the one specified in the request) my $update = shift; + my $params = shift; my $schema = Koha::Database->new->schema; @@ -138,6 +147,28 @@ sub SendPasswordRecoveryEmail { my $uuidLink = $opacbase . "/cgi-bin/koha/opac-password-recovery.pl?uniqueKey=$uuid_str"; + # Custom $uuidLink. This feature is used for external integrations that handle + # password resetting inside the system via e.g. Koha's REST API. + if (defined $params->{url}) { + # Make sure host name exists in whitelist + my $url = $params->{url}; + my $whitelist = C4::Context->preference( + 'OpacResetPasswordHostWhitelist' + ); + my @allowed_hosts = split /[,\s\|]/, $whitelist if defined $whitelist; + foreach my $allowed_host (@allowed_hosts) { + if ($url =~ /^https?:\/\/$allowed_host/) { + $url =~ s/{uuid}/$uuid_str/gi; + $uuidLink = $params->{url} = $url; + last; + } + } + Koha::Exceptions::WrongParameter->throw( error => 'System preference' + . ' OpacResetPasswordHostWhitelist does' + . " not contain the requested host of '$url'" + ) if $uuidLink ne $params->{url}; + } + # prepare the email my $letter = C4::Letters::GetPreparedLetter( module => 'members', diff --git a/Koha/REST/V1/Patrons/Password.pm b/Koha/REST/V1/Patrons/Password.pm index fa94650f02..8703b72d5a 100644 --- a/Koha/REST/V1/Patrons/Password.pm +++ b/Koha/REST/V1/Patrons/Password.pm @@ -79,9 +79,13 @@ sub recovery { ); } + my $link = $body->{'custom_link'}; + my $resend = ValidateBorrowernumber($patron->borrowernumber); - SendPasswordRecoveryEmail($patron, $patron->email, $resend); + SendPasswordRecoveryEmail($patron, $patron->email, $resend, { + url => $link + }); return $c->render(status => 201, openapi => { status => 1, @@ -96,6 +100,9 @@ sub recovery { if ($_->isa('Koha::Exceptions::BadParameter')) { return $c->render(status => 400, openapi => { error => $_->error }); } + elsif ($_->isa('Koha::Exceptions::WrongParameter')) { + return $c->render(status => 400, openapi => { error => $_->error }); + } elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) { return $c->render(status => 404, openapi => { error => $_->error }); } diff --git a/api/v1/swagger/paths/patrons.json b/api/v1/swagger/paths/patrons.json index d5a44230d5..eca6dbfd73 100644 --- a/api/v1/swagger/paths/patrons.json +++ b/api/v1/swagger/paths/patrons.json @@ -726,6 +726,10 @@ "email": { "description": "Patron's email (required)", "type": "string" + }, + "complete_url": { + "description": "The location where patron will complete their password recovery. This location will be added into the password recovery email. To avoid malicious use, whitelist certain hosts in OpacResetPasswordHostWhitelist system preference and forbid all other custom links. Use {uuid} for adding the password recovery uuid into your custom link. It will be replaced by the actual uuid in the email.", + "type": "string" } }, "required": ["email"] diff --git a/installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl b/installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl new file mode 100644 index 0000000000..fc1fe37519 --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl @@ -0,0 +1,11 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + + $dbh->do( + "INSERT INTO systempreferences (variable, value, options, explanation, type) VALUES ('OpacResetPasswordHostWhitelist', '', '', 'Whitelist external host names for password reset in third party service. Separate list by whitespace, comma or |', 'free')" + ); + + # Always end with this (adjust the bug info) + SetVersion( $DBversion ); + print "Upgrade to $DBversion done (Bug 19133 - Password recovery via REST API - whitelist custom links)\n"; +} diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 75b04e2fd0..73a155bb21 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -415,6 +415,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'), ('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|null','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'), ('OpacResetPassword','0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'), +('OpacResetPasswordHostWhitelist','','','Whitelist external host names for password reset in third party service. Separate list by whitespace, comma or |','free'), ('OPACResultsLibrary', 'homebranch', 'homebranch|holdingbranch', 'Defines whether the OPAC displays the holding or home branch in search results when using XSLT', 'Choice'), ('OPACResultsSidebar','','70|10','Define HTML to be included on the search results page, underneath the facets sidebar','Textarea'), ('OPACSearchForTitleIn','
  • Other Libraries (WorldCat)
  • \n
  • Other Databases (Google Scholar)
  • \n
  • Online Stores (Bookfinder.com)
  • \n
  • Open Library (openlibrary.org)
  • ','70|10','Enter the HTML that will appear in the \'Search for this title in\' box on the detail page in the OPAC. Enter {TITLE}, {AUTHOR}, or {ISBN} in place of their respective variables in the URL. Leave blank to disable \'More Searches\' menu.','Textarea'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref index 8d00e78155..d90309a3ce 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -390,6 +390,11 @@ OPAC: yes: "allowed" no: "not allowed" - " to recover their password via e-mail in the OPAC" + - + - "Allow the following " + - pref: OpacResetPasswordHostWhitelist + type: textarea + - " host names of third party services to be used as external password reset locations. Separate list elements by new line, whitespace, comma or |." - - pref: OPACPatronDetails choices: diff --git a/t/db_dependent/api/v1/patrons_password_recovery.t b/t/db_dependent/api/v1/patrons_password_recovery.t index d59979c810..3a1c57614f 100644 --- a/t/db_dependent/api/v1/patrons_password_recovery.t +++ b/t/db_dependent/api/v1/patrons_password_recovery.t @@ -43,7 +43,7 @@ my $remote_address = '127.0.0.1'; my $t = Test::Mojo->new('Koha::REST::V1'); subtest 'recovery() tests' => sub { - plan tests => 29; + plan tests => 31; $schema->storage->txn_begin; @@ -143,6 +143,18 @@ subtest 'recovery() tests' => sub { 'Password modification request found in database' ); + my $notice_content = Koha::Notice::Messages->search({ + borrowernumber => $patron->borrowernumber, + letter_code => 'PASSWORD_RESET', + message_transport_type => 'email' + }, { + order_by => { '-desc' => 'message_id' } + })->next->content; + like($notice_content, + qr/cgi-bin\/koha\/opac-password-recovery\.pl\?uniqueKey=/, + 'Found Koha OPAC link in email' + ); + $tx = $t->ua->build_tx(POST => $url => json => { email => $patron->email, userid => $patron->userid @@ -181,6 +193,58 @@ subtest 'recovery() tests' => sub { })->count; is($notice, 3, 'Found password reset letters in message queue.'); + subtest 'custom reset link' => sub { + plan tests => 5; + + t::lib::Mocks::mock_preference( + 'OpacResetPasswordHostWhitelist', '' + ); + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://notallowed' + }); + $tx->req->cookies({name => 'CGISESSID', value => $session->id}); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(400); + + t::lib::Mocks::mock_preference( + 'OpacResetPasswordHostWhitelist', 'allowed' + ); + + $tx = $t->ua->build_tx(POST => $url => json => { + email => $patron->email, + userid => $patron->userid, + custom_link => 'https://allowed/reset-password.pl?uniqueKey={uuid}' + }); + $tx->req->cookies({name => 'CGISESSID', value => $session->id}); + $tx->req->env({REMOTE_ADDR => '127.0.0.1'}); + $t->request_ok($tx) + ->status_is(201); + + my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery'); + my $uuid = quotemeta $rs->search({ + borrowernumber => $patron->borrowernumber + }, { + order_by => { '-desc' => 'valid_until' } + })->next->uuid; + my $notice_to_external_service = Koha::Notice::Messages->search({ + borrowernumber => $patron->borrowernumber, + letter_code => 'PASSWORD_RESET', + message_transport_type => 'email' + }, { + order_by => { '-desc' => 'message_id' } + })->next; + my $content = $notice_to_external_service->content; + like( + $content, + qr/https:\/\/allowed\/reset-password\.pl\?uniqueKey=$uuid/, + 'Found custom link in email' + ); + }; + $schema->storage->txn_rollback; }; -- 2.17.1