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

(-)a/Koha/Patron/Password/Recovery.pm (+31 lines)
Lines 22-27 use C4::Context; Link Here
22
use C4::Letters;
22
use C4::Letters;
23
use Crypt::Eksblowfish::Bcrypt qw(en_base64);
23
use Crypt::Eksblowfish::Bcrypt qw(en_base64);
24
24
25
use Koha::Exceptions;
26
25
use vars qw(@ISA @EXPORT);
27
use vars qw(@ISA @EXPORT);
26
28
27
BEGIN {
29
BEGIN {
Lines 98-109 sub GetValidLinkInfo { Link Here
98
100
99
 It creates an email using the templates and sends it to the user, using the specified email
101
 It creates an email using the templates and sends it to the user, using the specified email
100
102
103
 Parameters:
104
 C<$borrower>   Koha::Patron
105
 C<$userEmail>  to_address (the one specified in the request)
106
 C<$update>     Update existing password recovery request
107
 C<$params>     Additional parameters as follows:
108
                  url:       Custom password reset link (third party integrations)
101
=cut
109
=cut
102
110
103
sub SendPasswordRecoveryEmail {
111
sub SendPasswordRecoveryEmail {
104
    my $borrower  = shift;    # Koha::Patron
112
    my $borrower  = shift;    # Koha::Patron
105
    my $userEmail = shift;    #to_address (the one specified in the request)
113
    my $userEmail = shift;    #to_address (the one specified in the request)
106
    my $update    = shift;
114
    my $update    = shift;
115
    my $params    = shift;
107
116
108
    my $schema = Koha::Database->new->schema;
117
    my $schema = Koha::Database->new->schema;
109
118
Lines 138-143 sub SendPasswordRecoveryEmail { Link Here
138
    my $uuidLink = $opacbase
147
    my $uuidLink = $opacbase
139
      . "/cgi-bin/koha/opac-password-recovery.pl?uniqueKey=$uuid_str";
148
      . "/cgi-bin/koha/opac-password-recovery.pl?uniqueKey=$uuid_str";
140
149
150
    # Custom $uuidLink. This feature is used for external integrations that handle
151
    # password resetting inside the system via e.g. Koha's REST API.
152
    if (defined $params->{url}) {
153
        # Make sure host name exists in whitelist
154
        my $url = $params->{url};
155
        my $whitelist = C4::Context->preference(
156
            'OpacResetPasswordHostWhitelist'
157
        );
158
        my @allowed_hosts = split /[,\s\|]/, $whitelist if defined $whitelist;
159
        foreach my $allowed_host (@allowed_hosts) {
160
           if ($url =~ /^https?:\/\/$allowed_host/) {
161
                $url =~ s/{uuid}/$uuid_str/gi;
162
                $uuidLink = $params->{url} = $url;
163
                last;
164
            }
165
        }
166
        Koha::Exceptions::WrongParameter->throw( error => 'System preference'
167
              . ' OpacResetPasswordHostWhitelist does'
168
              . " not contain the requested host of '$url'"
169
        ) if $uuidLink ne $params->{url};
170
    }
171
141
    # prepare the email
172
    # prepare the email
142
    my $letter = C4::Letters::GetPreparedLetter(
173
    my $letter = C4::Letters::GetPreparedLetter(
143
        module      => 'members',
174
        module      => 'members',
(-)a/Koha/REST/V1/Patron/Password.pm (-1 / +8 lines)
Lines 69-77 sub recovery { Link Here
69
            );
69
            );
70
        }
70
        }
71
71
72
        my $link = $body->{'custom_link'};
73
72
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
74
        my $resend = ValidateBorrowernumber($patron->borrowernumber);
73
75
74
        SendPasswordRecoveryEmail($patron, $patron->email, $resend);
76
        SendPasswordRecoveryEmail($patron, $patron->email, $resend, {
77
            url => $link
78
        });
75
79
76
        return $c->render(status => 201, openapi => {
80
        return $c->render(status => 201, openapi => {
77
            status => 1,
81
            status => 1,
Lines 85-90 sub recovery { Link Here
85
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
89
        elsif ($_->isa('Koha::Exceptions::Patron::NotFound')) {
86
            return $c->render(status => 404, openapi => { error => $_->error });
90
            return $c->render(status => 404, openapi => { error => $_->error });
87
        }
91
        }
92
        elsif ($_->isa('Koha::Exceptions::WrongParameter')) {
93
            return $c->render(status => 400, openapi => { error => $_->error });
94
        }
88
        Koha::Exceptions::rethrow_exception($_);
95
        Koha::Exceptions::rethrow_exception($_);
89
    };
96
    };
90
}
97
}
(-)a/api/v1/swagger/paths/patrons.json (+4 lines)
Lines 1173-1178 Link Here
1173
            "email": {
1173
            "email": {
1174
              "description": "Patron's email (required)",
1174
              "description": "Patron's email (required)",
1175
              "type": "string"
1175
              "type": "string"
1176
            },
1177
            "complete_url": {
1178
              "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.",
1179
              "type": "string"
1176
            }
1180
            }
1177
          },
1181
          },
1178
          "required": ["email"]
1182
          "required": ["email"]
(-)a/installer/data/mysql/atomicupdate/Bug-19133-Password-recovery-custom-link-whitelist.perl (+11 lines)
Line 0 Link Here
1
$DBversion = 'XXX';  # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
4
    $dbh->do(
5
        "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')"
6
    );
7
8
    # Always end with this (adjust the bug info)
9
    SetVersion( $DBversion );
10
    print "Upgrade to $DBversion done (Bug 19133 - Password recovery via REST API - whitelist custom links)\n";
11
}
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 373-378 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
373
('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'),
373
('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'),
374
('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|null','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'),
374
('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|null','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'),
375
('OpacResetPassword','0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'),
375
('OpacResetPassword','0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'),
376
('OpacResetPasswordHostWhitelist','','','Whitelist external host names for password reset in third party service. Separate list by whitespace, comma or |','free'),
376
('OPACResultsLibrary', 'homebranch', 'homebranch|holdingbranch', 'Defines whether the OPAC displays the holding or home branch in search results when using XSLT', 'Choice'),
377
('OPACResultsLibrary', 'homebranch', 'homebranch|holdingbranch', 'Defines whether the OPAC displays the holding or home branch in search results when using XSLT', 'Choice'),
377
('OPACResultsSidebar','','70|10','Define HTML to be included on the search results page, underneath the facets sidebar','Textarea'),
378
('OPACResultsSidebar','','70|10','Define HTML to be included on the search results page, underneath the facets sidebar','Textarea'),
378
('OPACSearchForTitleIn','<li><a  href=\"http://worldcat.org/search?q={TITLE}\" target=\"_blank\">Other Libraries (WorldCat)</a></li>\n<li><a href=\"http://www.scholar.google.com/scholar?q={TITLE}\" target=\"_blank\">Other Databases (Google Scholar)</a></li>\n<li><a href=\"http://www.bookfinder.com/search/?author={AUTHOR}&amp;title={TITLE}&amp;st=xl&amp;ac=qr\" target=\"_blank\">Online Stores (Bookfinder.com)</a></li>\n<li><a href=\"https://openlibrary.org/search/?author=({AUTHOR})&title=({TITLE})\" target=\"_blank\">Open Library (openlibrary.org)</a></li>','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'),
379
('OPACSearchForTitleIn','<li><a  href=\"http://worldcat.org/search?q={TITLE}\" target=\"_blank\">Other Libraries (WorldCat)</a></li>\n<li><a href=\"http://www.scholar.google.com/scholar?q={TITLE}\" target=\"_blank\">Other Databases (Google Scholar)</a></li>\n<li><a href=\"http://www.bookfinder.com/search/?author={AUTHOR}&amp;title={TITLE}&amp;st=xl&amp;ac=qr\" target=\"_blank\">Online Stores (Bookfinder.com)</a></li>\n<li><a href=\"https://openlibrary.org/search/?author=({AUTHOR})&title=({TITLE})\" target=\"_blank\">Open Library (openlibrary.org)</a></li>','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 (+5 lines)
Lines 378-383 OPAC: Link Here
378
                  no: "not allowed"
378
                  no: "not allowed"
379
            - " to recover their password via e-mail in the OPAC"
379
            - " to recover their password via e-mail in the OPAC"
380
        -
380
        -
381
            - "Allow the following "
382
            - pref: OpacResetPasswordHostWhitelist
383
              type: textarea
384
            - " host names of third party services to be used as external password reset locations. Separate list elements by new line, whitespace, comma or |."
385
        -
381
            - pref: OPACPatronDetails
386
            - pref: OPACPatronDetails
382
              choices:
387
              choices:
383
                  yes: Allow
388
                  yes: Allow
(-)a/t/db_dependent/api/v1/passwordrecovery.t (-2 / +65 lines)
Lines 44-50 my $remote_address = '127.0.0.1'; Link Here
44
my $t              = Test::Mojo->new('Koha::REST::V1');
44
my $t              = Test::Mojo->new('Koha::REST::V1');
45
45
46
subtest 'recovery() tests' => sub {
46
subtest 'recovery() tests' => sub {
47
    plan tests => 29;
47
    plan tests => 31;
48
48
49
    $schema->storage->txn_begin;
49
    $schema->storage->txn_begin;
50
50
Lines 144-149 subtest 'recovery() tests' => sub { Link Here
144
        'Password modification request found in database'
144
        'Password modification request found in database'
145
    );
145
    );
146
146
147
    my $notice_content = Koha::Notice::Messages->search({
148
        borrowernumber => $patron->borrowernumber,
149
        letter_code => 'PASSWORD_RESET',
150
        message_transport_type => 'email'
151
    }, {
152
        order_by => { '-desc' => 'message_id' }
153
    })->next->content;
154
    like($notice_content,
155
         qr/cgi-bin\/koha\/opac-password-recovery\.pl\?uniqueKey=/,
156
         'Found Koha OPAC link in email'
157
    );
158
147
    $tx = $t->ua->build_tx(POST => $url => json => {
159
    $tx = $t->ua->build_tx(POST => $url => json => {
148
        email      => $patron->email,
160
        email      => $patron->email,
149
        userid     => $patron->userid
161
        userid     => $patron->userid
Lines 182-187 subtest 'recovery() tests' => sub { Link Here
182
    })->count;
194
    })->count;
183
    is($notice, 3, 'Found password reset letters in message queue.');
195
    is($notice, 3, 'Found password reset letters in message queue.');
184
196
197
    subtest 'custom reset link' => sub {
198
        plan tests => 5;
199
200
        t::lib::Mocks::mock_preference(
201
            'OpacResetPasswordHostWhitelist', ''
202
        );
203
204
        $tx = $t->ua->build_tx(POST => $url => json => {
205
            email      => $patron->email,
206
            userid     => $patron->userid,
207
            custom_link => 'https://notallowed'
208
        });
209
        $tx->req->cookies({name => 'CGISESSID', value => $session->id});
210
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
211
        $t->request_ok($tx)
212
          ->status_is(400);
213
214
        t::lib::Mocks::mock_preference(
215
            'OpacResetPasswordHostWhitelist', 'allowed'
216
        );
217
218
        $tx = $t->ua->build_tx(POST => $url => json => {
219
            email      => $patron->email,
220
            userid     => $patron->userid,
221
            custom_link => 'https://allowed/reset-password.pl?uniqueKey={uuid}'
222
        });
223
        $tx->req->cookies({name => 'CGISESSID', value => $session->id});
224
        $tx->req->env({REMOTE_ADDR => '127.0.0.1'});
225
        $t->request_ok($tx)
226
          ->status_is(201);
227
228
        my $rs = Koha::Database->new->schema->resultset('BorrowerPasswordRecovery');
229
        my $uuid = quotemeta $rs->search({
230
            borrowernumber => $patron->borrowernumber
231
        }, {
232
            order_by => { '-desc' => 'valid_until' }
233
        })->next->uuid;
234
        my $notice_to_external_service = Koha::Notice::Messages->search({
235
            borrowernumber => $patron->borrowernumber,
236
            letter_code => 'PASSWORD_RESET',
237
            message_transport_type => 'email'
238
        }, {
239
            order_by => { '-desc' => 'message_id' }
240
        })->next;
241
        my $content = $notice_to_external_service->content;
242
        like(
243
             $content,
244
             qr/https:\/\/allowed\/reset-password\.pl\?uniqueKey=$uuid/,
245
             'Found custom link in email'
246
        );
247
    };
248
185
    $schema->storage->txn_rollback;
249
    $schema->storage->txn_rollback;
186
};
250
};
187
251
188
- 

Return to bug 19133