diff --git a/bin/account-manager-web.pl b/bin/account-manager-web.pl
new file mode 100755
index 0000000000000000000000000000000000000000..d04e310a47ed7e968bfb9924f974d888c821c88f
--- /dev/null
+++ b/bin/account-manager-web.pl
@@ -0,0 +1,211 @@
+#!/usr/bin/perl
+
+## 15/09/2014, Olivier Salaün
+## Web interface for the eduGAIN Test IdP Account Manager
+## TODO : rename %erreurs e
+
+use strict;
+use utf8;
+use lib "/opt/testidp/IdPAccountManager/lib";
+
+use CGI;
+use CGI::Cookie;
+use CGI::Util;
+use Template;
+use Template::Constants qw( :debug );
+use POSIX;
+
+use IdPAccountManager::TestAccount;
+
+## 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,
+	      );
+
+my %actions = ('select_sp' => {'title_en' => 'Select your Service Provider'    }
+    );
+
+## Gives writes for the group
+umask 0002;
+
+chdir $IdPAccountManager::Conf::global{'root_manager_dir'};
+
+my $request = new WebRequest;
+
+if (defined $request) {
+    $request->execute();
+}
+
+$request->respond();
+
+package WebRequest;
+
+## New web request
+sub new {
+  my $pkg = shift;
+  my $request = {};
+  
+  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'} = \%IdPAccountManager::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'} = 'help';
+  }
+        
+  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 %{$request->{'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'}{'erreurs'}}, "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
+	  my $sub = 'req_'.$self->{'action'};
+      $status = &{$sub}($self);
+
+     }else {
+       ## Inknown action
+       push @{$self->{'param_out'}{'erreurs'}}, "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;
+
+  ## 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]},
+                #DEBUG => 'all',
+                #DEBUG => 'caller',
+                #DEBUG => 'parser'
+            });
+    
+    my $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 @erreurs_admin;
+   foreach my $id_erreur (@{$self->{'param_out'}{'erreurs'}}) {
+     unless ($id_erreur =~ /^(error_x)$/) {
+         push @erreurs_admin, $id_erreur;
+     }
+   }
+      
+   ## Mail notification of admins about the error
+   if (@erreurs_admin) {
+     &IdPAccountManager::Tools::mail_notice('template' => 'templates/mail/notification_generic_error.tt2.eml', 
+ 		 'data' => $self->{'param_out'});
+  }  
+
+}
\ No newline at end of file
diff --git a/lib/IdPAccountManager/Tools.pm b/lib/IdPAccountManager/Tools.pm
index 620d2d3ed10fde754965bbce7c0045024c6466d1..98b1a0948901c11add17b02bea285a5fc10f65c5 100644
--- a/lib/IdPAccountManager/Tools.pm
+++ b/lib/IdPAccountManager/Tools.pm
@@ -135,6 +135,70 @@ sub do_log {
     return 1;
 }
 
+## Send a mail notice
+## Default is to send email to the manager admins, unless other recipients are specified
+## mail_notice(IN)
+## IN is a HASH with expected entries :
+##   template : mail template file
+##   data : data used by the TT2 parser
+sub mail_notice {
+  my %in = @_;
+  my $tt2_file = $in{'template'};
+  my $mail_data = $in{'data'};
+
+  $mail_data->{'conf'} ||= \%IdPAccountManager::Conf::global;
+
+  my $notice_email = $in{'to'} || $IdPAccountManager::Conf::global{'admin_email'};
+  $mail_data->{'to'} = $notice_email;
+  
+  ## Protection to prevent notifications during test dev phases
+  ## Notify only adresses @renater.fr
+  if ($IdPAccountManager::Conf::global{'no_mail_outside'}) {
+	foreach my $email (split /,/, $notice_email) {
+		unless ($email =~ /\@(cru|renater)\.fr$/) {
+			&do_log('error',"Notification to an external address skipped");
+			return undef;
+		}
+	}
+  }
+
+  &do_log('trace', '(template=%s, to=%s)', $in{'template'}, $mail_data->{'to'});
+
+  open SENDMAIL, "|/usr/sbin/sendmail -f ".$IdPAccountManager::Conf::global{'notice_from'}." $notice_email";
+
+  my $tt2 = Template->new(FILTERS => {qencode => [\&qencode, 0]});
+  unless ($tt2->process($tt2_file, $mail_data, \*SENDMAIL)) {
+      &do_log('error', "Erreur TT2 : %s", $tt2->error());
+  }
+  close SENDMAIL;
+}
+
+sub qencode {
+    my $string = shift;
+    # We are not able to determine the name of header field, so assume
+    # longest (maybe) one.    
+    return MIME::EncWords::encode_mimewords(Encode::decode('utf8', $string),
+					    Encoding=>'A',
+					    Charset=> 'utf8',
+					    Field=>"subject");
+}
+
+## usefull to pass parameters to TT2
+sub encode_utf8 ($) {
+	my $string = shift||'';
+	
+	return Encode::encode('utf8', $string);
+}
+
+## usefull to pass parameters to TT2
+sub escape_quotes {
+	my $string = shift;
+	
+	$string =~ s/\'/\\\'/g;
+	
+	return $string;
+}
+
 1; # Magic true value required at end of module
 __END__
 
