Commit b9e2e918 authored by Yadd's avatar Yadd

TOTP Portal part seems finished (#1359)

TODO: Manager attributes
parent afa0f1d6
...@@ -24,8 +24,8 @@ portalSkin = bootstrap ...@@ -24,8 +24,8 @@ portalSkin = bootstrap
staticPrefix = /static staticPrefix = /static
languages = fr, en, vi, it, ar languages = fr, en, vi, it, ar
templateDir = __pwd__/lemonldap-ng-portal/site/templates templateDir = __pwd__/lemonldap-ng-portal/site/templates
;u2fActivation = 1 ;totp2fActivation = 1
;u2fSelfRegistration = 1 ;totpSelfRegistration = 1
[handler] [handler]
......
...@@ -48,7 +48,7 @@ sub _code { ...@@ -48,7 +48,7 @@ sub _code {
sub newSecret { sub newSecret {
my ($self) = @_; my ($self) = @_;
my @chars = ( 'a' .. 'z', 2 .. 7 ); my @chars = ( 'a' .. 'z', 2 .. 7 );
return join( '', @chars[ map { int( rand(32) ) } 1 .. 16 ] ); return join( '', @chars[ map { int( rand(32) ) } 1 .. 32 ] );
} }
1; 1;
...@@ -66,7 +66,7 @@ sub selfRegister { ...@@ -66,7 +66,7 @@ sub selfRegister {
my $code = $req->param('code'); my $code = $req->param('code');
unless ($code) { unless ($code) {
$self->logger->userInfo('TOTP registration: empty validation form'); $self->logger->userInfo('TOTP registration: empty validation form');
return $self->p->sendError( $req, 'missingCode', 400 ); return $self->p->sendError( $req, 'missingCode', 200 );
} }
my $r = $self->verifyCode( my $r = $self->verifyCode(
$self->conf->{totp2fInterval}, $self->conf->{totp2fInterval},
...@@ -89,7 +89,7 @@ sub selfRegister { ...@@ -89,7 +89,7 @@ sub selfRegister {
# Now code is verified, let's store the master key in persistent data # Now code is verified, let's store the master key in persistent data
$self->p->updatePersistentSession( $req, $self->p->updatePersistentSession( $req,
{ _totp2fSecret => $token->{_totp2fSecret} } ); { _totp2fSecret => $token->{_totp2fSecret} } );
$self->userLogger->logger('TOTP registration succeed'); $self->userLogger->notice('TOTP registration succeed');
return [ 200, [ 'Content-Type' => 'application/json' ], return [ 200, [ 'Content-Type' => 'application/json' ],
['{"result":1}'] ]; ['{"result":1}'] ];
} }
......
...@@ -33,11 +33,12 @@ sub init { ...@@ -33,11 +33,12 @@ sub init {
sub run { sub run {
my ( $self, $req, $token ) = @_; my ( $self, $req, $token ) = @_;
$self->logger->debug('Generate TOTP form');
# Prepare form # Prepare form
my $tmp = $self->p->sendHtml( my $tmp = $self->p->sendHtml(
$req, $req,
'ext2fcheck', 'totp2fcheck',
params => { params => {
SKIN => $self->conf->{portalSkin}, SKIN => $self->conf->{portalSkin},
TOKEN => $token TOKEN => $token
...@@ -51,6 +52,7 @@ sub run { ...@@ -51,6 +52,7 @@ sub run {
sub verify { sub verify {
my ( $self, $req, $session ) = @_; my ( $self, $req, $session ) = @_;
$self->logger->debug('TOTP verification');
my $code; my $code;
unless ( $code = $req->param('code') ) { unless ( $code = $req->param('code') ) {
$self->userLogger->error('TOTP 2F: no code'); $self->userLogger->error('TOTP 2F: no code');
......
...@@ -188,9 +188,12 @@ sub display { ...@@ -188,9 +188,12 @@ sub display {
} }
# 2.3 Case : user authenticated but an error was returned (bas url,...) # 2.3 Case : user authenticated but an error was returned (bas url,...)
elsif ( not $req->datas->{noerror} elsif (
and $req->userData $req->noLoginDisplay
and %{ $req->userData } ) or ( not $req->datas->{noerror}
and $req->userData
and %{ $req->userData } )
)
{ {
$skinfile = 'error'; $skinfile = 'error';
%templateParams = ( %templateParams = (
......
...@@ -16,8 +16,10 @@ our @pList = ( ...@@ -16,8 +16,10 @@ our @pList = (
portalDisplayResetPassword => '::Plugins::MailReset', portalDisplayResetPassword => '::Plugins::MailReset',
portalStatus => '::Plugins::Status', portalStatus => '::Plugins::Status',
cda => '::Plugins::CDA', cda => '::Plugins::CDA',
u2fActivation => '::2F::U2F',
ext2fActivation => '::2F::External2F', ext2fActivation => '::2F::External2F',
totp2fActivation => '::2F::TOTP',
totpSelfRegistration => '::2F::Register::TOTP',
u2fActivation => '::2F::U2F',
u2fSelfRegistration => '::2F::Register::U2F', u2fSelfRegistration => '::2F::Register::U2F',
notification => '::Plugins::Notifications', notification => '::Plugins::Notifications',
portalCheckLogins => '::Plugins::History', portalCheckLogins => '::Plugins::History',
......
...@@ -37,6 +37,10 @@ has customParameters => ( is => 'rw' ); ...@@ -37,6 +37,10 @@ has customParameters => ( is => 'rw' );
# Boolean to indicate that response must be a redirection # Boolean to indicate that response must be a redirection
has mustRedirect => ( is => 'rw' ); has mustRedirect => ( is => 'rw' );
# Boolean to indicate that login form must not be displayed (used to reset
# authentication)
has noLoginDisplay => ( is => 'rw' );
# Store URL for redirections # Store URL for redirections
has urldc => ( is => 'rw' ); has urldc => ( is => 'rw' );
has postUrl => ( is => 'rw' ); has postUrl => ( is => 'rw' );
......
...@@ -34,7 +34,8 @@ has prefix => ( is => 'rw' ); ...@@ -34,7 +34,8 @@ has prefix => ( is => 'rw' );
sub init { sub init {
my ($self) = @_; my ($self) = @_;
$self->addUnauthRoute( $self->prefix . '2fcheck', '_verify', ['POST'] ); $self->addUnauthRoute( $self->prefix . '2fcheck' => '_verify', ['POST'] );
$self->addUnauthRoute( $self->prefix . '2fcheck' => '_redirect', ['GET'] );
my $rule = $self->conf->{ $self->prefix . '2fActivation' }; my $rule = $self->conf->{ $self->prefix . '2fActivation' };
$rule = $self->p->HANDLER->substitute($rule); $rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) { unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
...@@ -46,6 +47,14 @@ sub init { ...@@ -46,6 +47,14 @@ sub init {
1; 1;
} }
sub _redirect {
my ( $self, $req ) = @_;
my $arg = $req->env->{QUERY_STRING};
return [
302, [ Location => $self->conf->{portal} . ( $arg ? "?$arg" : '' ) ], []
];
}
sub _run { sub _run {
my ( $self, $req ) = @_; my ( $self, $req ) = @_;
return PE_OK unless ( $self->rule->( $req, $req->sessionInfo ) ); return PE_OK unless ( $self->rule->( $req, $req->sessionInfo ) );
...@@ -70,6 +79,7 @@ sub _verify { ...@@ -70,6 +79,7 @@ sub _verify {
my $token; my $token;
unless ( $token = $req->param('token') ) { unless ( $token = $req->param('token') ) {
$self->userLogger->error( $self->prefix . ' 2F access without token' ); $self->userLogger->error( $self->prefix . ' 2F access without token' );
$req->mustRedirect(1);
return $self->p->do( $req, [ sub { PE_NOTOKEN } ] ); return $self->p->do( $req, [ sub { PE_NOTOKEN } ] );
} }
...@@ -84,6 +94,7 @@ sub _verify { ...@@ -84,6 +94,7 @@ sub _verify {
# Case error # Case error
if ($res) { if ($res) {
$req->noLoginDisplay(1);
return $self->p->do( $req, [ sub { $res } ] ); return $self->p->do( $req, [ sub { $res } ] );
} }
......
...@@ -23,12 +23,12 @@ getKey = (reset) -> ...@@ -23,12 +23,12 @@ getKey = (reset) ->
$.ajax $.ajax
type: "POST", type: "POST",
url: "#{portal}/totpregister/getkey" url: "#{portal}/totpregister/getkey"
dataType: 'json'
data: data:
newkey: reset newkey: reset
error: displayError error: displayError
# Display key and QR code # Display key and QR code
success: (data) -> success: (data) ->
console.log data
# Generate OTP url # Generate OTP url
s = "otpauth://totp/#{escape(data.portal)}:#{escape(data.user)}?secret=#{data.secret}&issuer=#{escape(data.portal)}" s = "otpauth://totp/#{escape(data.portal)}:#{escape(data.user)}?secret=#{data.secret}&issuer=#{escape(data.portal)}"
# Generate QR code # Generate QR code
...@@ -42,8 +42,34 @@ getKey = (reset) -> ...@@ -42,8 +42,34 @@ getKey = (reset) ->
if data.newkey if data.newkey
setMsg 'yourNewTotpKey', 'warning' setMsg 'yourNewTotpKey', 'warning'
else else
setMsg 'yourTotpKey', 'info' setMsg 'yourTotpKey', 'success'
token = data.token token = data.token
verify = ->
val = $('#code').val()
unless val
setMsg 'fillTheForm', 'danger'
else
$.ajax
type: "POST",
url: "#{portal}/totpregister/verify"
dataType: 'json'
data:
token: token
code: val
error: displayError
success: (data) ->
if data.error
if data.error.match /badCode/
setMsg 'badCode', 'warning'
else
setMsg data.error, 'danger'
else
setMsg 'yourKeyIsRegistered', 'success'
$(document).ready -> $(document).ready ->
getKey(0) getKey(0)
$('#changekey').on 'click', () ->
getKey(1)
$('#verify').on 'click', () ->
verify()
...@@ -5,7 +5,7 @@ LemonLDAP::NG TOTP registration script ...@@ -5,7 +5,7 @@ LemonLDAP::NG TOTP registration script
*/ */
(function() { (function() {
var displayError, getKey, setMsg, token; var displayError, getKey, setMsg, token, verify;
setMsg = function(msg, level) { setMsg = function(msg, level) {
$('#msg').html(window.translate(msg)); $('#msg').html(window.translate(msg));
...@@ -34,13 +34,13 @@ LemonLDAP::NG TOTP registration script ...@@ -34,13 +34,13 @@ LemonLDAP::NG TOTP registration script
return $.ajax({ return $.ajax({
type: "POST", type: "POST",
url: portal + "/totpregister/getkey", url: portal + "/totpregister/getkey",
dataType: 'json',
data: { data: {
newkey: reset newkey: reset
}, },
error: displayError, error: displayError,
success: function(data) { success: function(data) {
var qr, s; var qr, s;
console.log(data);
s = "otpauth://totp/" + (escape(data.portal)) + ":" + (escape(data.user)) + "?secret=" + data.secret + "&issuer=" + (escape(data.portal)); s = "otpauth://totp/" + (escape(data.portal)) + ":" + (escape(data.user)) + "?secret=" + data.secret + "&issuer=" + (escape(data.portal));
qr = new QRious({ qr = new QRious({
element: document.getElementById('qr'), element: document.getElementById('qr'),
...@@ -51,15 +51,51 @@ LemonLDAP::NG TOTP registration script ...@@ -51,15 +51,51 @@ LemonLDAP::NG TOTP registration script
if (data.newkey) { if (data.newkey) {
setMsg('yourNewTotpKey', 'warning'); setMsg('yourNewTotpKey', 'warning');
} else { } else {
setMsg('yourTotpKey', 'info'); setMsg('yourTotpKey', 'success');
} }
return token = data.token; return token = data.token;
} }
}); });
}; };
verify = function() {
var val;
val = $('#code').val();
if (!val) {
return setMsg('fillTheForm', 'danger');
} else {
return $.ajax({
type: "POST",
url: portal + "/totpregister/verify",
dataType: 'json',
data: {
token: token,
code: val
},
error: displayError,
success: function(data) {
if (data.error) {
if (data.error.match(/badCode/)) {
return setMsg('badCode', 'warning');
} else {
return setMsg(data.error, 'danger');
}
} else {
return setMsg('yourKeyIsRegistered', 'success');
}
}
});
}
};
$(document).ready(function() { $(document).ready(function() {
return getKey(0); getKey(0);
$('#changekey').on('click', function() {
return getKey(1);
});
return $('#verify').on('click', function() {
return verify();
});
}); });
}).call(this); }).call(this);
(function(){var a,b,d,c;d=function(e,f){$("#msg").html(window.translate(e));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+f);if(f==="positive"){f="success"}return $("#color").addClass("alert-"+f)};a=function(f,e,h){var g;console.log("Error",h);g=JSON.parse(f.responseText);if(g&&g.error){g=g.error.replace(/.* /,"");console.log("Returned error",g);return d(g,"warning")}};c="";b=function(e){return $.ajax({type:"POST",url:portal+"/totpregister/getkey",data:{newkey:e},error:a,success:function(h){var f,g;console.log(h);g="otpauth://totp/"+(escape(h.portal))+":"+(escape(h.user))+"?secret="+h.secret+"&issuer="+(escape(h.portal));f=new QRious({element:document.getElementById("qr"),value:g,size:150});$("#serialized").text(g);if(h.newkey){d("yourNewTotpKey","warning")}else{d("yourTotpKey","info")}return c=h.token}})};$(document).ready(function(){return b(0)})}).call(this); (function(){var a,b,d,c,e;d=function(f,g){$("#msg").html(window.translate(f));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+g);if(g==="positive"){g="success"}return $("#color").addClass("alert-"+g)};a=function(g,f,i){var h;console.log("Error",i);h=JSON.parse(g.responseText);if(h&&h.error){h=h.error.replace(/.* /,"");console.log("Returned error",h);return d(h,"warning")}};c="";b=function(f){return $.ajax({type:"POST",url:portal+"/totpregister/getkey",dataType:"json",data:{newkey:f},error:a,success:function(i){var g,h;h="otpauth://totp/"+(escape(i.portal))+":"+(escape(i.user))+"?secret="+i.secret+"&issuer="+(escape(i.portal));g=new QRious({element:document.getElementById("qr"),value:h,size:150});$("#serialized").text(h);if(i.newkey){d("yourNewTotpKey","warning")}else{d("yourTotpKey","success")}return c=i.token}})};e=function(){var f;f=$("#code").val();if(!f){return d("fillTheForm","danger")}else{return $.ajax({type:"POST",url:portal+"/totpregister/verify",dataType:"json",data:{token:c,code:f},error:a,success:function(g){if(g.error){if(g.error.match(/badCode/)){return d("badCode","warning")}else{return d(g.error,"danger")}}else{return d("yourKeyIsRegistered","success")}}})}};$(document).ready(function(){b(0);$("#changekey").on("click",function(){return b(1)});return $("#verify").on("click",function(){return e()})})}).call(this);
\ No newline at end of file \ No newline at end of file
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"autoAccept":"تقبل تلقائيا في 30 ثانية", "autoAccept":"تقبل تلقائيا في 30 ثانية",
"back2CasUrl":"التطبيق الذي قمت بتسجيل الخروج منه للتو قد وفرت وصلة قد ترغب في أن تتبعها", "back2CasUrl":"التطبيق الذي قمت بتسجيل الخروج منه للتو قد وفرت وصلة قد ترغب في أن تتبعها",
"back2Portal":"العودة إلى البوابة", "back2Portal":"العودة إلى البوابة",
"badCode":"Bad code",
"cancel":"إلغاء", "cancel":"إلغاء",
"captcha":"كلمة التحقق أو الكابتشا ", "captcha":"كلمة التحقق أو الكابتشا ",
"changeKey": "Generate new key", "changeKey": "Generate new key",
...@@ -120,7 +121,9 @@ ...@@ -120,7 +121,9 @@
"enterExt2fCode":"تم إرسال رمز إليك. الرجاء إدخاله", "enterExt2fCode":"تم إرسال رمز إليك. الرجاء إدخاله",
"enterOpenIDLogin":"الرجاء إدخال تسجيل الدخول الأوبين إيدي الخاص بك", "enterOpenIDLogin":"الرجاء إدخال تسجيل الدخول الأوبين إيدي الخاص بك",
"enterYubikey":"يرجى استخدام يوبي كي الخاص بك", "enterYubikey":"يرجى استخدام يوبي كي الخاص بك",
"enterTotpCode":"Enter TOTP code",
"errorMsg":"رسالة خاطئة", "errorMsg":"رسالة خاطئة",
"fillTheForm":"Fill the form",
"firstName":"الاسم الاول", "firstName":"الاسم الاول",
"forgotPwd":"نسيت كلمة المرور؟", "forgotPwd":"نسيت كلمة المرور؟",
"generatePwd":"إنشاء كلمة المرور تلقائيا", "generatePwd":"إنشاء كلمة المرور تلقائيا",
...@@ -211,6 +214,9 @@ ...@@ -211,6 +214,9 @@
"yourEmail":"بريدك الالكتروني", "yourEmail":"بريدك الالكتروني",
"yourIdentity":"هويتك", "yourIdentity":"هويتك",
"yourIdentityIs":"هويتك هي", "yourIdentityIs":"هويتك هي",
"yourKeyIsRegistered":"Your key is registered",
"yourNewTotpKey":"Your new TOTP key, please test it and enter the code",
"yourPhone":"رقم هاتفك", "yourPhone":"رقم هاتفك",
"yourProfile":"ملفك الشخصي" "yourProfile":"ملفك الشخصي",
"yourTotpKey":"Your TOTP key"
} }
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"autoAccept":"Automatically accept in 30 seconds", "autoAccept":"Automatically accept in 30 seconds",
"back2CasUrl":"The application you just logged out of has provided a link it would like you to follow", "back2CasUrl":"The application you just logged out of has provided a link it would like you to follow",
"back2Portal":"Go back to portal", "back2Portal":"Go back to portal",
"badCode":"Bad code",
"cancel":"Cancel", "cancel":"Cancel",
"captcha":"Captcha", "captcha":"Captcha",
"changeKey": "Generate new key", "changeKey": "Generate new key",
...@@ -119,8 +120,10 @@ ...@@ -119,8 +120,10 @@
"enterCred":"Please enter your credentials", "enterCred":"Please enter your credentials",
"enterExt2fCode":"A code has been sent to you. Please enter it", "enterExt2fCode":"A code has been sent to you. Please enter it",
"enterOpenIDLogin":"Please enter your OpenID login", "enterOpenIDLogin":"Please enter your OpenID login",
"enterTotpCode":"Enter TOTP code",
"enterYubikey":"Please use your Yubikey", "enterYubikey":"Please use your Yubikey",
"errorMsg":"Error Message", "errorMsg":"Error Message",
"fillTheForm":"Fill the form",
"firstName":"First name", "firstName":"First name",
"forgotPwd":"Forgot your password?", "forgotPwd":"Forgot your password?",
"generatePwd":"Generate the password automatically", "generatePwd":"Generate the password automatically",
...@@ -211,6 +214,9 @@ ...@@ -211,6 +214,9 @@
"yourEmail":"Your email", "yourEmail":"Your email",
"yourIdentity":"Your identity", "yourIdentity":"Your identity",
"yourIdentityIs":"Your identity is", "yourIdentityIs":"Your identity is",
"yourKeyIsRegistered":"Your key is registered",
"yourNewTotpKey":"Your new TOTP key, please test it and enter the code",
"yourPhone":"Your phone number", "yourPhone":"Your phone number",
"yourProfile":"Your profile" "yourProfile":"Your profile",
"yourTotpKey":"Your TOTP key"
} }
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"autoAccept":"Automatically accept in 30 seconds", "autoAccept":"Automatically accept in 30 seconds",
"back2CasUrl":"The application you just logged out of has provided a link it would like you to follow", "back2CasUrl":"The application you just logged out of has provided a link it would like you to follow",
"back2Portal":"Go back to portal", "back2Portal":"Go back to portal",
"badCode":"Bad code",
"cancel":"Cancel", "cancel":"Cancel",
"captcha":"Captcha", "captcha":"Captcha",
"changeKey": "Generate new key", "changeKey": "Generate new key",
...@@ -119,8 +120,10 @@ ...@@ -119,8 +120,10 @@
"enterCred":"Please enter your credentials", "enterCred":"Please enter your credentials",
"enterExt2fCode":"A code has been sent to you. Please enter it", "enterExt2fCode":"A code has been sent to you. Please enter it",
"enterOpenIDLogin":"Please enter your OpenID login", "enterOpenIDLogin":"Please enter your OpenID login",
"enterTotpCode":"Enter TOTP code",
"enterYubikey":"Please use your Yubikey", "enterYubikey":"Please use your Yubikey",
"errorMsg":"Error Message", "errorMsg":"Error Message",
"fillTheForm":"Fill the form",
"firstName":"First name", "firstName":"First name",
"forgotPwd":"Forgot your password?", "forgotPwd":"Forgot your password?",
"generatePwd":"Generate the password automatically", "generatePwd":"Generate the password automatically",
...@@ -211,6 +214,9 @@ ...@@ -211,6 +214,9 @@
"yourEmail":"Your email", "yourEmail":"Your email",
"yourIdentity":"Your identity", "yourIdentity":"Your identity",
"yourIdentityIs":"Your identity is", "yourIdentityIs":"Your identity is",
"yourKeyIsRegistered":"Your key is registered",
"yourNewTotpKey":"Your new TOTP key, please test it and enter the code",
"yourPhone":"Your phone number", "yourPhone":"Your phone number",
"yourProfile":"Your profile" "yourProfile":"Your profile",
"yourTotpKey":"Your TOTP key"
} }
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"autoAccept":"Automatically accept in 30 seconds", "autoAccept":"Automatically accept in 30 seconds",
"back2CasUrl":"The application you just logged out of has provided a link it would like you to follow", "back2CasUrl":"The application you just logged out of has provided a link it would like you to follow",
"back2Portal":"Go back to portal", "back2Portal":"Go back to portal",
"badCode":"Bad code",
"cancel":"Cancel", "cancel":"Cancel",
"captcha":"Captcha", "captcha":"Captcha",
"changeKey": "Generate new key", "changeKey": "Generate new key",
...@@ -119,8 +120,10 @@ ...@@ -119,8 +120,10 @@
"enterCred":"Please enter your credentials", "enterCred":"Please enter your credentials",
"enterExt2fCode":"A code has been sent to you. Please enter it", "enterExt2fCode":"A code has been sent to you. Please enter it",
"enterOpenIDLogin":"Please enter your OpenID login", "enterOpenIDLogin":"Please enter your OpenID login",
"enterTotpCode":"Enter TOTP code",
"enterYubikey":"Please use your Yubikey", "enterYubikey":"Please use your Yubikey",
"errorMsg":"Error Message", "errorMsg":"Error Message",
"fillTheForm":"Fill the form",
"firstName":"First name", "firstName":"First name",
"forgotPwd":"Forgot your password?", "forgotPwd":"Forgot your password?",
"generatePwd":"Generate the password automatically", "generatePwd":"Generate the password automatically",
...@@ -211,6 +214,9 @@ ...@@ -211,6 +214,9 @@
"yourEmail":"Your email", "yourEmail":"Your email",
"yourIdentity":"Your identity", "yourIdentity":"Your identity",
"yourIdentityIs":"Your identity is", "yourIdentityIs":"Your identity is",
"yourKeyIsRegistered":"Your key is registered",
"yourNewTotpKey":"Your new TOTP key, please test it and enter the code",
"yourPhone":"Your phone number", "yourPhone":"Your phone number",
"yourProfile":"Your profile" "yourProfile":"Your profile",
"yourTotpKey":"Your TOTP key"
} }
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"autoAccept":"Acceptation automatique dans 30 secondes", "autoAccept":"Acceptation automatique dans 30 secondes",
"back2CasUrl":"Le service duquel vous arrivez a fourni un lien que vous êtes invité à suivre", "back2CasUrl":"Le service duquel vous arrivez a fourni un lien que vous êtes invité à suivre",
"back2Portal":"Retourner au portail",