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

(-)a/Koha/Token.pm (+161 lines)
Line 0 Link Here
1
package Koha::Token;
2
3
# Created as wrapper for CSRF tokens, but designed for more general use
4
5
# Copyright 2016 Rijksmuseum
6
#
7
# This file is part of Koha.
8
#
9
# Koha is free software; you can redistribute it and/or modify it under the
10
# terms of the GNU General Public License as published by the Free Software
11
# Foundation; either version 3 of the License, or (at your option) any later
12
# version.
13
#
14
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License along
19
# with Koha; if not, write to the Free Software Foundation, Inc.,
20
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22
=head1 NAME
23
24
Koha::Token - Tokenizer
25
26
=head1 SYNOPSIS
27
28
    use Koha::Token;
29
    my $tokenizer = Koha::Token->new;
30
    my $token = $tokenizer->generate({ length => 20 });
31
32
    # safely generate a CSRF token (nonblocking)
33
    my $csrf_token = $tokenizer->generate({
34
        CSRF => 1, id => $id, secret => $secret,
35
    });
36
37
    # or check a CSRF token
38
    my $result = $tokenizer->check({
39
        CSRF => 1, id => $id, secret => $secret, token => $token,
40
    });
41
42
=head1 DESCRIPTION
43
44
    Designed for providing general tokens.
45
    Created due to the need for a nonblocking call to Bytes::Random::Secure
46
    when generating a CSRF token.
47
48
=cut
49
50
use Modern::Perl;
51
use base qw(Class::Accessor);
52
use constant HMAC_SHA1_LENGTH => 20;
53
54
=head1 METHODS
55
56
=head2 new
57
58
    Create object (via Class::Accessor).
59
60
=cut
61
62
sub new {
63
    my ( $class ) = @_;
64
    return $class->SUPER::new();
65
}
66
67
=head2 generate
68
69
    my $token = $tokenizer->generate({ length => 20 });
70
    my $csrf_token = $tokenizer->generate({
71
        CSRF => 1, id => $id, secret => $secret,
72
    });
73
74
    Generate several types of tokens. Now includes CSRF.
75
    Room for future extension.
76
77
=cut
78
79
sub generate {
80
    my ( $self, $params ) = @_;
81
    if( $params->{CSRF} ) {
82
        $self->{lasttoken} = _gen_csrf( $params );
83
    } else {
84
        $self->{lasttoken} = _gen_rand( $params );
85
    }
86
    return $self->{lasttoken};
87
}
88
89
=head2 check
90
91
    my $result = $tokenizer->check({
92
        CSRF => 1, id => $id, secret => $secret, token => $token,
93
    });
94
95
    Check several types of tokens. Now includes CSRF.
96
    Room for future extension.
97
98
=cut
99
100
sub check {
101
    my ( $self, $params ) = @_;
102
    if( $params->{CSRF} ) {
103
        return _chk_csrf( $params );
104
    }
105
    return;
106
}
107
108
# --- Internal routines ---
109
110
sub _gen_csrf {
111
112
# Since WWW::CSRF::generate_csrf_token does not use the NonBlocking
113
# parameter of Bytes::Random::Secure, we are passing random bytes from
114
# a non blocking source to WWW::CSRF via its Random parameter.
115
116
    my ( $params ) = @_;
117
    return if !$params->{id} || !$params->{secret};
118
119
    require Bytes::Random::Secure;
120
    require WWW::CSRF;
121
122
    my $randomizer = Bytes::Random::Secure->new( NonBlocking => 1 );
123
        # this is most fundamental: do not use /dev/random since it is
124
        # blocking, but use /dev/urandom !
125
    my $random = $randomizer->bytes( HMAC_SHA1_LENGTH );
126
    my $token = WWW::CSRF::generate_csrf_token(
127
        $params->{id}, $params->{secret}, { Random => $random },
128
    );
129
130
    return $token;
131
}
132
133
sub _chk_csrf {
134
    my ( $params ) = @_;
135
    return if !$params->{id} || !$params->{secret} || !$params->{token};
136
137
    require WWW::CSRF;
138
    my $csrf_status = WWW::CSRF::check_csrf_token(
139
        $params->{id},
140
        $params->{secret},
141
        $params->{token},
142
    );
143
    return $csrf_status == WWW::CSRF::CSRF_OK();
144
}
145
146
sub _gen_rand {
147
    my ( $params ) = @_;
148
    my $length = $params->{length} || 1;
149
    $length = 1 unless $length > 0;
150
151
    require String::Random;
152
    return String::Random::random_string( '.' x $length );
153
}
154
155
=head1 AUTHOR
156
157
    Marcel de Rooy, Rijksmuseum Amsterdam, The Netherlands
