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

(-)a/Koha/Token.pm (-14 / +33 lines)
Lines 34-42 Koha::Token - Tokenizer Link Here
34
        type => 'CSRF', id => $id, secret => $secret,
34
        type => 'CSRF', id => $id, secret => $secret,
35
    });
35
    });
36
36
37
    # or check a CSRF token
37
    # generate/check CSRF token with defaults and session id
38
    my $csrf_token = $tokenizer->generate_csrf({ session_id => $x });
38
    my $result = $tokenizer->check_csrf({
39
    my $result = $tokenizer->check_csrf({
39
        id => $id, secret => $secret, token => $token,
40
        session_id => $x, token => $token,
40
    });
41
    });
41
42
42
=head1 DESCRIPTION
43
=head1 DESCRIPTION
Lines 74-80 sub new { Link Here
74
75
75
    my $token = $tokenizer->generate({ length => 20 });
76
    my $token = $tokenizer->generate({ length => 20 });
76
    my $csrf_token = $tokenizer->generate({
77
    my $csrf_token = $tokenizer->generate({
77
        type => 'CSRF', id => $id,
78
        type => 'CSRF', id => $id, secret => $secret,
78
    });
79
    });
79
80
80
    Generate several types of tokens. Now includes CSRF.
81
    Generate several types of tokens. Now includes CSRF.
Lines 85-94 sub new { Link Here
85
sub generate {
86
sub generate {
86
    my ( $self, $params ) = @_;
87
    my ( $self, $params ) = @_;
87
    if( $params->{type} && $params->{type} eq 'CSRF' ) {
88
    if( $params->{type} && $params->{type} eq 'CSRF' ) {
88
        $self->{lasttoken} = _gen_csrf( {
89
        $self->{lasttoken} = _gen_csrf( $params );
89
            id     => Encode::encode( 'UTF-8', C4::Context->userenv->{id} . $params->{id} ),
90
            secret => md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) ),
91
        });
92
    } else {
90
    } else {
93
        $self->{lasttoken} = _gen_rand( $params );
91
        $self->{lasttoken} = _gen_rand( $params );
94
    }
92
    }
Lines 97-109 sub generate { Link Here
97
95
98
=head2 generate_csrf
96
=head2 generate_csrf
99
97
100
    Shortcut for: generate({ type => 'CSRF', ... })
98
    Like: generate({ type => 'CSRF', ... })
99
    Note: id defaults to userid from context, secret to database password.
100
    session_id is mandatory; it is combined with id.
101
101
102
=cut
102
=cut
103
103
104
sub generate_csrf {
104
sub generate_csrf {
105
    my ( $self, $params ) = @_;
105
    my ( $self, $params ) = @_;
106
    return unless $params->{id};
106
    return if !$params->{session_id};
107
    $params = _add_default_csrf_params( $params );
107
    return $self->generate({ %$params, type => 'CSRF' });
108
    return $self->generate({ %$params, type => 'CSRF' });
108
}
109
}
109
110
Lines 128-144 sub check { Link Here
128
129
129
=head2 check_csrf
130
=head2 check_csrf
130
131
131
    Shortcut for: check({ type => 'CSRF', ... })
132
    Like: check({ type => 'CSRF', ... })
133
    Note: id defaults to userid from context, secret to database password.
134
    session_id is mandatory; it is combined with id.
132
135
133
=cut
136
=cut
134
137
135
sub check_csrf {
138
sub check_csrf {
136
    my ( $self, $params ) = @_;
139
    my ( $self, $params ) = @_;
140
    return if !$params->{session_id};
141
    $params = _add_default_csrf_params( $params );
137
    return $self->check({ %$params, type => 'CSRF' });
142
    return $self->check({ %$params, type => 'CSRF' });
138
}
143
}
139
144
140
# --- Internal routines ---
145
# --- Internal routines ---
141
146
147
sub _add_default_csrf_params {
148
    my ( $params ) = @_;
149
    $params->{session_id} //= '';
150
    if( !$params->{id} ) {
151
        $params->{id} = Encode::encode( 'UTF-8', C4::Context->userenv->{id} . $params->{session_id} );
152
    } else {
153
        $params->{id} .= $params->{session_id};
154
    }
155
    $params->{id} //= Encode::encode( 'UTF-8', C4::Context->userenv->{id} );
156
    my $pw = C4::Context->config('pass');
157
    $params->{secret} //= md5_base64( Encode::encode( 'UTF-8', $pw ) ),
158
    return $params;
159
}
160
142
sub _gen_csrf {
161
sub _gen_csrf {
143
162
144
# Since WWW::CSRF::generate_csrf_token does not use the NonBlocking
163
# Since WWW::CSRF::generate_csrf_token does not use the NonBlocking
Lines 162-173 sub _gen_csrf { Link Here
162
181
163
sub _chk_csrf {
182
sub _chk_csrf {
164
    my ( $params ) = @_;
183
    my ( $params ) = @_;
165
    return if !$params->{id} || !$params->{token};
184
    return if !$params->{id} || !$params->{secret} || !$params->{token};
166
185
167
    my $id = Encode::encode( 'UTF-8', C4::Context->userenv->{id} . $params->{id} );
168
    my $secret = md5_base64( Encode::encode( 'UTF-8', C4::Context->config('pass') ) );
169
    my $csrf_status = WWW::CSRF::check_csrf_token(
186
    my $csrf_status = WWW::CSRF::check_csrf_token(
170
        $id, $secret, $params->{token},
187
        $params->{id},
188
        $params->{secret},
189
        $params->{token},
171
        { MaxAge => $params->{MaxAge} // ( CSRF_EXPIRY_HOURS * 3600 ) },
190
        { MaxAge => $params->{MaxAge} // ( CSRF_EXPIRY_HOURS * 3600 ) },
172
    );
191
    );
173
    return $csrf_status == WWW::CSRF::CSRF_OK();
192
    return $csrf_status == WWW::CSRF::CSRF_OK();
(-)a/t/Token.t (-13 / +12 lines)
Lines 33-45 is( length( $tokenizer->generate ), 1, "Generate without parameters" ); Link Here
33
my $token = $tokenizer->generate({ length => 20 });
33
my $token = $tokenizer->generate({ length => 20 });
34
is( length($token), 20, "Token $token has 20 chars" );
34
is( length($token), 20, "Token $token has 20 chars" );
35
35
36
my $id = $tokenizer->generate({ lyyGength => 8 });
36
my $id = $tokenizer->generate({ length => 8 });
37
my $csrftoken = $tokenizer->generate_csrf({ id => $id });
37
my $csrftoken = $tokenizer->generate_csrf({ session_id => $id });
38
isnt( length($csrftoken), 0, "Token $csrftoken should not be empty" );
38
isnt( length($csrftoken), 0, "Token $csrftoken should not be empty" );
39
39
40
is( $tokenizer->check, undef, "Check without any parameters" );
40
is( $tokenizer->check, undef, "Check without any parameters" );
41
my $result = $tokenizer->check_csrf({
41
my $result = $tokenizer->check_csrf({
42
    id => $id, token => $csrftoken,
42
    session_id => $id, token => $csrftoken,
43
});
43
});
44
is( $result, 1, "CSRF token verified" );
44
is( $result, 1, "CSRF token verified" );
45
45
Lines 51-75 isnt( $result, 1, "This token is no CSRF token" ); Link Here
51
# Test MaxAge parameter
51
# Test MaxAge parameter
52
my $age = 1; # 1 second
52
my $age = 1; # 1 second
53
$result = $tokenizer->check_csrf({
53
$result = $tokenizer->check_csrf({
54
    id => $id, token => $csrftoken, MaxAge => $age,
54
    session_id => $id, token => $csrftoken, MaxAge => $age,
55
});
55
});
56
is( $result, 1, "CSRF token still valid within one second" );
56
is( $result, 1, "CSRF token still valid within one second" );
57
usleep $age * 1000000 * 2; # micro (millionth) seconds + 100%
57
usleep $age * 1000000 * 2; # micro (millionth) seconds + 100%
58
$result = $tokenizer->check_csrf({
58
$result = $tokenizer->check_csrf({
59
    id => $id, token => $csrftoken, MaxAge => $age,
59
    session_id => $id, token => $csrftoken, MaxAge => $age,
60
});
60
});
61
isnt( $result, 1, "CSRF token expired after one second" );
61
isnt( $result, 1, "CSRF token expired after one second" );
62
62
63
subtest 'Same id (cookie CGISESSID) with an other logged in user' => sub {
63
subtest 'Same id (cookie CGISESSID) with an other logged in user' => sub {
64
    plan tests => 2;
64
    plan tests => 2;
65
    $csrftoken = $tokenizer->generate_csrf({ id => $id });
65
    $csrftoken = $tokenizer->generate_csrf({ session_id => $id });
66
    $result = $tokenizer->check_csrf({
66
    $result = $tokenizer->check_csrf({
67
        id => $id, token => $csrftoken,
67
        session_id => $id, token => $csrftoken,
68
    });
68
    });
69
    is( $result, 1, "CSRF token verified" );
69
    is( $result, 1, "CSRF token verified" );
70
    C4::Context->set_userenv(0,43,0,'firstname','surname', 'CPL', 'Library 1', 0, ', ');
70
    C4::Context->set_userenv(0,43,0,'firstname','surname', 'CPL', 'Library 1', 0, ', ');
71
    $result = $tokenizer->check_csrf({
71
    $result = $tokenizer->check_csrf({
72
        id => $id, token => $csrftoken,
72
        session_id => $id, token => $csrftoken,
73
    });
73
    });
74
    is( $result, '', "CSRF token is not verified if another logged in user is using the same id" );
74
    is( $result, '', "CSRF token is not verified if another logged in user is using the same id" );
75
};
75
};
Lines 77-91 subtest 'Same id (cookie CGISESSID) with an other logged in user' => sub { Link Here
77
subtest 'Same logged in user with another session (cookie CGISESSID)' => sub {
77
subtest 'Same logged in user with another session (cookie CGISESSID)' => sub {
78
    plan tests => 2;
78
    plan tests => 2;
79
    C4::Context->set_userenv(0,42,0,'firstname','surname', 'CPL', 'Library 1', 0, ', ');
79
    C4::Context->set_userenv(0,42,0,'firstname','surname', 'CPL', 'Library 1', 0, ', ');
80
    $csrftoken = $tokenizer->generate_csrf({ id => $id });
80
    $csrftoken = $tokenizer->generate_csrf({ session_id => $id });
81
    $result = $tokenizer->check_csrf({
81
    $result = $tokenizer->check_csrf({
82
        id => $id, token => $csrftoken,
82
        session_id => $id, token => $csrftoken,
83
    });
83
    });
84
    is( $result, 1, "CSRF token verified" );
84
    is( $result, 1, "CSRF token verified" );
85
    # Get another session id
85
    # Get another session id
86
    $id = $tokenizer->generate({ lyyGength => 8 });
86
    $id = $tokenizer->generate({ length => 8 });
87
    $result = $tokenizer->check_csrf({
87
    $result = $tokenizer->check_csrf({
88
        id => $id, token => $csrftoken,
88
        session_id => $id, token => $csrftoken,
89
    });
89
    });
90
    is( $result, '', "CSRF token is not verified if another session is used" );
90
    is( $result, '', "CSRF token is not verified if another session is used" );
91
};
91
};
92
- 

Return to bug 18124