diff --git a/bin/account-manager-web.pl b/bin/account-manager-web.pl
index b4af672294f701cd078cd753a9ca5c07de6a5d35..57cadd97f28ecac9ed74e4dd2ad627a79322741a 100755
--- a/bin/account-manager-web.pl
+++ b/bin/account-manager-web.pl
@@ -23,6 +23,7 @@ use IdPAccountManager::SAMLMetadata;
 use IdPAccountManager::ServiceProvider;
 
 use IdPAccountManager::AuthenticationToken;
+use IdPAccountManager::WebRequest;
 
 ## Defining parameters format
 my $urn_or_url_regex = '(http(s?):\/\/|urn:)[^\\\$\*\"\'\`\^\|\<\>\n\s]+'
@@ -49,565 +50,13 @@ umask 0002;
 
 chdir $Conf::global{'root_manager_dir'};
 
-my $request = new WebRequest;
+my $request = new IdPAccountManager::WebRequest(
+    actions => \%actions,
+    format  => \%format
+);
 
 if (defined $request) {
     $request->execute();
 }
 
 $request->respond();
-
-package WebRequest;
-
-## New web request
-sub new {
-    my $pkg     = shift;
-    my $request = {};
-    IdPAccountManager::Tools::do_log('info', "");
-
-    my $http_query = new CGI;
-
-    ## Input parameters
-    my %in_vars = $http_query->Vars;
-    $request->{'param_in'} = \%in_vars;
-
-    ## Check if admin acts as another user
-    $request->{'cookies'} = CGI::Cookie->fetch;
-
-#if (defined $request->{'cookies'}{'as_user'} && $request->{'is_admin'}) {
-#    $request->{'utilisateur'} =  $request->{'as_user'} = $request->{'cookies'}{'as_user'}->value;
-#    $request->{'is_admin'} = 0;
-#}
-
-    ## Usefull data for output (web pages or mail notices)
-    $request->{'param_out'}{'url_cgi'} = $ENV{'SCRIPT_NAME'};
-    $request->{'param_out'}{'env'}     = \%ENV;
-    $request->{'param_out'}{'actions'} = \%actions;
-    $request->{'param_out'}{'conf'}    = \%Conf::global;
-
-    ## Dumping input data
-#open TMP, ">/tmp/account_manager.in"; IdPAccountManager::Tools::dump_var($request->{'param_in'}, 0, \*TMP); close TMP;
-
-    ## Clean input vars
-    foreach my $key (keys %{ $request->{'param_in'} }) {
-
-#IdPAccountManager::Tools::do_log('trace', "PARAM_ENTREE: %s=%s", $key, $request->{'param_in'}{$key});
-
-        ## Removing all ^M (0D)
-        $request->{'param_in'}{$key} =~ s/\r//g;
-
-        $request->{'param_in'}{$key} =~ s/\s+$//;    ## Remove trailing spaces
-        $request->{'param_in'}{$key} =~ s/^\s+//;    ## Remove leading spaces
-            #if ($request->{'param_in'}{$key} =~ /\0/) {
-            #  my @valeurs = split /\0/, $request->{'param_in'}{$key};
-         #  $request->{'param_in'}{$key} = $valeurs[0]; ## Only keep first value of multi-valued parameters
-         #}
-
-        ## If action_xx param is set, then action=xx
-        ## Usefull to have sementicless values in submit forms
-        if ($key =~ /^action_(\w+)$/) {
-
-            #IdPAccountManager::Tools::do_log('trace', "ACTION $key");
-            $request->{'param_in'}{'action'} = $1;
-        }
-    }
-
-    ## Check the requested action
-    if ($request->{'param_in'}{'action'}) {
-        $request->{'action'} = $request->{'param_in'}{'action'};
-    } else {
-        ## Default action
-        IdPAccountManager::Tools::do_log('info', "Default action");
-        $request->{'action'} = 'home';
-    }
-
-    bless $request, $pkg;
-
-    return $request;
-}
-
-## Execute a web request
-sub execute {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('debug', "");
-
-    my $status;
-
-    ## Check input parameters format
-    foreach my $key (keys %{ $self->{'param_in'} }) {
-        if (   $self->{'param_in'}{$key} !~ /^\s*$/
-            && defined $format{$key}
-            && !ref($format{$key}))
-        {
-            unless ($self->{'param_in'}{$key} =~ /^$format{$key}$/) {
-                push @{ $self->{'param_out'}{'errors'} }, "format_$key";
-                IdPAccountManager::Tools::do_log('error',
-                    "Incorrect parameter format : $key");
-                return undef;
-            }
-        }
-    }
-
-    do {
-        ## Actions can be chained
-        $self->{'action'} = $self->{'next_action'} if ($self->{'next_action'});
-        delete $self->{'next_action'};    ## Prevent loops
-
-        if (defined $actions{ $self->{'action'} }) {
-            ## Execute the target subroutine named req_actionName
-            no strict 'refs';
-            my $sub = 'req_' . $self->{'action'};
-            $status = &{$sub}($self);
-        } else {
-            ## Inknown action
-            push @{ $self->{'param_out'}{'errors'} }, "unknown_action";
-            IdPAccountManager::Tools::do_log('error', "Unknown action '%s'",
-                $self->{'action'});
-
-        }
-
-    } while ($self->{'next_action'});
-
-    #return undef if (!defined $status);
-
-    return 1;
-}
-
-## Return HTML content
-sub respond {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('debug', "");
-
-    ## Dump output data
-#open TMP, ">/tmp/account_registry.out"; IdPAccountManager::Tools::dump_var($self->{'param_out'}, 0, \*TMP); close TMP;
-
-    ## Enable dumping off all variables in web pages
-    #$self->{'param_out'}{'dump'} =  $self->{'param_out'};
-
-    ## Automatic pass object entries to the output hash
-    foreach my $key (keys %{$self}) {
-
-        #IdPAccountManager::Tools::do_log('trace', "Passing $key");
-        $self->{'param_out'}{$key} ||= $self->{$key}
-          unless ($key eq 'param_out');
-    }
-
-    ## An action may redirect to an external URL
-    if ($self->{'url_redirection'}) {
-
-#IdPAccountManager::Tools::do_log('trace', "URL Redirect : $self->{'url_redirection'}");
-        printf "Location: %s\n\n", $self->{'url_redirection'};
-
-    } else {
-
-#$self->{'param_out'}{'cookie'} = CGI::Cookie->new(-name=>'as_user',-value=>$self->{'as_user'},-expires=>'-1M');
-
-        ## Parse template
-        my $tt2 = Template->new(
-            {
-                ENCODING => 'iso-8859-1',    ## le défaut apparemment
-                FILTERS  => {
-                    'encode_utf8', =>
-                      [ \&IdPAccountManager::Tools::encode_utf8, 0 ],
-                    'escape_quotes' =>
-                      [ \&IdPAccountManager::Tools::escape_quotes, 0 ]
-                },
-                INCLUDE_PATH => $Conf::global{'root_manager_dir'} . ':'
-                  . $Conf::global{'root_manager_dir'}
-                  . '/templates/accountProfiles',
-
-                #DEBUG => 'all',
-                #DEBUG => 'caller',
-                #DEBUG => 'parser'
-            }
-        );
-
-        my $template;
-
-        ## nobanner is used to do AJAX to get only pieces of HTML to load in the web client
-        if ($self->{'param_in'}{'style'} eq 'nobanner') {
-            $template = 'templates/web/index-nobanner.tt2.html';
-        } else {
-            $template = 'templates/web/index.tt2.html';
-        }
-
-        unless ($tt2->process($template, $self->{'param_out'}, \*STDOUT)) {
-            printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
-            IdPAccountManager::Tools::do_log('error', "Web parser error : %s",
-                $tt2->error());
-        }
-    }
-
-    ## Ignore some type of errors
-    my @errors_admin;
-    foreach my $id_error (@{ $self->{'param_out'}{'errors'} }) {
-        unless ($id_error =~ /^(error_x)$/) {
-            push @errors_admin, $id_error;
-        }
-    }
-
-    ## Mail notification of admins about the error
-    if (@errors_admin) {
-        $self->{'param_out'}{'subject'} = 'Error notification - web interface';
-        IdPAccountManager::Tools::mail_notice(
-            'template' => 'templates/mail/notification_generic_error.tt2.eml',
-            'data'     => $self->{'param_out'}
-        );
-    }
-}
-
-## Return the list of known SPs first
-sub req_account_wizard {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('info', "");
-
-    my $federation_metadata = new IdPAccountManager::SAMLMetadata;
-    unless (
-        $federation_metadata->load(
-            federation_metadata_file_path =>
-              $Conf::global{'federation_metadata_file_path'}
-        )
-      )
-    {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to load federation metadata : $!");
-        return undef;
-    }
-
-    unless ($federation_metadata->parse()) {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to parse federation metadata : $!");
-        return undef;
-    }
-
-    $self->{'param_out'}{'federation_metadata_as_hashref'} =
-      $federation_metadata->{'federation_metadata_as_hashref'};
-
-    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 {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('info', "");
-
-    unless ($self->{'param_in'}{'sp_entityid'}) {
-        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
-        IdPAccountManager::Tools::do_log('error',
-            "Missing parameter sp_entityid");
-        return undef;
-    }
-
-    my $federation_metadata = new IdPAccountManager::SAMLMetadata;
-    unless (
-        $federation_metadata->load(
-            federation_metadata_file_path =>
-              $Conf::global{'federation_metadata_file_path'}
-        )
-      )
-    {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to load federation metadata : $!");
-        return undef;
-    }
-
-    unless (
-        $federation_metadata->parse(
-            filter_entity_id => $self->{'param_in'}{'sp_entityid'}
-        )
-      )
-    {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to parse federation metadata : $!");
-        return undef;
-    }
-
-    ## Create a serviceprovider object to store major parameters for this SP in DB
-    my $service_provider = new IdPAccountManager::ServiceProvider(
-        entityid => $self->{'param_in'}{'sp_entityid'});
-
-    ## Prepare data
-#open TMP, ">/tmp/account_manager_metadata.dump"; IdPAccountManager::Tools::dump_var($federation_metadata->{'federation_metadata_as_hashref'}[0], 0, \*TMP); close TMP;
-    my $sp_metadata_as_hashref =
-      $federation_metadata->{'federation_metadata_as_hashref'}[0];
-    my @contacts;
-    if (defined $sp_metadata_as_hashref->{'contacts'}) {
-        foreach my $contact (@{ $sp_metadata_as_hashref->{'contacts'} }) {
-            my $email = $contact->{'EmailAddress'};
-            $email =~ s/^(mailto:)//;    ## Remove 'mailto:' prefixes if any
-            push @contacts, $email;
-        }
-    }
-    my $display_name;
-    if (defined $sp_metadata_as_hashref->{'display_name'}) {
-        ## Use English version of displayName if available
-        if ($sp_metadata_as_hashref->{'display_name'}{'en'}) {
-            $display_name = $sp_metadata_as_hashref->{'display_name'}{'en'};
-            ## Else any language
-        } else {
-            foreach
-              my $lang (keys %{ $sp_metadata_as_hashref->{'display_name'} })
-            {
-#IdPAccountManager::Tools::do_log('TRACE', "Display name(%s): %s", $lang, $sp_metadata_as_hashref->{'display_name'}{$lang});
-                $display_name =
-                  $sp_metadata_as_hashref->{'display_name'}{$lang};
-                last;
-            }
-        }
-    }
-
-    ## Try loading DB object first
-    if ($service_provider->load(speculative => 1)) {
-        $service_provider->contacts(join(',', @contacts));
-        $service_provider->displayname($display_name);
-
-    } else {
-
-        $service_provider = new IdPAccountManager::ServiceProvider(
-            entityid    => $self->{'param_in'}{'sp_entityid'},
-            contacts    => join(',', @contacts),
-            displayname => $display_name
-        );
-        unless (defined $service_provider) {
-            push @{ $self->{'param_out'}{'errors'} }, "internal";
-            IdPAccountManager::Tools::do_log('error',
-                "Failed to create serviceprovider object");
-            return undef;
-        }
-    }
-
-    unless ($service_provider->save()) {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to save serviceprovider object");
-        return undef;
-    }
-
-    $self->{'param_out'}{'sp_metadata_as_hashref'} =
-      $federation_metadata->{'federation_metadata_as_hashref'}[0];
-    $self->{'param_out'}{'serviceprovider'} = $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 {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('info', "");
-
-    unless ($self->{'param_in'}{'sp_entityid'}) {
-        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
-        IdPAccountManager::Tools::do_log('error',
-            "Missing parameter sp_entityid");
-        return undef;
-    }
-
-    unless ($self->{'param_in'}{'email_address'}) {
-        push @{ $self->{'param_out'}{'errors'} }, "email_address";
-        IdPAccountManager::Tools::do_log('error',
-            "Missing parameter email_address");
-        return undef;
-    }
-
-    ## Create a serviceprovider object to load parameters for this SP from DB
-    my $service_provider = new IdPAccountManager::ServiceProvider(
-        entityid => $self->{'param_in'}{'sp_entityid'});
-
-    # Try loading DB object first
-    unless ($service_provider->load(speculative => 1)) {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Failed to load SP with entityid '%s'",
-            $self->{'param_in'}{'sp_entityid'}
-        );
-        return undef;
-    }
-
-    ## Check that email_address is a known contact for this SP
-    unless ($service_provider->is_contact($self->{'param_in'}{'email_address'}))
-    {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Requested a token for %s for an unautorized address '%s'",
-            $self->{'param_in'}{'sp_entityid'},
-            $self->{'param_in'}{'email_address'}
-        );
-        return undef;
-    }
-
-    my $authentication_token = new IdPAccountManager::AuthenticationToken(
-        'email_address' => $self->{'param_in'}{'email_address'},
-        'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
-    );
-    unless (defined $authentication_token) {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to create authentication token");
-        return undef;
-    }
-
-    ## First remove token if one exist for this email+SP
-    if ($authentication_token->load()) {
-        unless ($authentication_token->delete()) {
-            push @{ $self->{'param_out'}{'errors'} }, "internal";
-            IdPAccountManager::Tools::do_log(
-                'error',
-                "Failed to delete previous authentication token with ID %s",
-                $authentication_token->get('id')
-            );
-            return undef;
-        }
-
-        $authentication_token = new IdPAccountManager::AuthenticationToken(
-            'email_address' => $self->{'param_in'}{'email_address'},
-            'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
-        );
-        unless (defined $authentication_token) {
-            push @{ $self->{'param_out'}{'errors'} }, "internal";
-            IdPAccountManager::Tools::do_log('error',
-                "Failed to create authentication token");
-            return undef;
-        }
-    }
-
-    unless ($authentication_token->save()) {
-        push @{ $self->{'param_out'}{'errors'} }, "internal";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to save authentication token");
-        return undef;
-    }
-
-    $self->{'param_out'}{'authentication_token'} =
-      $authentication_token->get('token');
-    $self->{'param_out'}{'email_address'} =
-      $self->{'param_in'}{'email_address'};
-    $self->{'param_out'}{'sp_entityid'} = $self->{'param_in'}{'sp_entityid'};
-    $self->{'param_out'}{'to'}          = $self->{'param_in'}{'email_address'};
-
-    ## Send the challenge email with the token
-    IdPAccountManager::Tools::mail_notice(
-        'template' => 'templates/mail/send_authentication_token.tt2.eml',
-        'to'       => $self->{'param_in'}{'email_address'},
-        'data'     => $self->{'param_out'}
-    );
-
-    IdPAccountManager::Tools::do_log(
-        'info',
-        "Token send to %s for sp_entityid=%s;token=%s",
-        $self->{'param_in'}{'email_address'},
-        $self->{'param_in'}{'sp_entityid'},
-        $self->{'param_out'}{'authentication_token'}
-    );
-
-    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 {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('info', "");
-
-    unless ($self->{'param_in'}{'sp_entityid'}) {
-        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
-        IdPAccountManager::Tools::do_log('error',
-            "Missing parameter sp_entityid");
-        return undef;
-    }
-
-    unless ($self->{'param_in'}{'authentication_token'}) {
-        push @{ $self->{'param_out'}{'errors'} },
-          "missing_authentication_token";
-        IdPAccountManager::Tools::do_log('error',
-            "Missing parameter authentication_token");
-        return undef;
-    }
-
-    my $authentication_token = new IdPAccountManager::AuthenticationToken(
-        token => $self->{'param_in'}{'authentication_token'});
-
-    unless ($authentication_token->load()) {
-        push @{ $self->{'param_out'}{'errors'} }, "wrong_token";
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Failed to validate authentication token %s for sp_entityid %s",
-            $self->{'param_in'}{'authentication_token'},
-            $self->{'param_in'}{'sp_entityid'}
-        );
-        return undef;
-    }
-
-    unless ($authentication_token->get('sp_entityid') eq
-        $self->{'param_in'}{'sp_entityid'})
-    {
-        push @{ $self->{'param_out'}{'errors'} }, "wrong_token_for_sp";
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Authentication token %s cannot be used for SP with entityid %s",
-            $self->{'param_in'}{'authentication_token'},
-            $self->{'param_in'}{'sp_entityid'}
-        );
-        return undef;
-    }
-
-    ## delete the token
-    unless ($authentication_token->delete()) {
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Failed to delete authentication token %s",
-            $self->{'param_in'}{'authentication_token'}
-        );
-    }
-
-    ## create test accounts
-    my @test_accounts =
-      IdPAccountManager::TestAccount::create_test_accounts_for_sp(
-        sp_entityid => $self->{'param_in'}{'sp_entityid'});
-
-    unless (@test_accounts) {
-        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
-        IdPAccountManager::Tools::do_log(
-            'error',
-            "Failed to create test accounts for SP with entityid %s",
-            $self->{'param_in'}{'sp_entityid'}
-        );
-        return undef;
-    }
-
-    ## Update simpleSAMLphp configuration to enable test accounts
-    unless (IdPAccountManager::Tools::update_ssp_authsources()) {
-        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
-        IdPAccountManager::Tools::do_log('error',
-            "Failed to create simpleSAMLphp configuration file");
-        return undef;
-    }
-
-    IdPAccountManager::Tools::do_log(
-        'info',
-        "Token validated for sp_entityid=%s;token=%s",
-        $self->{'param_in'}{'sp_entityid'},
-        $self->{'param_in'}{'authentication_token'}
-    );
-
-    $self->{'param_out'}{'sp_entityid'}   = $self->{'param_in'}{'sp_entityid'};
-    $self->{'param_out'}{'test_accounts'} = \@test_accounts;
-
-    return 1;
-}
-
-## Return the homepage of the service
-sub req_home {
-    my $self = shift;
-    IdPAccountManager::Tools::do_log('info', "");
-
-    return 1;
-}
diff --git a/lib/IdPAccountManager/WebRequest.pm b/lib/IdPAccountManager/WebRequest.pm
new file mode 100755
index 0000000000000000000000000000000000000000..de1c370f94904d88c8da31eaeabcda76f149211e
--- /dev/null
+++ b/lib/IdPAccountManager/WebRequest.pm
@@ -0,0 +1,556 @@
+package IdPAccountManager::WebRequest;
+
+## New web request
+sub new {
+    my $pkg     = shift;
+    my $request = {};
+    IdPAccountManager::Tools::do_log('info', "");
+
+    my $http_query = new CGI;
+
+    ## Input parameters
+    my %in_vars = $http_query->Vars;
+    $request->{'param_in'} = \%in_vars;
+
+    ## Check if admin acts as another user
+    $request->{'cookies'} = CGI::Cookie->fetch;
+
+#if (defined $request->{'cookies'}{'as_user'} && $request->{'is_admin'}) {
+#    $request->{'utilisateur'} =  $request->{'as_user'} = $request->{'cookies'}{'as_user'}->value;
+#    $request->{'is_admin'} = 0;
+#}
+
+    ## Usefull data for output (web pages or mail notices)
+    $request->{'param_out'}{'url_cgi'} = $ENV{'SCRIPT_NAME'};
+    $request->{'param_out'}{'env'}     = \%ENV;
+    $request->{'param_out'}{'actions'} = \%actions;
+    $request->{'param_out'}{'conf'}    = \%Conf::global;
+
+    ## Dumping input data
+#open TMP, ">/tmp/account_manager.in"; IdPAccountManager::Tools::dump_var($request->{'param_in'}, 0, \*TMP); close TMP;
+
+    ## Clean input vars
+    foreach my $key (keys %{ $request->{'param_in'} }) {
+
+#IdPAccountManager::Tools::do_log('trace', "PARAM_ENTREE: %s=%s", $key, $request->{'param_in'}{$key});
+
+        ## Removing all ^M (0D)
+        $request->{'param_in'}{$key} =~ s/\r//g;
+
+        $request->{'param_in'}{$key} =~ s/\s+$//;    ## Remove trailing spaces
+        $request->{'param_in'}{$key} =~ s/^\s+//;    ## Remove leading spaces
+            #if ($request->{'param_in'}{$key} =~ /\0/) {
+            #  my @valeurs = split /\0/, $request->{'param_in'}{$key};
+         #  $request->{'param_in'}{$key} = $valeurs[0]; ## Only keep first value of multi-valued parameters
+         #}
+
+        ## If action_xx param is set, then action=xx
+        ## Usefull to have sementicless values in submit forms
+        if ($key =~ /^action_(\w+)$/) {
+
+            #IdPAccountManager::Tools::do_log('trace', "ACTION $key");
+            $request->{'param_in'}{'action'} = $1;
+        }
+    }
+
+    ## Check the requested action
+    if ($request->{'param_in'}{'action'}) {
+        $request->{'action'} = $request->{'param_in'}{'action'};
+    } else {
+        ## Default action
+        IdPAccountManager::Tools::do_log('info', "Default action");
+        $request->{'action'} = 'home';
+    }
+
+    bless $request, $pkg;
+
+    return $request;
+}
+
+## Execute a web request
+sub execute {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('debug', "");
+
+    my $status;
+
+    ## Check input parameters format
+    foreach my $key (keys %{ $self->{'param_in'} }) {
+        if (   $self->{'param_in'}{$key} !~ /^\s*$/
+            && defined $format{$key}
+            && !ref($format{$key}))
+        {
+            unless ($self->{'param_in'}{$key} =~ /^$format{$key}$/) {
+                push @{ $self->{'param_out'}{'errors'} }, "format_$key";
+                IdPAccountManager::Tools::do_log('error',
+                    "Incorrect parameter format : $key");
+                return undef;
+            }
+        }
+    }
+
+    do {
+        ## Actions can be chained
+        $self->{'action'} = $self->{'next_action'} if ($self->{'next_action'});
+        delete $self->{'next_action'};    ## Prevent loops
+
+        if (defined $actions{ $self->{'action'} }) {
+            ## Execute the target subroutine named req_actionName
+            no strict 'refs';
+            my $sub = 'req_' . $self->{'action'};
+            $status = &{$sub}($self);
+        } else {
+            ## Inknown action
+            push @{ $self->{'param_out'}{'errors'} }, "unknown_action";
+            IdPAccountManager::Tools::do_log('error', "Unknown action '%s'",
+                $self->{'action'});
+
+        }
+
+    } while ($self->{'next_action'});
+
+    #return undef if (!defined $status);
+
+    return 1;
+}
+
+## Return HTML content
+sub respond {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('debug', "");
+
+    ## Dump output data
+#open TMP, ">/tmp/account_registry.out"; IdPAccountManager::Tools::dump_var($self->{'param_out'}, 0, \*TMP); close TMP;
+
+    ## Enable dumping off all variables in web pages
+    #$self->{'param_out'}{'dump'} =  $self->{'param_out'};
+
+    ## Automatic pass object entries to the output hash
+    foreach my $key (keys %{$self}) {
+
+        #IdPAccountManager::Tools::do_log('trace', "Passing $key");
+        $self->{'param_out'}{$key} ||= $self->{$key}
+          unless ($key eq 'param_out');
+    }
+
+    ## An action may redirect to an external URL
+    if ($self->{'url_redirection'}) {
+
+#IdPAccountManager::Tools::do_log('trace', "URL Redirect : $self->{'url_redirection'}");
+        printf "Location: %s\n\n", $self->{'url_redirection'};
+
+    } else {
+
+#$self->{'param_out'}{'cookie'} = CGI::Cookie->new(-name=>'as_user',-value=>$self->{'as_user'},-expires=>'-1M');
+
+        ## Parse template
+        my $tt2 = Template->new(
+            {
+                ENCODING => 'iso-8859-1',    ## le défaut apparemment
+                FILTERS  => {
+                    'encode_utf8', =>
+                      [ \&IdPAccountManager::Tools::encode_utf8, 0 ],
+                    'escape_quotes' =>
+                      [ \&IdPAccountManager::Tools::escape_quotes, 0 ]
+                },
+                INCLUDE_PATH => $Conf::global{'root_manager_dir'} . ':'
+                  . $Conf::global{'root_manager_dir'}
+                  . '/templates/accountProfiles',
+
+                #DEBUG => 'all',
+                #DEBUG => 'caller',
+                #DEBUG => 'parser'
+            }
+        );
+
+        my $template;
+
+        ## nobanner is used to do AJAX to get only pieces of HTML to load in the web client
+        if ($self->{'param_in'}{'style'} eq 'nobanner') {
+            $template = 'templates/web/index-nobanner.tt2.html';
+        } else {
+            $template = 'templates/web/index.tt2.html';
+        }
+
+        unless ($tt2->process($template, $self->{'param_out'}, \*STDOUT)) {
+            printf "Content-type: text/plain\n\n Error: %s", $tt2->error();
+            IdPAccountManager::Tools::do_log('error', "Web parser error : %s",
+                $tt2->error());
+        }
+    }
+
+    ## Ignore some type of errors
+    my @errors_admin;
+    foreach my $id_error (@{ $self->{'param_out'}{'errors'} }) {
+        unless ($id_error =~ /^(error_x)$/) {
+            push @errors_admin, $id_error;
+        }
+    }
+
+    ## Mail notification of admins about the error
+    if (@errors_admin) {
+        $self->{'param_out'}{'subject'} = 'Error notification - web interface';
+        IdPAccountManager::Tools::mail_notice(
+            'template' => 'templates/mail/notification_generic_error.tt2.eml',
+            'data'     => $self->{'param_out'}
+        );
+    }
+}
+
+## Return the list of known SPs first
+sub req_account_wizard {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('info', "");
+
+    my $federation_metadata = new IdPAccountManager::SAMLMetadata;
+    unless (
+        $federation_metadata->load(
+            federation_metadata_file_path =>
+              $Conf::global{'federation_metadata_file_path'}
+        )
+      )
+    {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to load federation metadata : $!");
+        return undef;
+    }
+
+    unless ($federation_metadata->parse()) {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to parse federation metadata : $!");
+        return undef;
+    }
+
+    $self->{'param_out'}{'federation_metadata_as_hashref'} =
+      $federation_metadata->{'federation_metadata_as_hashref'};
+
+    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 {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('info', "");
+
+    unless ($self->{'param_in'}{'sp_entityid'}) {
+        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
+        IdPAccountManager::Tools::do_log('error',
+            "Missing parameter sp_entityid");
+        return undef;
+    }
+
+    my $federation_metadata = new IdPAccountManager::SAMLMetadata;
+    unless (
+        $federation_metadata->load(
+            federation_metadata_file_path =>
+              $Conf::global{'federation_metadata_file_path'}
+        )
+      )
+    {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to load federation metadata : $!");
+        return undef;
+    }
+
+    unless (
+        $federation_metadata->parse(
+            filter_entity_id => $self->{'param_in'}{'sp_entityid'}
+        )
+      )
+    {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to parse federation metadata : $!");
+        return undef;
+    }
+
+    ## Create a serviceprovider object to store major parameters for this SP in DB
+    my $service_provider = new IdPAccountManager::ServiceProvider(
+        entityid => $self->{'param_in'}{'sp_entityid'});
+
+    ## Prepare data
+#open TMP, ">/tmp/account_manager_metadata.dump"; IdPAccountManager::Tools::dump_var($federation_metadata->{'federation_metadata_as_hashref'}[0], 0, \*TMP); close TMP;
+    my $sp_metadata_as_hashref =
+      $federation_metadata->{'federation_metadata_as_hashref'}[0];
+    my @contacts;
+    if (defined $sp_metadata_as_hashref->{'contacts'}) {
+        foreach my $contact (@{ $sp_metadata_as_hashref->{'contacts'} }) {
+            my $email = $contact->{'EmailAddress'};
+            $email =~ s/^(mailto:)//;    ## Remove 'mailto:' prefixes if any
+            push @contacts, $email;
+        }
+    }
+    my $display_name;
+    if (defined $sp_metadata_as_hashref->{'display_name'}) {
+        ## Use English version of displayName if available
+        if ($sp_metadata_as_hashref->{'display_name'}{'en'}) {
+            $display_name = $sp_metadata_as_hashref->{'display_name'}{'en'};
+            ## Else any language
+        } else {
+            foreach
+              my $lang (keys %{ $sp_metadata_as_hashref->{'display_name'} })
+            {
+#IdPAccountManager::Tools::do_log('TRACE', "Display name(%s): %s", $lang, $sp_metadata_as_hashref->{'display_name'}{$lang});
+                $display_name =
+                  $sp_metadata_as_hashref->{'display_name'}{$lang};
+                last;
+            }
+        }
+    }
+
+    ## Try loading DB object first
+    if ($service_provider->load(speculative => 1)) {
+        $service_provider->contacts(join(',', @contacts));
+        $service_provider->displayname($display_name);
+
+    } else {
+
+        $service_provider = new IdPAccountManager::ServiceProvider(
+            entityid    => $self->{'param_in'}{'sp_entityid'},
+            contacts    => join(',', @contacts),
+            displayname => $display_name
+        );
+        unless (defined $service_provider) {
+            push @{ $self->{'param_out'}{'errors'} }, "internal";
+            IdPAccountManager::Tools::do_log('error',
+                "Failed to create serviceprovider object");
+            return undef;
+        }
+    }
+
+    unless ($service_provider->save()) {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to save serviceprovider object");
+        return undef;
+    }
+
+    $self->{'param_out'}{'sp_metadata_as_hashref'} =
+      $federation_metadata->{'federation_metadata_as_hashref'}[0];
+    $self->{'param_out'}{'serviceprovider'} = $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 {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('info', "");
+
+    unless ($self->{'param_in'}{'sp_entityid'}) {
+        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
+        IdPAccountManager::Tools::do_log('error',
+            "Missing parameter sp_entityid");
+        return undef;
+    }
+
+    unless ($self->{'param_in'}{'email_address'}) {
+        push @{ $self->{'param_out'}{'errors'} }, "email_address";
+        IdPAccountManager::Tools::do_log('error',
+            "Missing parameter email_address");
+        return undef;
+    }
+
+    ## Create a serviceprovider object to load parameters for this SP from DB
+    my $service_provider = new IdPAccountManager::ServiceProvider(
+        entityid => $self->{'param_in'}{'sp_entityid'});
+
+    # Try loading DB object first
+    unless ($service_provider->load(speculative => 1)) {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Failed to load SP with entityid '%s'",
+            $self->{'param_in'}{'sp_entityid'}
+        );
+        return undef;
+    }
+
+    ## Check that email_address is a known contact for this SP
+    unless ($service_provider->is_contact($self->{'param_in'}{'email_address'}))
+    {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Requested a token for %s for an unautorized address '%s'",
+            $self->{'param_in'}{'sp_entityid'},
+            $self->{'param_in'}{'email_address'}
+        );
+        return undef;
+    }
+
+    my $authentication_token = new IdPAccountManager::AuthenticationToken(
+        'email_address' => $self->{'param_in'}{'email_address'},
+        'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
+    );
+    unless (defined $authentication_token) {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to create authentication token");
+        return undef;
+    }
+
+    ## First remove token if one exist for this email+SP
+    if ($authentication_token->load()) {
+        unless ($authentication_token->delete()) {
+            push @{ $self->{'param_out'}{'errors'} }, "internal";
+            IdPAccountManager::Tools::do_log(
+                'error',
+                "Failed to delete previous authentication token with ID %s",
+                $authentication_token->get('id')
+            );
+            return undef;
+        }
+
+        $authentication_token = new IdPAccountManager::AuthenticationToken(
+            'email_address' => $self->{'param_in'}{'email_address'},
+            'sp_entityid'   => $self->{'param_in'}{'sp_entityid'}
+        );
+        unless (defined $authentication_token) {
+            push @{ $self->{'param_out'}{'errors'} }, "internal";
+            IdPAccountManager::Tools::do_log('error',
+                "Failed to create authentication token");
+            return undef;
+        }
+    }
+
+    unless ($authentication_token->save()) {
+        push @{ $self->{'param_out'}{'errors'} }, "internal";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to save authentication token");
+        return undef;
+    }
+
+    $self->{'param_out'}{'authentication_token'} =
+      $authentication_token->get('token');
+    $self->{'param_out'}{'email_address'} =
+      $self->{'param_in'}{'email_address'};
+    $self->{'param_out'}{'sp_entityid'} = $self->{'param_in'}{'sp_entityid'};
+    $self->{'param_out'}{'to'}          = $self->{'param_in'}{'email_address'};
+
+    ## Send the challenge email with the token
+    IdPAccountManager::Tools::mail_notice(
+        'template' => 'templates/mail/send_authentication_token.tt2.eml',
+        'to'       => $self->{'param_in'}{'email_address'},
+        'data'     => $self->{'param_out'}
+    );
+
+    IdPAccountManager::Tools::do_log(
+        'info',
+        "Token send to %s for sp_entityid=%s;token=%s",
+        $self->{'param_in'}{'email_address'},
+        $self->{'param_in'}{'sp_entityid'},
+        $self->{'param_out'}{'authentication_token'}
+    );
+
+    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 {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('info', "");
+
+    unless ($self->{'param_in'}{'sp_entityid'}) {
+        push @{ $self->{'param_out'}{'errors'} }, "missing_sp_entityid";
+        IdPAccountManager::Tools::do_log('error',
+            "Missing parameter sp_entityid");
+        return undef;
+    }
+
+    unless ($self->{'param_in'}{'authentication_token'}) {
+        push @{ $self->{'param_out'}{'errors'} },
+          "missing_authentication_token";
+        IdPAccountManager::Tools::do_log('error',
+            "Missing parameter authentication_token");
+        return undef;
+    }
+
+    my $authentication_token = new IdPAccountManager::AuthenticationToken(
+        token => $self->{'param_in'}{'authentication_token'});
+
+    unless ($authentication_token->load()) {
+        push @{ $self->{'param_out'}{'errors'} }, "wrong_token";
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Failed to validate authentication token %s for sp_entityid %s",
+            $self->{'param_in'}{'authentication_token'},
+            $self->{'param_in'}{'sp_entityid'}
+        );
+        return undef;
+    }
+
+    unless ($authentication_token->get('sp_entityid') eq
+        $self->{'param_in'}{'sp_entityid'})
+    {
+        push @{ $self->{'param_out'}{'errors'} }, "wrong_token_for_sp";
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Authentication token %s cannot be used for SP with entityid %s",
+            $self->{'param_in'}{'authentication_token'},
+            $self->{'param_in'}{'sp_entityid'}
+        );
+        return undef;
+    }
+
+    ## delete the token
+    unless ($authentication_token->delete()) {
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Failed to delete authentication token %s",
+            $self->{'param_in'}{'authentication_token'}
+        );
+    }
+
+    ## create test accounts
+    my @test_accounts =
+      IdPAccountManager::TestAccount::create_test_accounts_for_sp(
+        sp_entityid => $self->{'param_in'}{'sp_entityid'});
+
+    unless (@test_accounts) {
+        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
+        IdPAccountManager::Tools::do_log(
+            'error',
+            "Failed to create test accounts for SP with entityid %s",
+            $self->{'param_in'}{'sp_entityid'}
+        );
+        return undef;
+    }
+
+    ## Update simpleSAMLphp configuration to enable test accounts
+    unless (IdPAccountManager::Tools::update_ssp_authsources()) {
+        push @{ $self->{'param_out'}{'errors'} }, "accounts_creation_failed";
+        IdPAccountManager::Tools::do_log('error',
+            "Failed to create simpleSAMLphp configuration file");
+        return undef;
+    }
+
+    IdPAccountManager::Tools::do_log(
+        'info',
+        "Token validated for sp_entityid=%s;token=%s",
+        $self->{'param_in'}{'sp_entityid'},
+        $self->{'param_in'}{'authentication_token'}
+    );
+
+    $self->{'param_out'}{'sp_entityid'}   = $self->{'param_in'}{'sp_entityid'};
+    $self->{'param_out'}{'test_accounts'} = \@test_accounts;
+
+    return 1;
+}
+
+## Return the homepage of the service
+sub req_home {
+    my $self = shift;
+    IdPAccountManager::Tools::do_log('info', "");
+
+    return 1;
+}
+
+1;