diff --git a/resources/geant_logo_rgb_300dpi.jpg b/resources/geant_logo_rgb_300dpi.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..820b7dee541a942c219abcbcf329b189e4433a96
Binary files /dev/null and b/resources/geant_logo_rgb_300dpi.jpg differ
diff --git a/templates/mail/notification_generic_error.tt2.eml b/templates/mail/notification_generic_error.tt2.eml
new file mode 100644
index 0000000000000000000000000000000000000000..9908628a5886a4be15b3a70480d029c4a450df13
--- /dev/null
+++ b/templates/mail/notification_generic_error.tt2.eml
@@ -0,0 +1,19 @@
+From: [% conf.app_name %] <[% conf.notice_from %]>
+To: [% to %]
+Subject: [% subject %]
+Content-type: text/plain; charset=UTF-8; format=flowed
+
+You receive this notification as administrator of the eduGAIN Test Account Manager
+An error occured during test account processing.
+
+[% IF error_type == 'skipping_provider_metadata_issue' %]
+Le fichier de méta-données pour le provider [% entityid %] a un format incorrect ou n'a pas pu être téléchargé.
+
+L'URL des méta-données : [% provider.get('metadataurl') %]
+
+Check logs for more details
+[% ELSE %]
+Error: [% error_type %]
+
+Check logs for more details
+[% END %]
diff --git a/templates/web/content.tt2.html b/templates/web/content.tt2.html
new file mode 100644
index 0000000000000000000000000000000000000000..16963a7ebced6cf34be2dc9e4e05d3603b5a7514
--- /dev/null
+++ b/templates/web/content.tt2.html
@@ -0,0 +1,13 @@
+
+
+[% IF action == 'select_sp' %]
+ [% TRY %]
+  [% PROCESS 'templates/select_sp.tt2.html' %]
+ [% CATCH %]
+  An error occured
+ [% END %]
+
+[% ELSE %]
+Error: unknown action
+
+[% END %]
diff --git a/templates/web/index.tt2.html b/templates/web/index.tt2.html
new file mode 100644
index 0000000000000000000000000000000000000000..84f8a4381b8bc49bc9ce50ccf266a0786efe76ec
--- /dev/null
+++ b/templates/web/index.tt2.html
@@ -0,0 +1,153 @@
+Content-Type: text/html
+[% IF cookie %]Set-Cookie: [% cookie %][% END %]
+
+<?xml version="1.0" encoding="utf-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/x
+html1/DTD/xhtml1-transitional.dtd">
+<html xml:lang="[% iso639 = locale.match('^(.)(.)'); iso639.0; iso639.1 %]" xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+
+<link type="text/css" href="/jquery/jquery-ui-1.8.16/css/smoothness/jquery-ui-1.8.16.custom.css" rel="Stylesheet" />	
+<link rel="stylesheet" media="screen" type="text/css" href="/css/renater.css" />
+<link rel="icon" type="image/png" href="/images/favicon.png" />
+
+<script type="text/javascript" src="/jquery/jquery-1.7.min.js"></script>
+<script type="text/javascript" src="/jquery/jquery-ui-1.8.16.custom.min.js"></script>
+
+<SCRIPT TYPE="text/javascript"> 
+<!--
+
+  // To confirm on a link (A HREF)
+  function request_confirm_link(my_url, my_message) {
+    question = confirm(my_message);
+    if (question !="0") {
+         top.location = my_url;
+    }
+  }
+
+function showhide(div){
+    var oDiv = document.getElementById(div);
+    if(oDiv.style.display == "none"){
+        oDiv.style.display = "block";
+    }else{
+        oDiv.style.display = "none";
+    }
+}
+
+function hide(div) {
+    var oDiv = document.getElementById(div);
+    oDiv.style.display = "none";
+}
+
+//-->      
+</SCRIPT>
+
+<STYLE type="text/css"><!--
+
+.login {float: left}
+.menu {float: right}
+.footer {text-align: center}
+.prod {background-color: #028A34}
+h1 {text-align: center}
+li,dd {margin-left:10px;}
+li.parametre {margin-bottom: 15px;}
+dd.parametre {margin-bottom: 15px;}
+div.important{border-style:solid;border-color:black;border-width:1px;background-color:#F5DEB3;padding:5px;}
+.mandatory{color:#e00853;font-style:italic;font-weight:bold;margin:0 3px}
+.notice{border:2px solid #05a;padding:5px 5px 5px 5px;margin:20px;}
+--></STYLE>
+<title> [% PROCESS 'templates/web/title.tt2.html' %] </title>
+
+</head>
+
+<body>
+
+<div id="wrapper">
+  <div id="header">
+	<div id="bandeau">
+		<div class="logo">
+			<img alt="GEANT logo" width="150" src="/resources/geant_logo_rgb_300dpi.jpg"/>
+		</div>
+		<div class="connection">
+	</div>
+
+	<div class="title">
+		[% PROCESS 'templates/web/title.tt2.html' %]        
+	</div>
+
+  </div>
+
+
+  <div id="content">
+
+[% IF erreurs %]
+<div class="ui-widget">
+ [% FOREACH err IN erreurs %]
+
+<p class="ui-state-error ui-corner-all" style="margin-top: 20px; padding: 0 .7em;"><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
+  [% IF lang == 'en' %]Error: [% ELSE %]Erreur : [% END %]
+
+  [% IF err == 'unknown_action' %]
+   [% IF lang == 'en' %]Unknown action[% ELSE %]action non supportée.[% END %]
+
+  [% ELSIF err == 'internal' %]
+   [% IF lang == 'en' %]internal error; administrators of the federation registry have been notified.[% ELSE %]erreur interne ; les administrateurs du guichet de la fédération ont été notifiés.[% END %]
+
+  [% ELSIF (matches = err.match('missing_(\w+)')) %]
+   [% IF lang == 'en' %]missing parameter '[% matches.0 %]'[% ELSE %]paramètre '[% matches.0 %]' manquant.[% END %]
+
+  [% ELSE %]
+   [% err %]
+
+  [% END %] <!-- autorisation -->
+</p>
+<br/>
+ [% END %] <!-- FOREACH -->
+</div>
+[% ELSE %]
+
+
+[% IF notifications %]
+<div class="ui-widget">
+ [% FOREACH notif IN notifications %]
+<p class="ui-state-highlight ui-corner-all" style="border: 2px solid #10427a; background: #DFF1EE;padding: 0.7em;"><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
+  [% IF lang == 'en' %]Notice: [% ELSE %]Notification : [% END %]
+
+  [% IF notif == 'done' %]
+
+  [% IF lang == 'en' %]Operation has been performed[% ELSE %]L'opération a été effectuée;[%END%]
+
+  [% ELSE %]
+
+   [% notif %]<br/>
+
+  [% END %]
+
+ [% END %] <!-- FOREACH -->
+</b></div>
+[% END %] <!-- notifications -->
+
+[% PROCESS 'templates/web/content.tt2.html' %]
+
+[% END %] <!-- IF erreurs -->
+
+<p>
+
+[% IF dump %]
+DUMP :<br>
+
+ [% USE Dumper %]
+ [% Dumper.dump_html(dump) %]
+</p>
+[% END %]
+
+		<div>
+	<hr>
+
+<div align="middle">Test Account Manager [% conf.version %]</div>
+		</div>
+	</div>
+</div>
+</body>
+</html>
diff --git a/templates/web/title.tt2.html b/templates/web/title.tt2.html
new file mode 100644
index 0000000000000000000000000000000000000000..80302a00d4cc7db07e8d7d40797d65b384318920
--- /dev/null
+++ b/templates/web/title.tt2.html
@@ -0,0 +1,6 @@
+[% conf.app_name %] - 
+
+  [% IF actions.$action %]
+    [% FILTER encode_utf8 %][% actions.$action.title %][% END %]
+  [% ELSE %]
+  [% END %]