Newer
Older
use AccountManager::Account;
use AccountManager::Account::Manager;
use AccountManager::Metadata;
use AccountManager::Service;
use AccountManager::Token;
## Defining parameters format
my $urn_or_url_regex = '(http(s?):\/\/|urn:)[^\\\$\*\"\'\`\^\|\<\>\n\s]+'
; ## Format de type URL HTTP ou URN
my $url_regex = 'http(s?):\/\/[^\\\$\*\"\'\`\^\|\<\>\n\s]+';
my $email_regex = '([\w\-\_\.\/\+\=\'\&]+|\".*\")\@[\w\-]+(\.[\w\-]+)+';
my $domains_regex = '[\w\.\-]+(,[\w\.\-]+)*';
my %format = (
## URL
#'attributeauthority' => $url_regex,
'sp_entityid' => $urn_or_url_regex,
);
my %actions = (
select_sp => 'req_select_sp',
account_wizard => 'req_account_wizard',
generate_token => 'req_generate_token',
home => 'req_home',
);
## New web request
sub new {
configuration => $args{configuration},
Log::Any::Adapter->set(
'File',
$self->{configuration}->{log}->{file},
log_level => $self->{configuration}->{log}->{level}
$self->{logger} = Log::Any->get_logger();
driver => $self->{configuration}->{database}->{type},
database => $self->{configuration}->{database}->{name},
host => $self->{configuration}->{database}->{host},
password => $self->{configuration}->{database}->{password},
username => $self->{configuration}->{database}->{user}
sub run {
my ($self) = @_;
$self->execute();
$self->respond();
}
## Execute a web request
sub execute {
# process input parameters
my %parameters = $self->{cgi}->Vars();
foreach my $parameter (keys %parameters) {
# cleanup
$parameters{$parameter} =~ s/\r//g; # remove &0D char
$parameters{$parameter} =~ s/\s+$//; # remove trailing spaces
$parameters{$parameter} =~ s/^\s+//; # remove leading spaces
# format check
if (defined $format{$parameter}
&& !ref($format{$parameter})) {
if ($parameters{$parameter} !~ /^$format{$parameter}$/) {
push @{ $self->{out}->{errors} }, "format_$parameter";
$self->{logger}->error(
"Incorrect parameter format : $parameter"
# If action_xx parameter is set, set action parameter with value xx
if ($parameter =~ /^action_(\w+)$/) {
$parameters{action} = $1;
}
# register needed parameters
email_address => $parameters{email_address},
style => $parameters{style},
sp_entityid => $parameters{sp_entityid},
authentication_token => $parameters{authentication_token}
};
my $action = $parameters{action} || 'home';
# initialize output parameters
$self->{out} = {
env => {
REMOTE_HOST => $ENV{REMOTE_HOST},
REMOTE_ADDR => $ENV{REMOTE_ADDR},
SCRIPT_NAME => $ENV{SCRIPT_NAME}
},
conf => {
accounts_validity_period => $self->{configuration}->{service}->{account_validity_period},
app_name => $self->{configuration}->{app}->{name},
app_url => $self->{configuration}->{app}->{url},
idp_scope => $self->{configuration}->{idp}->{scope},
idp_displayname => $self->{configuration}->{idp}->{displayname},
support_email => $self->{configuration}->{app}->{support_email},
version => $self->{configuration}->{app}->{version},
},
action => $action,
title => $self->{configuration}->{app}->{name}
};
# process requested action
if ($actions{$action}) {
my $method = $actions{$action};
$status = $self->$method();
push @{ $self->{out}->{errors} }, "unknown_action";
$self->{logger}->error( "Unknown action '$action'");
return 1;
}
## Return HTML content
sub respond {
INCLUDE_PATH => $self->{configuration}->{_}->{templates_dir}
## nobanner is used to do AJAX to get only pieces of HTML to load in the web client
if ($self->{in}->{style} && $self->{in}->{style} eq 'nobanner') {
$template = 'web/index-nobanner.tt2.html';
unless ($tt2->process($template, $self->{out}, \*STDOUT)) {
printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
$self->{logger}->errorf("Web parser error : %s", $tt2->error());
}
}
## Return the list of known SPs first
sub req_account_wizard {
$metadata = AccountManager::Metadata->new(
file => $self->{configuration}->{_}->{federation_metadata_file}
);
};
if ($EVAL_ERROR) {
$self->{logger}->error("Failed to load federation metadata: $EVAL_ERROR");
$self->{out}->{metadata} = $metadata->parse(type => 'sp');
$self->{out}->{subtitle} = 'Select your Service Provider';
return 1;
}
## Select a Service Provider and return metadata sctucture for the SP
## Sample URL : https://dev-edugain.renater.fr/accountmanager?action=select_sp&sp_entityid=http%3A%2F%2Fsp.lat.csc.fi
sub req_select_sp {
unless ($self->{in}->{sp_entityid}) {
push @{ $self->{out}->{errors} }, "missing_sp_entityid";
$self->{logger}->error("Missing parameter sp_entityid");
# Create a persistent service provider object
my $provider = AccountManager::Service->new(
db => $self->{db},
entityid => $self->{in}->{sp_entityid}
if ($provider->load(speculative => 1)) {
# already present in DB, nothing todo
} else {
# extract information from metadata
my $metadata;
eval {
$metadata = AccountManager::Metadata->new(
file => $self->{configuration}->{_}->{federation_metadata_file}
);
};
if ($EVAL_ERROR) {
push @{ $self->{out}->{errors} }, "internal";
$self->{logger}->error("Failed to load federation metadata: $EVAL_ERROR");
my $sps = $metadata->parse(id => $self->{in}->{sp_entityid});
if (!@$sps) {
push @{ $self->{out}->{errors} }, "no_such_entity";
$self->{logger}->errorf(
"No such SP '%s' in metadata", $self->{in}->{sp_entityid}
# complete persistent object
$provider->displayname($sp->{display_name});
$provider->contacts(uniq map { $_->{EmailAddress} } @{$sp->{contacts}})
# save in DB
unless ($provider->save()) {
$self->{logger}->error("Failed to save service provider object");
# replace metadata contacts from configuration contacts if defined
my $entity = $self->{in}->{sp_entityid};
my $contacts =
$self->{configuration}->{$entity}->{contacts} ||
$self->{configuration}->{service}->{contacts};
$provider->contacts(split(/, */, $contacts)) if $contacts;
$self->{out}->{provider} = $provider;
$self->{out}->{subtitle} = 'Select your Service Provider';
return 1;
}
## Generate an authentication token to validate an email address
## Sample call : dev-edugain.renater.fr/accountmanager?action=generate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&email_address=support%40renater.fr
sub req_generate_token {
unless ($self->{in}->{sp_entityid}) {
push @{ $self->{out}->{errors} }, "missing_sp_entityid";
$self->{logger}->error("Missing parameter sp_entityid");
push @{ $self->{out}->{errors} }, "missing_email_address";
$self->{logger}->error("Missing parameter email_address");
my $provider = AccountManager::Service->new(
db => $self->{db},
entityid => $self->{in}->{sp_entityid},
unless ($provider->load(speculative => 1)) {
push @{ $self->{out}->{errors} }, "no_such_entity";
$self->{logger}->errorf("No such SP '%s' in database", $self->{in}->{sp_entityid});
# replace metadata contacts from configuration contacts if defined
my $entity = $self->{in}->{sp_entityid};
my $contacts =
$self->{configuration}->{$entity}->{contacts} ||
$self->{configuration}->{service}->{contacts};
$provider->contacts(split(/, */, $contacts)) if $contacts;
## Check that email_address is a known contact for this SP
unless ($provider->is_contact($self->{in}->{email_address}))
$self->{logger}->errorf(
"Requested a token for %s for an unautorized address '%s'",
$self->{in}->{sp_entityid},
$self->{in}->{email_address}
# delete any previous token for the same email/service couple
my $old_token = AccountManager::Token->new(
email_address => $self->{in}->{email_address},
sp_entityid => $self->{in}->{sp_entityid}
if ($old_token->load(speculative => 1)) {
unless ($old_token->delete()) {
$self->{logger}->errorf(
"Failed to delete previous authentication token with ID %s",
my $validity_period =
$self->{configuration}->{_}->{tokens_validity_period};
my $token = AccountManager::Token->new(
db => $self->{db},
email_address => $self->{in}->{email_address},
sp_entityid => $self->{in}->{sp_entityid},
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
token => AccountManager::Tools::generate_token()
$self->{logger}->error("Failed to save authentication token");
$self->{out}->{email_address} = $self->{in}->{email_address};
$self->{out}->{sp_entityid} = $self->{in}->{sp_entityid};
$self->{out}->{subtitle} = 'Generate an authentication token';
my $sender = $self->{configuration}->{_}->{notice_from};
my $recipient = $self->{in}->{email_address};
my $sendmail = $self->{configuration}->{_}->{sendmail_path} ||
'/usr/sbin/sendmail';
open(my $handle, '|-', "$sendmail -f $sender $recipient") or do {
push @{ $self->{out}->{errors} }, "mail_notification_error";
$self->{logger}->errorf("Unable to run sendmail executable: %s", $ERRNO);
INCLUDE_PATH => $self->{configuration}->{_}->{templates_dir}
});
my $template = 'mail/send_authentication_token.tt2.eml';
my $data = {
env => {
REMOTE_HOST => $ENV{REMOTE_HOST},
REMOTE_ADDR => $ENV{REMOTE_ADDR},
},
conf => {
app_name => $self->{configuration}->{app}->{name},
app_url => $self->{configuration}->{app}->{url},
support_email => $self->{configuration}->{app}->{support_email},
},
from => $sender,
to => $recipient,
sp_entityid => $self->{in}->{sp_entityid},
authentication_token => $token->token(),
unless ($tt2->process($template, $data, $handle)) {
push @{ $self->{out}->{errors} }, "mail_notification_error";
$self->{logger}->errorf("Mail notification error: %s", $tt2->error());
$self->{logger}->infof(
"Token send to %s for sp_entityid=%s;token=%s",
$self->{in}->{email_address},
$self->{in}->{sp_entityid},
);
return 1;
}
## Validate an authentication token
## Test accounts get created
## Sample call : dev-edugain.renater.fr/accountmanager?action=validate_token&style=nobanner&sp_entityid=https%3A%2F%2Fsourcesup.cru.fr%2Fshibboleth&authentication_token=c1cfecb51ea40d39a695
sub req_validate_token {
unless ($self->{in}->{sp_entityid}) {
push @{ $self->{out}->{errors} }, "missing_sp_entityid";
$self->{logger}->error("Missing parameter sp_entityid");
push @{ $self->{out}->{errors} }, "missing_authentication_token";
$self->{logger}->error("Missing parameter authentication_token");
my $token = AccountManager::Token->new(
token => $self->{in}->{authentication_token}
);
if (! $token->load(speculative => 1)) {
push @{ $self->{out}->{errors} }, "wrong_token";
$self->{logger}->errorf(
"Failed to validate authentication token %s for sp_entityid %s",
$self->{in}->{authentication_token},
$self->{in}->{sp_entityid}
if (! $token->sp_entityid() eq $self->{in}->{sp_entityid}) {
push @{ $self->{out}->{errors} }, "wrong_token_for_sp";
$self->{logger}->errorf(
"Authentication token %s cannot be used for SP with entityid %s",
$self->{in}->{authentication_token},
$self->{in}->{sp_entityid}
}
## delete the token
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
);
}
## create test accounts
my $entity = $self->{in}->{sp_entityid};
my $profiles =
$self->{configuration}->{$entity}->{account_profiles} ||
$self->{configuration}->{service}->{account_profiles};
my $validity_period =
$self->{configuration}->{$entity}->{account_validity_period} ||
$self->{configuration}->{service}->{account_validity_period};
foreach my $profile (split(/, */, $profiles)) {
my $password = AccountManager::Tools::generate_password();
my $account = AccountManager::Account->new(
db => $self->{db},
profile => $profile,
sp_entityid => $entity,
scope => $self->{configuration}->{idp}->{scope},
password => $password,
password_hash => AccountManager::Tools::sha256_hash($password),
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(days => $validity_period)
push @{ $self->{out}->{errors} }, "accounts_creation_failed";
$self->{logger}->errorf(
"Failed to create test accounts for SP with entityid %s",
}
## Update simpleSAMLphp configuration to enable test accounts
my $accounts = AccountManager::Account::Manager->get_accounts(
db => $self->{db}
);
AccountManager::Tools::update_ssp_authsources(
$self->{configuration}->{_}->{templates_dir},
$self->{configuration}->{idp}->{accounts_file},
push @{ $self->{out}->{errors} }, "accounts_creation_failed";
$self->{logger}->errorf(
"Failed to create simpleSAMLphp configuration file: %s",
$EVAL_ERROR
$self->{logger}->infof(
"Token validated for sp_entityid=%s;token=%s",
$self->{in}->{sp_entityid},
$self->{in}->{authentication_token}
$self->{out}->{sp_entityid} = $self->{in}->{sp_entityid};
$self->{out}->{accounts} = \@accounts;
$self->{out}->{subtitle} = 'Complete Email Challenge';
return 1;
}
## Return the homepage of the service
sub req_home {
return 1;
}
1;