158
159
=cut
160
161
1;
(-)a/opac/opac-memberentry.pl (-6 / +23 lines)
Lines 20-26 use Modern::Perl; Link Here
20
use CGI qw ( -utf8 );
20
use CGI qw ( -utf8 );
21
use Digest::MD5 qw( md5_base64 md5_hex );
21
use Digest::MD5 qw( md5_base64 md5_hex );
22
use String::Random qw( random_string );
22
use String::Random qw( random_string );
23
use WWW::CSRF qw(generate_csrf_token check_csrf_token CSRF_OK);
24
use HTML::Entities;
23
use HTML::Entities;
25
24
26
use C4::Auth;
25
use C4::Auth;
Lines 34-39 use C4::Scrubber; Link Here
34
use Email::Valid;
33
use Email::Valid;
35
use Koha::DateUtils;
34
use Koha::DateUtils;
36
use Koha::Patron::Images;
35
use Koha::Patron::Images;
36
use Koha::Token;
37
37
38
my $cgi = new CGI;
38
my $cgi = new CGI;
39
my $dbh = C4::Context->dbh;
39
my $dbh = C4::Context->dbh;
Lines 182-189 if ( $action eq 'create' ) { Link Here
182
elsif ( $action eq 'update' ) {
182
elsif ( $action eq 'update' ) {
183
183
184
    my $borrower = GetMember( borrowernumber => $borrowernumber );
184
    my $borrower = GetMember( borrowernumber => $borrowernumber );
185
    my $csrf_status = check_csrf_token($borrower->{userid}, md5_base64(C4::Context->config('pass')), scalar $cgi->param('csrf_token'));
185
    die "Wrong CSRF token"
186
    die "Wrong CSRF token" unless ($csrf_status == CSRF_OK);
186
        unless Koha::Token->new->check({
187
            CSRF   => 1,
188
            id     => $borrower->{userid},
189
            secret => md5_base64( C4::Context->config('pass') ),
190
            token  => scalar $cgi->param('csrf_token'),
191
        });
187
192
188
    my %borrower = ParseCgiForBorrower($cgi);
193
    my %borrower = ParseCgiForBorrower($cgi);
189
194
Lines 197-203 elsif ( $action eq 'update' ) { Link Here
197
            empty_mandatory_fields => \@empty_mandatory_fields,
202
            empty_mandatory_fields => \@empty_mandatory_fields,
198
            invalid_form_fields    => $invalidformfields,
203
            invalid_form_fields    => $invalidformfields,
199
            borrower               => \%borrower,
204
            borrower               => \%borrower,
200
            csrf_token             => generate_csrf_token($borrower->{userid}, md5_base64(C4::Context->config('pass'))),
205
            csrf_token             => Koha::Token->new->generate({
206
                CSRF   => 1,
207
                id     => $borrower->{userid},
208
                secret => md5_base64( C4::Context->config('pass') ),
209
            }),
201
        );
210
        );
202
211
203
        $template->param( action => 'edit' );
212
        $template->param( action => 'edit' );
Lines 229-235 elsif ( $action eq 'update' ) { Link Here
229
                action => 'edit',
238
                action => 'edit',
230
                nochanges => 1,
239
                nochanges => 1,
231
                borrower => GetMember( borrowernumber => $borrowernumber ),
240
                borrower => GetMember( borrowernumber => $borrowernumber ),
232
                csrf_token => generate_csrf_token($borrower->{userid}, md5_base64(C4::Context->config('pass')))
241
                csrf_token => Koha::Token->new->generate({
242
                    CSRF   => 1,
243
                    id     => $borrower->{userid},
244
                    secret => md5_base64( C4::Context->config('pass') ),
245
                }),
233
            );
246
            );
234
        }
247
        }
235
    }
248
    }
Lines 249-255 elsif ( $action eq 'edit' ) { #Display logged in borrower's data Link Here
249
        borrower  => $borrower,
262
        borrower  => $borrower,
250
        guarantor => scalar Koha::Patrons->find($borrowernumber)->guarantor(),
263
        guarantor => scalar Koha::Patrons->find($borrowernumber)->guarantor(),
251
        hidden => GetHiddenFields( $mandatory, 'modification' ),
264
        hidden => GetHiddenFields( $mandatory, 'modification' ),
252
        csrf_token => generate_csrf_token($borrower->{userid}, md5_base64(C4::Context->config('pass')))
265
        csrf_token => Koha::Token->new->generate({
266
            CSRF   => 1,
267
            id     => $borrower->{userid},
268
            secret => md5_base64( C4::Context->config('pass') ),
269
        }),
253
    );
270
    );
254
271
255
    if (C4::Context->preference('OPACpatronimages')) {
272
    if (C4::Context->preference('OPACpatronimages')) {
(-)a/t/Token.t (-1 / +45 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# tests for Koha::Token
4
5
# Copyright 2016 Rijksmuseum
6
#
7
# This file is part of Koha.
8
#
9
# Koha is free software; you can redistribute it and/or modify it under the
10
# terms of the GNU General Public License as published by the Free Software
11
# Foundation; either version 3 of the License, or (at your option) any later
12
# version.
13
#
14
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License along
19
# with Koha; if not, write to the Free Software Foundation, Inc.,
20
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22
use Modern::Perl;
23
use Test::More tests => 6;
24
use Koha::Token;
25
26
my $tokenizer = Koha::Token->new;
27
is( length( $tokenizer->generate ), 1, "Generate without parameters" );
28
my $token = $tokenizer->generate({ length => 20 });
29
is( length($token), 20, "Token $token has 20 chars" );
30
31
my $id = $tokenizer->generate({ length => 8 });
32
my $secr = $tokenizer->generate({ length => 32 });
33
my $csrftoken = $tokenizer->generate({ CSRF => 1, id => $id, secret => $secr });
34
isnt( length($csrftoken), 0, "Token $csrftoken should not be empty" );
35
36
is( $tokenizer->check, undef, "Check without any parameters" );
37
my $result = $tokenizer->check({
38
    CSRF => 1, id => $id, secret => $secr, token => $csrftoken,
39
});
40
is( $result, 1, "CSRF token verified" );
41
42
$result = $tokenizer->check({
43
    CSRF => 1, id => $id, secret => $secr, token => $token,
44
});
45
isnt( $result, 1, "This token is no CSRF token" );

Return to bug 16929