package Local::Dancer2::CSRF;
# port of Dancer2::Plugin::CSRF
# Dancer2::Plugin::CSRF doesn't work well with DP::Deferred which appends
# '?dpdid=.....' to form submission which breaks token validation, it also
# uses deprecated dsl keywords (eg request->dispatch_path)
# to use $self->dsl->debug, need to use private subs, or caller is
# ~/perl5/lib/perl5/Dancer2/Plugin.pm for debug output
# set log level to debug in development config to see _debug() output
use 5.26.0;
use Term::ANSIColor::Simple;
use Crypt::SaltedHash;
use Dancer2::Plugin;
use Data::Printer;
use Data::UUID;
my $HASHER = Crypt::SaltedHash->new( algorithm => 'SHA-1' ); # SHA1 is default anyway
my $UUID = Data::UUID->new();
plugin_keywords qw( get_csrf_token check_csrf_token );
has session_key_name => (
is => 'ro', # this plugin config session_key_name if set:
default => sub { $_[0]->config->{session_key_name} || 'csrf_token' }
);
# caller[1] is ~/[..]/Dancer2/Plugin.pm for DSL debug output, solution is to
# wrap private subs, then caller is this class
sub check_csrf_token { _check_csrf_token(@_) }
sub get_csrf_token { _get_csrf_token(@_) }
#===============================================================================
sub _get_csrf_token { # caller[1] is this class
my $self = shift;
my $app = $self->app; # p $app->session;
# my $dsl = $app->dsl; $dsl->debug("form_url:$form_url"); using _debug() now:
$self->_debug(__LINE__,'getting csrf token .....');
my $key_name = $self->session_key_name;
$self->_debug(__LINE__,"key_name:$key_name");
my $session = $app->session; # p $session;
# create a session $key_name => value pair if it doesn't exist:
if (! $session->read($key_name) ) {
$self->_debug(__LINE__,"session lacks $key_name key");
my $str = $UUID->create_str();
$self->_debug(__LINE__,"creating new $key_name:$str pair");
$session->write( $key_name => $str );
} # p $session;
else { $self->_debug(__LINE__,"session contains $key_name key") }
# generate a new token:
my $token = $self->_generate_token; # $dsl->debug("token:$token"); already done it
return $token;
}
# check whether submitted form token matches its expected value, then delete
# csrf key value from session to block back-button resubmission:
sub _check_csrf_token { # caller[1] is this class
my ($self, $form_token) = @_; # p $form_token;
$self->_debug(__LINE__,"checking csrf token .....");
# generate expected token (using same function as original form) for
# comparison with $form_token (return a failure if no session key):
my $expected_token = $self->_generate_token || return 0; # p $expected_token;
$self->_debug(__LINE__,"expected token:$expected_token");
$self->_debug(__LINE__,"received token:$form_token");
# clear csrf-token from session to prevent back-button resubmission:
my $key_name = $self->session_key_name; # p $key_name;
$self->_debug(__LINE__,"deleting session key $key_name");
$self->app->session->delete($key_name);
# returns boolen truth:
return $form_token eq $expected_token;
}
# generate a salted-hash token using session $key_name value & form_url value:
sub _generate_token { # caller[1] is this class
my $self = shift;
my $app = $self->app; # p $app->session; p $app->environment;
$self->_debug(__LINE__,'creating new token');
my $session_key_name = $self->session_key_name; # p $session_key_name;
$self->_debug(__LINE__,"session_key_name:$session_key_name");
my $session_key_value = $app->session->read($session_key_name) || return 0;
$self->_debug(__LINE__,"session_key_value:$session_key_value"); # p $session_key_value;
=begin
p $app->request->path; # eg "/upload"
p $app->request->request_uri; # eg "/upload?dpdid=59091261"
p $app->request->base; # URI::http object, stringifies to "http://localhost:3000/"
p $app->request->uri_base; # eg "http://localhost:3000"
=cut
my $request = $app->request;
# this needs to work for both initial get & associated post requests:
# my $form_url = do {
# ( my $path = $request->path ) =~ s{^/}{}; # p $path;
# $request->base . $path;
# };
my $form_url = $request->uri_base . $request->path; # p $form_url;
$self->_debug(__LINE__,"form_url:$form_url");
my $token = $HASHER->add($session_key_value, $form_url)->generate(); # p $token;
$HASHER->clear();
$self->_debug(__LINE__,"created new token:$token");
return $token;
}
sub _debug {
my ($self, $line, $str) = @_; # p $str;
my $app = $self->app; # p $app->logger_engine->log_level;
my @caller = caller(1); # p @caller;
# require development env with log level core or debug:
return unless $app->environment eq 'development'
&& grep { $app->logger_engine->log_level eq $_ } qw(core debug);
say color($caller[0] . ' line #' . $line)->red;
say color($str)->green;
}
1;