Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1050 lines
37 KiB

package Anvil::Tools::Account;
#
# This module contains methods used to handle user accounts, logging in and out.
#
use strict;
use warnings;
use Digest::SHA qw(sha256_base64 sha384_base64 sha512_base64);
use Scalar::Util qw(weaken isweak);
our $VERSION = "3.0.0";
my $THIS_FILE = "Account.pm";
### Methods;
# encrypt_password
# login
# logout
# read_cookies
# read_details
# validate_password
# _build_cookie_hash
# _write_cookies
=pod
=encoding utf8
=head1 NAME
Anvil::Tools::Account
Provides all methods related to user management and log in/out features.
=head1 SYNOPSIS
use Anvil::Tools;
# Get a common object handle on all Anvil::Tools modules.
my $anvil = Anvil::Tools->new();
# Access to methods using '$anvil->Account->X'.
=head1 METHODS
Methods in the core module;
=cut
sub new
{
my $class = shift;
my $self = {};
bless $self, $class;
return ($self);
}
# Get a handle on the Anvil::Tools object. I know that technically that is a sibling module, but it makes more
# sense in this case to think of it as a parent.
sub parent
{
my $self = shift;
my $parent = shift;
$self->{HANDLE}{TOOLS} = $parent if $parent;
# Defend against memory leads. See Scalar::Util'.
if (not isweak($self->{HANDLE}{TOOLS}))
{
weaken($self->{HANDLE}{TOOLS});
}
return ($self->{HANDLE}{TOOLS});
}
#############################################################################################################
# Public methods #
#############################################################################################################
=head2 encrypt_password
This takes a string (a new password from a user), generates a salt, appends the salt to the string and hashes that using C<< sys::password::algorithm >>, the re-hashes the string C<< sys::password::hash_count >> times. The default algorithm is 'sha512' and the default rehashing count is '500,000' times.
This method returns a hash reference with the following keys;
* user_password_hash: The final encrypted hash.
* user_salt: The salt created (or used) to generate the hash.
* user_algorithm: The algorithm used to compute the hash.
* user_hash_count: The number of re-encryptions of the initial hash.
If anything goes wrong, all four keys will have empty strings.
Parameters
=head3 algorithm (optional)
If set, the password will be encrypted using the given algoritm. Otherwise, c<< sys::password::algorithm >> is used. If that is not set, C<< sha256 >> is used.
=head3 hash_count (Optional, default 500000)
This controls how many times we re-encrypt the password hash. This is designed to slow down how quickly a brute-force attacker can test hashes. This should be a high enough number to take some time (~0.5 seconds) on a modern machine, but not so high that it noticeably slows down user login attempts.
If set to C<< 0 >>, no re-hashing will occur, but the initial hash still will.
=head3 password (required)
This is the password (string) to encrypt.
=head3 salt (optional)
This is the salt to use when hashing the password. If this is not passed, a new salt will be generated.
=cut
sub encrypt_password
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->encrypt_password()" }});
my $algorithm = defined $parameter->{algorithm} ? $parameter->{algorithm} : "";
my $hash_count = defined $parameter->{hash_count} ? $parameter->{hash_count} : "";
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $salt = defined $parameter->{salt} ? $parameter->{salt} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
algorithm => $algorithm,
hash_count => $hash_count,
password => $anvil->Log->is_secure($password),
salt => $salt,
}});
# We'll fill these out below if we succeed.
my $answer = {
user_password_hash => "",
user_salt => "",
user_hash_count => "",
user_algorithm => "",
};
# Make sure we got a string
if (not $password)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->encrypt_password()", parameter => "password" }});
return($answer);
}
my $user_password_hash = $password;
# Set the re-hash count, if not already set.
my $user_hash_count = $hash_count;
if ($user_hash_count eq "")
{
$user_hash_count = $anvil->data->{sys}{password}{hash_count} =~ /^\d+$/ ? $anvil->data->{sys}{password}{hash_count} : 500000;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
}
# Generate a salt.
my $user_salt = $salt;
if (not $user_salt)
{
$user_salt = $anvil->Get->_salt;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_salt => $user_salt }});
}
### TODO: Look at using/support bcrypt as the default algorithm. Needed RPMs are already in the el7 AN!Repo.
# We support sha256, sha384 and sha512, possible new ones later.
my $user_algorithm = $algorithm;
if (not $algorithm)
{
$user_algorithm = $anvil->data->{sys}{password}{algorithm} ? $anvil->data->{sys}{password}{algorithm} : "sha512";
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_algorithm => $user_algorithm }});
if ($user_algorithm eq "sha256" )
{
$user_password_hash = sha256_base64($user_password_hash.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$user_password_hash = sha256_base64($user_password_hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
}
}
elsif ($user_algorithm eq "sha384" )
{
$user_password_hash = sha384_base64($user_password_hash.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$user_password_hash = sha384_base64($user_password_hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
}
}
elsif ($user_algorithm eq "sha512" )
{
$user_password_hash = sha512_base64($user_password_hash.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$user_password_hash = sha512_base64($user_password_hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }});
}
}
else
{
# Bash algorith.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0171", variables => { user_algorithm => $user_algorithm }});
return($answer);
}
$answer = {
user_password_hash => $user_password_hash,
user_salt => $user_salt,
user_hash_count => $user_hash_count,
user_algorithm => $user_algorithm,
};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
'answer->user_password_hash' => $answer->{user_password_hash},
'answer->user_salt' => $answer->{user_salt},
'answer->user_hash_count' => $answer->{user_hash_count},
'answer->user_algorithm' => $answer->{user_algorithm},
}});
return($answer);
}
=head2 login
This checks to see if the CGI C<< username >> and C<< password >> passed in are for a valid user or not. If so, their details are loaded and C<< 0 >> is returned. If not, C<< 1 >> is returned.
This method takes no parameters.
=cut
sub login
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $password = $parameter->{password} // $anvil->data->{cgi}{password}{value};
my $username = $parameter->{username} // $anvil->data->{cgi}{username}{value};
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->login()" }});
if ((not $username) or (not $password))
{
# The user forgot something...
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }});
return(1);
}
my $query = "
SELECT
user_uuid,
user_password_hash,
user_salt,
user_algorithm,
user_hash_count
FROM
users
WHERE
user_algorithm != 'DELETED'
AND
user_name = ".$anvil->Database->quote($username)."
;";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }});
my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
results => $results,
count => $count,
}});
if (not $count)
{
# User not found.
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }});
return(1);
}
my $user_uuid = $results->[0]->[0];
my $user_password_hash = $results->[0]->[1];
my $user_salt = $results->[0]->[2];
my $user_algorithm = $results->[0]->[3];
my $user_hash_count = $results->[0]->[4];
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
user_uuid => $user_uuid,
user_password_hash => $user_password_hash,
user_salt => $user_salt,
user_algorithm => $user_algorithm,
user_hash_count => $user_hash_count,
}});
# Test the passed-in password.
my $test_password_answer = $anvil->Account->encrypt_password({
debug => 2,
password => $password,
salt => $user_salt,
algorithm => $user_algorithm,
hash_count => $user_hash_count,
});
my $test_password_hash = $test_password_answer->{user_password_hash};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { test_password_hash => $test_password_hash }});
if ($test_password_hash eq $user_password_hash)
{
# User passed a valid username/password. Create a session hash.
my ($session_hash, $session_salt) = $anvil->Account->_build_cookie_hash({
debug => $debug,
uuid => $user_uuid,
offset => 0,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
session_hash => $session_hash,
session_salt => $session_salt,
}});
if (not $session_hash)
{
# Something went wrong generating the session cookie, login failed.
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0028"}) }});
return(1);
}
else
{
my $session_uuid = $anvil->Database->insert_or_update_sessions({
debug => $debug,
session_user_uuid => $user_uuid,
session_salt => $session_salt,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { session_uuid => $session_uuid }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $username }});
$anvil->Account->_write_cookies({
debug => $debug,
hash => $session_hash,
uuid => $user_uuid,
});
}
}
else
{
# User DID NOT pass a valid username/password.
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0184", variables => {
user_agent => $ENV{HTTP_USER_AGENT} ? $ENV{HTTP_USER_AGENT} : "#!string!log_0185!#",
source_ip => $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : "#!string!log_0185!#",
user => $username,
}});
# Slow them down a bit...
sleep 5;
return(1);
}
return(0);
}
=head2 logout
This deletes the user's UUID and hash cookies, which effectively logs them out.
If there is no C<< user_uuid >>, this will return C<< 1 >>. Otherwise, C<< 0 >> is returned.
Parameters;
=head3 user_uuid (optional, default 'cookie::anvil_user_uuid')
This is the user to log out.
=head3 host_uuid (optional, default 'Get->host_uuid')
This is the host to log out of. This takes the special C<< all >> value which logs the user out of all hosts sessions.
=cut
sub logout
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->logout()" }});
# Delete the user's cookie data. Sending nothing to '_write_cookies' does this.
$anvil->Account->_write_cookies({debug => $debug});
my $user_uuid = defined $parameter->{user_uuid} ? $parameter->{user_uuid} : "";
my $host_uuid = defined $parameter->{host_uuid} ? $parameter->{host_uuid} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
user_uuid => $user_uuid,
host_uuid => $host_uuid,
}});
if (not $host_uuid)
{
$host_uuid = $anvil->Get->host_uuid;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_uuid => $host_uuid }});
}
if (($anvil->data->{cookie}{anvil_user_uuid}) && (not $user_uuid))
{
$user_uuid = $anvil->data->{cookie}{anvil_user_uuid};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_uuid => $user_uuid }});
}
# If I don't have a user UUID, we can't proceed.
if (not $user_uuid)
{
# User not found.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "error_0040"});
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0040"}) }});
return(1);
}
# If the host_uuid is 'all', we're logging out all sessions.
# Delete the user's session salt. We don't use Database->insert_or_update_sessions() to not
# complicate handling 'all' hosts.
my $query = "
UPDATE
sessions
SET
session_salt = '',
modified_date = ".$anvil->Database->quote($anvil->Database->refresh_timestamp)."
WHERE
session_user_uuid = ".$anvil->Database->quote($user_uuid)." ";
if ($host_uuid ne "all")
{
$query .= "
AND
session_host_uuid = ".$anvil->Database->quote($host_uuid)." ";
}
$query .= "
;";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }});
$anvil->Database->write({debug => $debug, query => $query, source => $THIS_FILE, line => __LINE__});
my $user = $anvil->data->{cgi}{username}{value} ? $anvil->data->{cgi}{username}{value} : "--";
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0198", variables => { user => $user }});
# Log that they're out
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0179"});
return(0);
}
=head2 read_cookies
This method (tries to) read the user's cookies to see if their session is valid. If so, it will read in their account details.
This method takes no parameters.
Return codes;
=head3 0
The cookies were read, the account was validated and the user's details were loaded.
=head3 1
No cookie was found or read. The user needs to log in
=head3 2
There was a problem reading the user's UUID (it wasn't found in the database), so the cookies were deleted (via C<< Account->logout() >>. The user needs to log back in.
=head3 3
There user's hash is invalid, it is probably expired. The user has been logged out and needs to log back in.
=cut
sub read_cookies
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->read_cookies()" }});
# Read in any cookies
if (defined $ENV{HTTP_COOKIE})
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "ENV{HTTP_COOKIE}" => $ENV{HTTP_COOKIE} }});
my @data = (split /; /, $ENV{HTTP_COOKIE});
foreach my $pair (@data)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { pair => $pair }});
my ($key, $value) = split/=/, $pair;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
key => $key,
value => $value,
}});
next if ((not defined $value) or ($value eq ""));
if ($key =~ /^anvil_/)
{
$anvil->data->{cookie}{$key} = $value;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cookie::${key}" => $anvil->data->{cookie}{$key} }});
}
}
}
# Did we read a cookie?
if ((not defined $anvil->data->{cookie}{anvil_user_uuid}) or (not $anvil->data->{cookie}{anvil_user_uuid}))
{
# No cookie read.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0177"});
return(1);
}
elsif (not defined $anvil->data->{cookie}{anvil_user_hash})
{
$anvil->data->{cookie}{anvil_user_hash} = "";
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"cookie::anvil_user_uuid" => $anvil->data->{cookie}{anvil_user_uuid},
"cookie::anvil_user_hash" => $anvil->data->{cookie}{anvil_user_hash},
}});
# Validate the cookie if there is a User UUID. Pick the random number up from the database.
my $query = "
SELECT
a.user_name,
b.session_salt
FROM
users a,
sessions b
WHERE
a.user_uuid = b.session_user_uuid
AND
b.session_user_uuid = ".$anvil->Database->quote($anvil->data->{cookie}{anvil_user_uuid})."
AND
b.session_host_uuid = ".$anvil->Database->quote($anvil->Get->host_uuid)."
;";
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0124", variables => { query => $query }});
my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
results => $results,
count => $count,
}});
if ($count < 1)
{
# The user in the cookie isn't in the database. The user was deleted?
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0215"});
$anvil->Account->logout();
# Record the error message for the user.
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0023"}) }});
# We're done.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0178", variables => { uuid => $anvil->data->{cookie}{anvil_user_uuid} }});
return(2);
}
# Read in their "rand" value
$anvil->data->{sys}{users}{user_name} = $results->[0]->[0];
$anvil->data->{sessions}{session_salt} = $results->[0]->[1];
$anvil->data->{sessions}{session_salt} = "" if not defined $anvil->data->{sessions}{session_salt};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"sys::users::user_name" => $anvil->data->{sys}{users}{user_name},
"sessions::session_salt" => $anvil->data->{sessions}{session_salt},
}});
# Generate a hash using today and yesterday's date.
my ($today_hash) = $anvil->Account->_build_cookie_hash({
debug => $debug,
uuid => $anvil->data->{cookie}{anvil_user_uuid},
salt => $anvil->data->{sessions}{session_salt},
offset => 0,
});
my ($yesterday_hash) = $anvil->Account->_build_cookie_hash({
debug => $debug,
uuid => $anvil->data->{cookie}{anvil_user_uuid},
salt => $anvil->data->{sessions}{session_salt},
offset => -86400,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
"s1:cookie::anvil_user_hash" => $anvil->data->{cookie}{anvil_user_hash},
"s2:today_hash" => $today_hash,
"s3:yesterday_hash" => $yesterday_hash,
}});
# See if either hash matches what the user has stored.
if ($anvil->data->{cookie}{anvil_user_hash} eq $today_hash)
{
# Valid hash, user can proceed.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0180"});
# Load the account details
$anvil->Account->read_details({debug => $debug});
}
elsif ($anvil->data->{cookie}{anvil_user_hash} eq $yesterday_hash)
{
# The hash was valid yesterday, so we'll update the cookie with today's hash and proceed
# (which also loads the user's details).
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0181"});
$anvil->Account->_write_cookies({
debug => $debug,
hash => $today_hash,
uuid => $anvil->data->{cookie}{anvil_user_uuid},
});
}
else
{
# The user's cookie is invalid, log the user out.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0215"});
$anvil->Account->logout();
# Record the error message for the user.
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0024"}) }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0182"});
return(3);
}
return(0);
}
=head2 read_details
This method takes a user uuid and, if the user is found, reads in the details and sets C<< sys::users::<column names> >>. If the user is found, C<< 1 >> is returned. If not, C<< 0 >> is returned.
Parameters;
=head3 user_uuid (optional)
This is the user UUID being searched for. If it is not set, C<< cookie::anvil_user_uuid >>
=cut
sub read_details
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->read_details()" }});
my $user_uuid = defined $parameter->{user_uuid} ? $parameter->{user_uuid} : $anvil->data->{cookie}{anvil_user_uuid};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_uuid => $user_uuid }});
if (not $anvil->Validate->uuid({uuid => $user_uuid}))
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "error_0025", variables => { uuid => $user_uuid }});
return(0);
}
my $query = "
SELECT
user_name,
user_password_hash,
user_salt,
user_algorithm,
user_hash_count,
user_language,
user_is_admin,
user_is_experienced,
user_is_trusted
FROM
users
WHERE
user_uuid = ".$anvil->Database->quote($user_uuid)."
;";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }});
my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
results => $results,
count => $count,
}});
if (not $count)
{
# User doesn't exist.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "error_0026", variables => { uuid => $user_uuid }});
return(0);
}
my $user_name = $results->[0]->[0];
my $user_password_hash = $results->[0]->[1];
my $user_salt = $results->[0]->[2];
my $user_algorithm = $results->[0]->[3];
my $user_hash_count = $results->[0]->[4];
my $user_language = $results->[0]->[5];
my $user_is_admin = $results->[0]->[6];
my $user_is_experienced = $results->[0]->[7];
my $user_is_trusted = $results->[0]->[8];
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
user_name => $user_name,
user_password_hash => $user_password_hash,
user_salt => $user_salt,
user_algorithm => $user_algorithm,
user_hash_count => $user_hash_count,
user_language => $user_language,
user_is_admin => $user_is_admin,
user_is_experienced => $user_is_experienced,
user_is_trusted => $user_is_trusted,
}});
$anvil->data->{sys}{users}{user_name} = $user_name;
$anvil->data->{sys}{users}{user_uuid} = $user_uuid;
$anvil->data->{sys}{users}{user_password_hash} = $user_password_hash,
$anvil->data->{sys}{users}{user_salt} = $user_salt,
$anvil->data->{sys}{users}{user_algorithm} = $user_algorithm,
$anvil->data->{sys}{users}{user_hash_count} = $user_hash_count,
$anvil->data->{sys}{users}{user_language} = $user_language,
$anvil->data->{sys}{users}{user_is_admin} = $user_is_admin,
$anvil->data->{sys}{users}{user_is_experienced} = $user_is_experienced,
$anvil->data->{sys}{users}{user_is_trusted} = $user_is_trusted,
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
'sys::users::user_name' => $anvil->data->{sys}{users}{user_name},
'sys::users::user_uuid' => $anvil->data->{sys}{users}{user_uuid},
'sys::users::user_password_hash' => $anvil->data->{sys}{users}{user_password_hash},
'sys::users::user_salt' => $anvil->data->{sys}{users}{user_salt},
'sys::users::user_algorithm' => $anvil->data->{sys}{users}{user_algorithm},
'sys::users::user_hash_count' => $anvil->data->{sys}{users}{user_hash_count},
'sys::users::user_language' => $anvil->data->{sys}{users}{user_language},
'sys::users::user_is_admin' => $anvil->data->{sys}{users}{user_is_admin},
'sys::users::user_is_experienced' => $anvil->data->{sys}{users}{user_is_experienced},
'sys::users::user_is_trusted' => $anvil->data->{sys}{users}{user_is_trusted},
}});
# Change the active language, if needed
if ($anvil->data->{sys}{users}{user_language})
{
# Switch to the user's language
$anvil->Words->language({set => $anvil->data->{sys}{users}{user_language}});
}
return(1);
}
=head2 validate_password
This method takes a user name and password and checks to see if the password matches.
If the password is wrong, or if the user isn't found, C<< 0 >> is returned. If the password matches, C<< 1 >> is returned.
Parameters;
=head3 password (required)
This is the password to test.
=head3 user (required)
This is the user whose password we're testing.
=cut
sub validate_password
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->validate_password()" }});
my $password = defined $parameter->{password} ? $parameter->{password} : "";
my $user = defined $parameter->{user} ? $parameter->{user} : "";
my $valid = 0;
my $hash = "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
password => $anvil->Log->is_secure($password),
user => $user,
}});
if (not $password)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->validate_password()", parameter => "password" }});
return($valid);
}
if (not $user)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->validate_password()", parameter => "user" }});
return($valid);
}
my $query = "
SELECT
user_password_hash,
user_salt,
user_algorithm,
user_hash_count
FROM
users
WHERE
user_name = ".$anvil->Database->quote($user)."
;";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }});
my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__});
my $count = @{$results};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
results => $results,
count => $count,
}});
if (not $count)
{
# User not found.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0172", variables => { user => $user }});
return($valid);
}
my $user_password_hash = $results->[0]->[0];
my $user_salt = $results->[0]->[1];
my $user_algorithm = $results->[0]->[2];
my $user_hash_count = $results->[0]->[3];
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
user_password_hash => $user_password_hash,
user_salt => $user_salt,
user_algorithm => $user_algorithm,
user_hash_count => $user_hash_count,
}});
if ($user_algorithm eq "sha256" )
{
$hash = sha256_base64($password.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$hash = sha256_base64($hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
}
}
elsif ($user_algorithm eq "sha384" )
{
$hash = sha384_base64($password.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$hash = sha384_base64($hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
}
}
elsif ($user_algorithm eq "sha512" )
{
$hash = sha512_base64($password.$user_salt);
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
if ($user_hash_count > 0)
{
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }});
for (1..$user_hash_count)
{
$hash = sha512_base64($hash);
}
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }});
}
}
else
{
# Bad algorith.
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0173", variables => { user_algorithm => $user_algorithm }});
return($valid);
}
# Test.
if ($hash eq $user_password_hash)
{
# Good password.
$valid = 1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { valid => $valid }});
}
return($valid);
}
# =head3
#
# Private Functions;
#
# =cut
#############################################################################################################
# Private functions #
#############################################################################################################
=head2 _build_cookie_hash
This takes a (user) UUID and offset (stated as seconds) and builds a hash approporiate for use in cookies (or a test hash to validate a read cookie hash). The resulting hash and the salt used to generate the hash are returned.
If there is a problem, C<< 0 >> will be returned for both the hash and salt.
Parameters;
=head3 offset (optional, default '0')
This is used to offset the date when generating the date part of the string to hash. It is passed as-is directly to C<< Get->date_and_time >>.
=head3 user_agent (optional, default 'HTTP_USER_AGENT' environment variable)
This is the user agent to use when generating the string to hash.
=head3 uuid (optional, default 'cookie::anvil_user_uuid')
This is the UUID to use when generating the string to hash. Generally it is the user's UUID.
=cut
sub _build_cookie_hash
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->_build_cookie_hash()" }});
my $offset = defined $parameter->{offset} ? $parameter->{offset} : 0;
my $user_agent = defined $parameter->{user_agent} ? $parameter->{user_agent} : $ENV{HTTP_USER_AGENT};
my $salt = defined $parameter->{salt} ? $parameter->{salt} : "";
my $uuid = defined $parameter->{uuid} ? $parameter->{uuid} : $anvil->data->{cookie}{anvil_user_uuid};
# I know I could do chained conditionals, but it gets hard to read.
$user_agent = "" if not defined $user_agent;
$uuid = "" if not defined $uuid;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
offset => $offset,
user_agent => $user_agent,
salt => $salt,
uuid => $uuid,
}});
if (not $anvil->Validate->uuid({uuid => $uuid}))
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->_build_cookie_hash()", parameter => "uuid" }});
return(0, 0);
}
if (not $user_agent)
{
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->_build_cookie_hash()", parameter => "user_agent" }});
return(0, 0);
}
my $date = $anvil->Get->date_and_time({date_only => 1, offset => $offset});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { date => $date }});
my $session_string = $uuid.":".$date.":".$user_agent;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { session_string => $session_string }});
if (not $salt)
{
$salt = $anvil->Get->_salt;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { salt => $salt }});
}
# Generate a hash, but unike normal passwords, we won't re-encrypt it.
my $answer = $anvil->Account->encrypt_password({
debug => $debug,
hash_count => 0,
password => $session_string,
salt => $salt,
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
hash => $answer->{user_password_hash},
salt => $answer->{user_salt},
}});
return($answer->{user_password_hash}, $answer->{user_salt});
}
=head2 _write_cookies
This sets (or clears) the user's cookies.
Parameters;
=head3 hash (optional)
This is the hash to use for the session. If it is blank, it will log the user out.
=head3 uuid (optional)
This is the UUID of the user. If it is blank, it will log the user out.
=cut
sub _write_cookies
{
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->_write_cookies()" }});
my $hash = defined $parameter->{hash} ? $parameter->{hash} : "";
my $uuid = defined $parameter->{uuid} ? $parameter->{uuid} : "";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => {
hash => $hash,
uuid => $uuid,
}});
# If we have a users ID, load the user details
if (($hash) && ($uuid))
{
# Write the cookies
print "Set-Cookie:anvil_user_uuid=".$uuid.";\n";
print "Set-Cookie:anvil_user_hash=".$hash.";\n";
# Load the user's details
$anvil->Account->read_details({
debug => $debug,
user_uuid => $uuid});
# Update the active language, if needed.
if ($anvil->data->{sys}{users}{user_language})
{
# Switch to the user's language
$anvil->Words->language({set => $anvil->data->{sys}{users}{user_language}});
}
}
else
{
print "Set-Cookie:anvil_user_uuid=; expires=-1d;\n";
print "Set-Cookie:anvil_user_hash=; expires=-1d;\n";
}
return(0);
}
1;