@@ -, +, @@ service to reset their password disabled Koha's OPAC userid and email as parameters, as well as custom_link to a password reset page in VF (instead of Koha OPAC's opac-password-recovery.pl) to its value. "email", "userid" and "custom_link" (custom_link value e.g. "https://anything/reset-password.pl?token={uuid}") the link you provided in step 4 --- Koha/Patron/Password/Recovery.pm | 31 ++++++++++ Koha/REST/V1/Patron/Password.pm | 9 ++- api/v1/swagger/paths/patrons.json | 4 ++ ...33-Password-recovery-custom-link-whitelist.perl | 11 ++++ installer/data/mysql/sysprefs.sql | 1 + .../prog/en/modules/admin/preferences/opac.pref | 5 ++ t/db_dependent/api/v1/passwordrecovery.t | 66 +++++++++++++++++++++- 7 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl --- a/Koha/Patron/Password/Recovery.pm +++ a/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', --- a/Koha/REST/V1/Patron/Password.pm +++ a/Koha/REST/V1/Patron/Password.pm @@ -69,9 +69,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, @@ -85,6 +89,9 @@ sub recovery { elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) { return $c->render(status => 404, openapi => { error => $_->error }); } + elsif ($_->isa('Koha::Exceptions::WrongParameter')) { + return $c->render(status => 400, openapi => { error => $_->error }); + } Koha::Exceptions::rethrow_exception($_); }; } --- a/api/v1/swagger/paths/patrons.json +++ a/api/v1/swagger/paths/patrons.json @@ -1173,6 +1173,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"] --- a/installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl +++ a/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"; +} --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -373,6 +373,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'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -378,6 +378,11 @@ OPAC: 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: yes: Allow --- a/t/db_dependent/api/v1/passwordrecovery.t +++ a/t/db_dependent/api/v1/passwordrecovery.t @@ -44,7 +44,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; @@ -144,6 +144,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 @@ -182,6 +194,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; }; --