Newer
Older
use Template::Constants qw(:chomp);
use AccountManager::Account;
use AccountManager::Account::Manager;
use AccountManager::Metadata;
use AccountManager::Service;
use AccountManager::Token;
use AccountManager::L10N;
# Format de type URL HTTP ou URN
my $entity_id_pattern = qr{
^
(?:
https?://[\w.:/-]+
|
urn:[\w.:-]+
)
$
}x;
my %actions = (
start => 'req_start',
select_federation => 'req_select_federation',
select_sp => 'req_select_sp',
select_email => 'req_select_email',
complete_challenge => 'req_complete_challenge',
create_accounts => 'req_create_accounts',
download_accounts => 'req_download_accounts',
configuration => $args{configuration},
bless $self, $pkg;
if ($self->{configuration}->{logger}) {
Log::Any::Adapter->set(
'File',
$self->{configuration}->{logger}->{file},
log_level => $self->{configuration}->{logger}->{level}
} else {
warn "no logger in configuration, logging disabled\n";
$self->{logger} = Log::Any->get_logger();
$self->{cgi} = CGI->new();
my $lang =
$self->{cgi}->param('lang') ||
$self->{cgi}->cookie('lang');
$self->{lh} = AccountManager::L10N->get_handle($lang ? $lang: ());
if (!$self->{lh}) {
$self->{logger}->fatal("Unable to get suitable language handle");
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{mailer}) {
$self->{logger}->fatal(
"No mailer defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{idp}) {
$self->{logger}->fatal(
"No IDP defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{federations}) {
$self->{logger}->fatal(
"No federations defined in configuration, aborting"
);
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ 'internal' ]
}
);
}
if (!$self->{configuration}->{database}) {
$self->{logger}->fatal(
"No database defined in configuration, aborting"
);
$self->respond(
data => {
errors => [ 'internal' ]
}
);
} else {
AccountManager::DB->register_db(
driver => $self->{configuration}->{database}->{type},
database => $self->{configuration}->{database}->{name},
host => $self->{configuration}->{database}->{host},
username => $self->{configuration}->{database}->{username},
password => $self->{configuration}->{database}->{password},
options => [ split(/, */, $self->{configuration}->{database}->{options}) ]
);
# 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
# register needed parameters
email => $parameters{email},
entityid => $parameters{entityid},
token => $parameters{token},
key => $parameters{key},
federation => $parameters{federation},
# process requested action
my $action = $parameters{action} || 'home';
if ($actions{$action}) {
$self->{logger}->debug("Processing action '$action'");
my $method = $actions{$action};
$self->abort(
logs => "Unknown action '$action'",
user => "Unknown action '$action'"
return 1;
}
## Return HTML content
sub respond {
my ($self, %in) = @_;
$in{data}->{app} = {
url => $ENV{SCRIPT_NAME},
support_email => $self->{configuration}->{app}->{support_email},
$in{data}->{lh} = $self->{lh};
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $self->{configuration}->{setup}->{templates_dir} . "/web"
$self->{logger}->debug("Responding with template '$in{template}'");
my $cookie = $self->{cgi}->cookie(
-name => 'lang',
-value => $self->{lh}->language_tag(),
-expires => undef,
);
print $self->{cgi}->header(
-type => 'text/html',
-charset => 'utf8',
-cookie => [ $cookie ]
unless ($tt2->process($in{template}, $in{data}, \*STDOUT)) {
printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
$self->{logger}->errorf("Web parser error : %s", $tt2->error());
sub abort {
my $self = shift;
my %args = @_;
$self->{logger}->error($args{log}) if $args{log};
$self->respond(
template => 'errors.tt2.html',
data => {
errors => [ $args{user} ]
}
);
}
sub req_start {
my ($self) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
if (@federations == 1) {
$self->{in}->{federation} = $federations[0];
$self->req_select_sp();
} else {
$self->req_select_federation();
}
}
sub req_select_federation {
my ($self) = @_;
my @federations = keys %{$self->{configuration}->{federations}};
$self->respond(
template => 'select_federation.tt2.html',
data => {
action => 'select_federation',
federations => \@federations
}
);
}
my $federation = $self->{in}->{federation};
$self->abort(
log => "Missing parameter: federation",
user => "missing_federation"
) if !$federation;
my $file = $self->{configuration}->{federations}->{$federation};
$self->abort(
log => "Incorrect parameter: federation",
user => "invalid_federation"
) if !$file;
$metadata = AccountManager::Metadata->new(
$self->abort(
log => "Failed to load federation metadata: $EVAL_ERROR",
user => "internal"
) if $EVAL_ERROR;
template => 'select_sp.tt2.html',
action => 'select_sp',
metadata => $metadata->parse(type => 'sp'),
federation => $federation,
my $federation = $self->{in}->{federation};
$self->abort(
log => "Missing parameter: federation",
user => "missing_federation"
) if !$federation;
my $file = $self->{configuration}->{federations}->{$federation};
$self->abort(
log => "Incorrect parameter: federation",
user => "invalid_federation"
) if !$file;
$self->abort(
log => "Missing parameter: entityid",
user => "missing_entityid"
) if !$self->{in}->{entityid};
$self->abort(
log => "Incorrect parameter format: entityid",
user => "format_entityid"
) if $self->{in}->{entityid} !~ $entity_id_pattern;
# Create a persistent service provider object
# already present in DB, nothing todo
} else {
# extract information from metadata
my $metadata;
eval {
$metadata = AccountManager::Metadata->new(
$self->abort(
log => "Failed to load federation metadata: $EVAL_ERROR",
user => "internal"
) if $EVAL_ERROR;
my $entities = $metadata->parse(id => $self->{in}->{entityid});
my $entity = $entities->[0];
$self->abort(
log => sprintf("No such SP '%s' in metadata", $self->{in}->{entityid}),
user => "no_such_entity"
) if !$entity;
$sp->displayname($entity->{display_name});
$sp->contacts(uniq map { $_->{EmailAddress} } @{$entity->{contacts}})
if $entity->{contacts};
$self->abort(
log => "Failed to save service provider object",
user => "internal"
) if !$sp->save();
# override metadata contacts if needed
$self->{configuration}->{$self->{in}->{entityid}}->{contacts} ||
$self->{configuration}->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$sp->contacts($sp->contacts(), split(/, */, $1));
} else {
# replace original contacts
template => 'select_email.tt2.html',
action => 'select_email',
federation => $federation,
sp => $sp,
my $federation = $self->{in}->{federation};
$self->abort(
log => "Missing parameter: federation",
user => "missing_federation"
) if !$federation;
my $file = $self->{configuration}->{federations}->{$federation};
$self->abort(
log => "Incorrect parameter: federation",
user => "invalid_federation"
) if !$file;
$self->abort(
log => "Missing parameter: entityid",
user => "missing_entityid"
) if !$self->{in}->{entityid};
$self->abort(
log => "Incorrect parameter format: entityid",
user => "format_entityid"
) if $self->{in}->{entityid} !~ $entity_id_pattern;
$self->abort(
log => "Missing parameter: email",
user => "missing_email"
) if !$self->{in}->{email};
my $provider = AccountManager::Service->new(
$self->abort(
log => sprintf("No such SP '%s' in database", $self->{in}->{entityid}),
user => "no_such_entity"
) if !$provider->load(speculative => 1);
# override metadata contacts if needed
$self->{configuration}->{$self->{in}->{entityid}}->{contacts} ||
$self->{configuration}->{service}->{contacts};
if ($contacts) {
if ($contacts =~ /^\+(.+)/) {
# complement original contacts
$provider->contacts($provider->contacts(), split(/, */, $1));
} else {
# replace original contacts
$provider->contacts(split(/, */, $contacts));
}
}
## Check that email is a known contact for this SP
"Requested a token for %s for an unautorized address '%s'",
),
user => "internal",
) if !$provider->is_contact($self->{in}->{email});
# delete any previous token for the same email/service couple
my $old_token = AccountManager::Token->new(
sp_entityid => $self->{in}->{entityid},
if ($old_token->load(speculative => 1)) {
$self->abort(
log => sprintf("Failed to delete previous authentication token with ID %s", $old_token->id()),
user => "internal"
) if !$old_token->delete();
$self->{configuration}->{service}->{tokens_validity_period};
my $token = AccountManager::Token->new(
email_address => $self->{in}->{email},
sp_entityid => $self->{in}->{entityid},
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
token => AccountManager::Tools::generate_secret(20)
$self->abort(
log => "Failed to save service authentication token",
user => "internal"
) if !$token->save();
ENCODING => 'utf8',
PRE_CHOMP => CHOMP_ONE,
INCLUDE_PATH => $self->{configuration}->{setup}->{templates_dir} . "/mail",
app => {
url => $self->{configuration}->{app}->{url},
support_email => $self->{configuration}->{app}->{support_email},
version => $self->{configuration}->{app}->{version},
to => $self->{in}->{email},
entityid => $self->{in}->{entityid},
token => $token->token(),
challenge_url => sprintf(
'%s&action=complete_challenge&federation=%s&entity=%s&email=%s',
$self->{configuration}->{app}->{url},
$self->{in}->{federation},
$self->{in}->{email},
),
my $text_content;
my $html_content;
$tt2->process('send_authentication_token.tt2.txt', $data, \$text_content);
$tt2->process('send_authentication_token.tt2.html', $data, \$html_content);
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
eval "require Email::MIME";
eval "require Email::Sender::Simple";
my $email = Email::MIME->create(
header_str => [
'From' => sprintf('eduGAIN Access Check <%s>', $self->{configuration}->{mailer}->{from}),
'To' => $self->{in}->{email},
'Subject' => sprintf('[eduGAIN Access Check] %s', $self->{lh}->maketext("Test accounts request")),
'Content-Type' => 'multipart/alternative'
],
parts => [
Email::MIME->create(
attributes => {
content_type => "text/plain",
charset => 'utf-8',
encoding => 'quoted-printable'
},
body_str => $text_content
),
Email::MIME->create(
attributes => {
content_type => "text/html",
charset => 'utf-8',
encoding => 'quoted-printable'
},
body_str => $html_content
),
]
Email::Sender::Simple->send($email);
$self->abort(
log => "Mail notification error: $EVAL_ERROR",
user => "mail_notification_failure"
) if $EVAL_ERROR;
$self->{logger}->infof(
"Token send to %s for entityid=%s;token=%s",
$self->{in}->{email},
$self->{in}->{entityid},
template => 'complete_challenge.tt2.html',
action => 'complete_challenge',
federation => $federation,
entityid => $self->{in}->{entityid},
email => $self->{in}->{email},
$self->abort(
log => "Missing parameter: entityid",
user => "missing_entityid"
) if !$self->{in}->{entityid};
$self->abort(
log => "Incorrect parameter format: entityid",
user => "format_entityid"
) if $self->{in}->{entityid} !~ $entity_id_pattern;
$self->abort(
log => "Missing parameter: token",
user => "missing_token"
) if !$self->{in}->{token};
$self->abort(
log => "Missing parameter: email",
user => "missing_email"
) if !$self->{in}->{email};
my $token = AccountManager::Token->new(
"Failed to validate authentication token %s for entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
),
user => "wrong_token"
) if !$token->load(speculative => 1);
"Authentication token %s cannot be used for SP with entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
),
user => "wrong_token_for_sp"
) if $token->sp_entityid() ne $self->{in}->{entityid};
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
);
}
## create test accounts
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};
my $download_token = AccountManager::Token->new(
db => $self->{db},
email_address => $self->{in}->{email},
sp_entityid => $self->{in}->{entityid},
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(hours => $validity_period),
token => AccountManager::Tools::generate_secret(20)
);
$self->abort(
log => "Failed to save download authentication token",
user => "internal"
) if !$download_token->save();
my $key = AccountManager::Tools::generate_secret(10);
foreach my $profile (split(/, */, $profiles)) {
my $password = AccountManager::Tools::generate_password(10);
my $account = AccountManager::Account->new(
db => $self->{db},
profile => $profile,
sp_entityid => $entity,
scope => $self->{configuration}->{idp}->{scope},
password => $password,
password_crypt => AccountManager::Tools::encrypt($password, $key),
password_hash => AccountManager::Tools::sha256_hash($password),
token => $download_token->token(),
creation_date => DateTime->now(),
expiration_date => DateTime->now()->add(days => $validity_period)
"Failed to create test accounts for SP with entityid %s",
),
user => "accounts_creation_failure"
) if !@accounts;
## Update simpleSAMLphp configuration to enable test accounts
my $accounts = AccountManager::Account::Manager->get_accounts(
db => $self->{db}
);
AccountManager::Tools::update_ssp_authsources(
$self->{configuration}->{setup}->{templates_dir},
$self->{configuration}->{setup}->{accounts_file},
"Failed to create simpleSAMLphp configuration file: %s",
$EVAL_ERROR
),
user => "accounts_creation_failed"
) if $EVAL_ERROR;
$self->{logger}->infof(
"Token validated for entityid=%s;token=%s",
$self->{in}->{entityid},
$self->{in}->{token}
template => 'create_accounts.tt2.html',
action => 'create_accounts',
accounts => \@accounts,
entityid => $self->{in}->{entityid},
email => $self->{in}->{email},
key => $key,
token => $download_token->token(),
sub req_download_accounts {
my ($self) = @_;
$self->abort(
log => "Missing parameter: entityid",
user => "missing_entityid"
) if !$self->{in}->{entityid};
$self->abort(
log => "Incorrect parameter format: entityid",
user => "format_entityid"
) if $self->{in}->{entityid} !~ $entity_id_pattern;
$self->abort(
log => "Missing parameter: token",
user => "missing_token"
) if !$self->{in}->{token};
$self->abort(
log => "Missing parameter: key",
user => "missing_key"
) if !$self->{in}->{key};
my $token = AccountManager::Token->new(
db => $self->{db},
token => $self->{in}->{token}
);
$self->abort(
log => sprintf(
"Failed to validate authentication token %s for entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
),
user => "wrong_token"
) if !$token->load(speculative => 1);
$self->abort(
log => sprintf(
"Authentication token %s cannot be used for SP with entityid %s",
$self->{in}->{token},
$self->{in}->{entityid}
),
user => "wrong_token_for_sp"
) if $token->sp_entityid() ne $self->{in}->{entityid};
# delete the token
unless ($token->delete()) {
$self->{logger}->errorf(
"Failed to delete authentication token %s",
$self->{in}->{token}
);
}
# load accounts from database
my $accounts = AccountManager::Account::Manager->get_accounts(
db => $self->{db},
query => [
token => $self->{in}->{token}
],
);
binmode(STDOUT, ":utf8");
print $self->{cgi}->header(
-type => 'text/csv',
-content_disposition => 'attachment; filename="accounts.csv"'
);
my $csv = Text::CSV->new ({ binary => 1, eol => "\r\n", quote_space => 0 });
$csv->print(\*STDOUT, [ qw/
username
password
profile
cn
displayName
givenName
mail
eduPersonAffiliation
eduPersonScopedAffiliation
eduPersonPrincipalName
schacHomeOrganization
schacHomeOrganizationType
/ ]);
foreach my $account (@$accounts) {
my $password = AccountManager::Tools::decrypt(
$account->password_crypt(),
$self->{in}->{key}
);
$account->password($password);
$csv->print(\*STDOUT, [
$account->internal_uid(),
$account->password(),
$account->profile(),
$account->cn(),
$account->displayName(),
$account->givenName(),
$account->mail(),
join(', ', $account->eduPersonAffiliation()),
join(', ', $account->eduPersonScopedAffiliation()),
$account->eduPersonPrincipalName(),
$account->schacHomeOrganization(),
$account->schacHomeOrganizationType(),
]);
}
}
## Return the homepage of the service
sub req_home {
data => {
action => 'home'
}