Koha/Koha/Token.pm
Jonathan Druart 7d8b96803f
Bug 24545: Fix license statements
Bug 9978 should have fixed them all, but some were missing.
We want all the license statements part of Koha to be identical, and
using the GPLv3 statement.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
2020-02-24 13:31:26 +00:00

229 lines
6 KiB
Perl

package Koha::Token;
# Created as wrapper for CSRF tokens, but designed for more general use
# Copyright 2016 Rijksmuseum
#
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
=head1 NAME
Koha::Token - Tokenizer
=head1 SYNOPSIS
use Koha::Token;
my $tokenizer = Koha::Token->new;
my $token = $tokenizer->generate({ length => 20 });
# safely generate a CSRF token (nonblocking)
my $csrf_token = $tokenizer->generate({
type => 'CSRF', id => $id, secret => $secret,
});
# generate/check CSRF token with defaults and session id
my $csrf_token = $tokenizer->generate_csrf({ session_id => $x });
my $result = $tokenizer->check_csrf({
session_id => $x, token => $token,
});
=head1 DESCRIPTION
Designed for providing general tokens.
Created due to the need for a nonblocking call to Bytes::Random::Secure
when generating a CSRF token.
=cut
use Modern::Perl;
use Bytes::Random::Secure ();
use String::Random ();
use WWW::CSRF ();
use Digest::MD5 qw(md5_base64);
use Encode qw( encode );
use Koha::Exceptions::Token;
use base qw(Class::Accessor);
use constant HMAC_SHA1_LENGTH => 20;
use constant CSRF_EXPIRY_HOURS => 8; # 8 hours instead of 7 days..
=head1 METHODS
=head2 new
Create object (via Class::Accessor).
=cut
sub new {
my ( $class ) = @_;
return $class->SUPER::new();
}
=head2 generate
my $token = $tokenizer->generate({ length => 20 });
my $csrf_token = $tokenizer->generate({
type => 'CSRF', id => $id, secret => $secret,
});
Generate several types of tokens. Now includes CSRF.
For non-CSRF tokens an optional pattern parameter overrides length.
Room for future extension.
Pattern parameter could be write down using this subset of regular expressions:
\w Alphanumeric + "_".
\d Digits.
\W Printable characters other than those in \w.
\D Printable characters other than those in \d.
. Printable characters.
[] Character classes.
{} Repetition.
* Same as {0,}.
? Same as {0,1}.
+ Same as {1,}.
=cut
sub generate {
my ( $self, $params ) = @_;
if( $params->{type} && $params->{type} eq 'CSRF' ) {
$self->{lasttoken} = _gen_csrf( $params );
} else {
$self->{lasttoken} = _gen_rand( $params );
}
return $self->{lasttoken};
}
=head2 generate_csrf
Like: generate({ type => 'CSRF', ... })
Note: id defaults to userid from context, secret to database password.
session_id is mandatory; it is combined with id.
=cut
sub generate_csrf {
my ( $self, $params ) = @_;
return if !$params->{session_id};
$params = _add_default_csrf_params( $params );
return $self->generate({ %$params, type => 'CSRF' });
}
=head2 check
my $result = $tokenizer->check({
type => 'CSRF', id => $id, token => $token,
});
Check several types of tokens. Now includes CSRF.
Room for future extension.
=cut
sub check {
my ( $self, $params ) = @_;
if( $params->{type} && $params->{type} eq 'CSRF' ) {
return _chk_csrf( $params );
}
return;
}
=head2 check_csrf
Like: check({ type => 'CSRF', ... })
Note: id defaults to userid from context, secret to database password.
session_id is mandatory; it is combined with id.
=cut
sub check_csrf {
my ( $self, $params ) = @_;
return if !$params->{session_id};
$params = _add_default_csrf_params( $params );
return $self->check({ %$params, type => 'CSRF' });
}
# --- Internal routines ---
sub _add_default_csrf_params {
my ( $params ) = @_;
$params->{session_id} //= '';
if( !$params->{id} ) {
$params->{id} = Encode::encode( 'UTF-8', C4::Context->userenv->{id} . $params->{session_id} );
} else {
$params->{id} .= $params->{session_id};
}
$params->{id} //= Encode::encode( 'UTF-8', C4::Context->userenv->{id} );
my $pw = C4::Context->config('pass');
$params->{secret} //= md5_base64( Encode::encode( 'UTF-8', $pw ) ),
return $params;
}
sub _gen_csrf {
# Since WWW::CSRF::generate_csrf_token does not use the NonBlocking
# parameter of Bytes::Random::Secure, we are passing random bytes from
# a non blocking source to WWW::CSRF via its Random parameter.
my ( $params ) = @_;
return if !$params->{id} || !$params->{secret};
my $randomizer = Bytes::Random::Secure->new( NonBlocking => 1 );
# this is most fundamental: do not use /dev/random since it is
# blocking, but use /dev/urandom !
my $random = $randomizer->bytes( HMAC_SHA1_LENGTH );
my $token = WWW::CSRF::generate_csrf_token(
$params->{id}, $params->{secret}, { Random => $random },
);
return $token;
}
sub _chk_csrf {
my ( $params ) = @_;
return if !$params->{id} || !$params->{secret} || !$params->{token};
my $csrf_status = WWW::CSRF::check_csrf_token(
$params->{id},
$params->{secret},
$params->{token},
{ MaxAge => $params->{MaxAge} // ( CSRF_EXPIRY_HOURS * 3600 ) },
);
return $csrf_status == WWW::CSRF::CSRF_OK();
}
sub _gen_rand {
my ( $params ) = @_;
my $length = $params->{length} || 1;
$length = 1 unless $length > 0;
my $pattern = $params->{pattern} // '.{'.$length.'}'; # pattern overrides length parameter
my $token;
eval {
$token = String::Random::random_regex( $pattern );
};
Koha::Exceptions::Token::BadPattern->throw($@) if $@;
return $token;
}
=head1 AUTHOR
Marcel de Rooy, Rijksmuseum Amsterdam, The Netherlands
=cut
1;