U2F is ready for skin bootstrap (#1148)
This commit is contained in:
parent
8936677deb
commit
a04f5acd1d
|
@ -97,6 +97,7 @@ sub portalTab {
|
|||
80 => 'PORTAL_REGISTERALREADYEXISTS',
|
||||
81 => 'PE_NOTOKEN',
|
||||
82 => 'PE_TOKENEXPIRED',
|
||||
83 => 'PE_U2FFAILED',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -117,6 +117,7 @@ site/coffee/confirm.coffee
|
|||
site/coffee/info.coffee
|
||||
site/coffee/oidcchecksession.coffee
|
||||
site/coffee/portal.coffee
|
||||
site/coffee/u2fcheck.coffee
|
||||
site/coffee/u2fregistration.coffee
|
||||
site/cron/purgeCentralCache
|
||||
site/cron/purgeCentralCache.cron.d
|
||||
|
@ -213,6 +214,8 @@ site/htdocs/static/common/js/portal.js
|
|||
site/htdocs/static/common/js/portal.min.js
|
||||
site/htdocs/static/common/js/u2f-api.js
|
||||
site/htdocs/static/common/js/u2f-api.min.js
|
||||
site/htdocs/static/common/js/u2fcheck.js
|
||||
site/htdocs/static/common/js/u2fcheck.min.js
|
||||
site/htdocs/static/common/js/u2fregistration.js
|
||||
site/htdocs/static/common/js/u2fregistration.min.js
|
||||
site/htdocs/static/common/key.png
|
||||
|
@ -269,6 +272,7 @@ site/templates/bootstrap/password.tpl
|
|||
site/templates/bootstrap/redirect.tpl
|
||||
site/templates/bootstrap/register.tpl
|
||||
site/templates/bootstrap/standardform.tpl
|
||||
site/templates/bootstrap/u2fcheck.tpl
|
||||
site/templates/bootstrap/u2fregister.tpl
|
||||
site/templates/bootstrap/yubikeyform.tpl
|
||||
site/templates/common/background.tpl
|
||||
|
|
|
@ -183,7 +183,7 @@ sub getNotifBack {
|
|||
# All pending notifications have been accepted, restore cookies and
|
||||
# launch 'controlUrl' to restore "urldc" using do()
|
||||
$self->lmLog( 'All pending notifications have been accepted', 'debug' );
|
||||
$self->rebuildCookies($req);
|
||||
$self->p->rebuildCookies($req);
|
||||
return $self->p->do( $req, ['controlUrl'] );
|
||||
}
|
||||
else {
|
||||
|
@ -195,9 +195,6 @@ sub getNotifBack {
|
|||
}
|
||||
}
|
||||
|
||||
*rebuildCookies =
|
||||
\&Lemonldap::NG::Portal::Plugins::Notifications::rebuildCookies;
|
||||
|
||||
sub toForm {
|
||||
my ( $self, @notifs ) = @_;
|
||||
my $i = 0;
|
||||
|
|
|
@ -234,7 +234,7 @@ sub getNotifBack {
|
|||
# All pending notifications have been accepted, restore cookies and
|
||||
# launch 'controlUrl' to restore "urldc" using do()
|
||||
$self->lmLog( 'All pending notifications have been accepted', 'debug' );
|
||||
$self->rebuildCookies($req);
|
||||
$self->p->rebuildCookies($req);
|
||||
return $self->p->do( $req, ['controlUrl'] );
|
||||
}
|
||||
else {
|
||||
|
@ -246,7 +246,4 @@ sub getNotifBack {
|
|||
}
|
||||
}
|
||||
|
||||
*rebuildCookies =
|
||||
\&Lemonldap::NG::Portal::Plugins::Notifications::rebuildCookies;
|
||||
|
||||
1;
|
||||
|
|
|
@ -85,6 +85,7 @@ use constant {
|
|||
PE_REGISTERALREADYEXISTS => 80,
|
||||
PE_NOTOKEN => 81,
|
||||
PE_TOKENEXPIRED => 82,
|
||||
PE_U2FFAILED => 83,
|
||||
};
|
||||
|
||||
# EXPORTER PARAMETERS
|
||||
|
@ -109,7 +110,7 @@ our @EXPORT_OK = qw( PE_SENDRESPONSE PE_INFO PE_REDIRECT PE_DONE PE_OK
|
|||
PE_MAILNOTFOUND PE_PASSWORDFIRSTACCESS PE_MAILCONFIRMOK
|
||||
PE_RADIUSCONNECTFAILED PE_MUST_SUPPLY_OLD_PASSWORD PE_FORBIDDENIP
|
||||
PE_CAPTCHAERROR PE_CAPTCHAEMPTY PE_REGISTERFIRSTACCESS PE_REGISTERFORMEMPTY
|
||||
PE_REGISTERALREADYEXISTS PE_NOTOKEN PE_TOKENEXPIRED HANDLER
|
||||
PE_REGISTERALREADYEXISTS PE_NOTOKEN PE_TOKENEXPIRED HANDLER PE_U2FFAILED
|
||||
);
|
||||
our %EXPORT_TAGS = ( 'all' => [ @EXPORT_OK, 'import' ], );
|
||||
|
||||
|
|
|
@ -449,23 +449,25 @@ sub store {
|
|||
|
||||
sub buildCookie {
|
||||
my ( $self, $req ) = @_;
|
||||
$req->addCookie(
|
||||
$self->cookie(
|
||||
name => $self->conf->{cookieName},
|
||||
value => $req->{id},
|
||||
domain => $self->conf->{domain},
|
||||
secure => $self->conf->{securedCookie},
|
||||
)
|
||||
);
|
||||
if ( $self->conf->{securedCookie} >= 2 ) {
|
||||
if ( $req->id ) {
|
||||
$req->addCookie(
|
||||
$self->cookie(
|
||||
name => $self->conf->{cookieName} . "http",
|
||||
value => $req->{sessionInfo}->{_httpSession},
|
||||
name => $self->conf->{cookieName},
|
||||
value => $req->{id},
|
||||
domain => $self->conf->{domain},
|
||||
secure => 0,
|
||||
secure => $self->conf->{securedCookie},
|
||||
)
|
||||
);
|
||||
if ( $self->conf->{securedCookie} >= 2 ) {
|
||||
$req->addCookie(
|
||||
$self->cookie(
|
||||
name => $self->conf->{cookieName} . "http",
|
||||
value => $req->{sessionInfo}->{_httpSession},
|
||||
domain => $self->conf->{domain},
|
||||
secure => 0,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
PE_OK;
|
||||
}
|
||||
|
|
|
@ -669,4 +669,15 @@ sub sendHtml {
|
|||
return $self->SUPER::sendHtml( $req, $template, %args );
|
||||
}
|
||||
|
||||
sub rebuildCookies {
|
||||
my ( $self, $req ) = @_;
|
||||
my @tmp;
|
||||
for ( my $i = 0 ; $i < @{ $req->{respHeaders} } ; $i += 2 ) {
|
||||
push @tmp, $req->respHeaders->[0], $req->respHeaders->[1]
|
||||
unless ( $req->respHeaders->[0] eq 'Set-Cookie' );
|
||||
}
|
||||
$req->{respHeaders} = \@tmp;
|
||||
$self->buildCookie($req);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -99,7 +99,7 @@ sub checkNotifDuringAuth {
|
|||
if ( $req->{datas}->{notification} =
|
||||
$self->module->checkForNotifications($req) )
|
||||
{
|
||||
$self->rebuildCookies($req);
|
||||
$self->p->rebuildCookies($req);
|
||||
|
||||
# Restore and cipher cookies
|
||||
return PE_NOTIFICATION;
|
||||
|
@ -114,15 +114,4 @@ sub getNotifBack {
|
|||
return $self->module->getNotifBack(@_);
|
||||
}
|
||||
|
||||
sub rebuildCookies {
|
||||
my ( $self, $req ) = @_;
|
||||
my @tmp;
|
||||
for ( my $i = 0 ; $i < @{ $req->{respHeaders} } ; $i += 2 ) {
|
||||
push @tmp, $req->respHeaders->[0], $req->respHeaders->[1]
|
||||
unless ( $req->respHeaders->[0] eq 'Set-Cookie' );
|
||||
}
|
||||
$req->{respHeaders} = \@tmp;
|
||||
$self->p->buildCookie($req);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -6,10 +6,14 @@ package Lemonldap::NG::Portal::Plugins::U2F;
|
|||
|
||||
use strict;
|
||||
use Mouse;
|
||||
use MIME::Base64;
|
||||
use Lemonldap::NG::Portal::Main::Constants qw(
|
||||
PE_BADCREDENTIALS
|
||||
PE_ERROR
|
||||
PE_NOTOKEN
|
||||
PE_OK
|
||||
PE_SENDRESPONSE
|
||||
PE_TOKENEXPIRED
|
||||
PE_U2FFAILED
|
||||
);
|
||||
|
||||
our $VERSION = '2.0.0';
|
||||
|
@ -50,10 +54,30 @@ sub run {
|
|||
if ( my $res = $self->loadUser($req) ) {
|
||||
return PE_ERROR if ( $res == -1 );
|
||||
|
||||
# TODO: remove cookie and set token
|
||||
$req->sessionInfo->{_u2fRealSession} = $req->id;
|
||||
my $token = $self->ott->createToken( $req->sessionInfo );
|
||||
$req->id(0);
|
||||
$self->p->rebuildCookies($req);
|
||||
|
||||
my $challenge = $self->crypter->authenticationChallenge;
|
||||
return $self->sendHtml( $req, 'u2fcheck',
|
||||
params => { CHALLENGE => $challenge } );
|
||||
my $tmp = $self->p->sendHtml(
|
||||
$req,
|
||||
'u2fcheck',
|
||||
params => {
|
||||
PORTAL_URL => $self->conf->{portal},
|
||||
SKIN => $self->conf->{portalSkin},
|
||||
CHALLENGE => $challenge,
|
||||
TOKEN => $token
|
||||
}
|
||||
);
|
||||
$self->lmLog(
|
||||
'Prepare U2F verification for '
|
||||
. $req->sessionInfo->{ $self->conf->{whatToTrace} },
|
||||
"debug"
|
||||
);
|
||||
|
||||
$req->response($tmp);
|
||||
return PE_SENDRESPONSE;
|
||||
}
|
||||
return PE_OK;
|
||||
}
|
||||
|
@ -62,34 +86,70 @@ sub verify {
|
|||
my ( $self, $req ) = @_;
|
||||
|
||||
# TODO: set sessionInfo with token
|
||||
if ( my $resp = $req->param('u2fSignature') ) {
|
||||
my $token;
|
||||
unless ( $token = $req->param('token') ) {
|
||||
$self->p->userError('U2F access without token');
|
||||
$req->error(PE_NOTOKEN);
|
||||
return $self->fail($req);
|
||||
}
|
||||
unless ( $req->sessionInfo( $self->ott->getToken($token) ) ) {
|
||||
$self->p->userInfo('Token expired');
|
||||
$req->error(PE_TOKENEXPIRED);
|
||||
return $self->fail($req);
|
||||
}
|
||||
if ( my $resp = $req->param('signature') ) {
|
||||
unless ( $self->loadUser($req) == 1 ) {
|
||||
return $self->p->do( $req, [ sub { PE_ERROR } ] );
|
||||
$req->error(PE_ERROR);
|
||||
return $self->fail($req);
|
||||
}
|
||||
if ( $self->crypter->authenticationVerify($resp) ) {
|
||||
|
||||
# OK
|
||||
return [ 200, [ 'Content-Type', 'text/plain' ], ['OK'] ];
|
||||
$req->id( $req->sessionInfo->{_u2fRealSession} );
|
||||
delete $req->sessionInfo->{_u2fRealSession};
|
||||
$self->p->rebuildCookies($req);
|
||||
$req->mustRedirect(1);
|
||||
$self->p->userInfo( 'U2F signature verified for '
|
||||
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
|
||||
return $self->p->do( $req, [ sub { PE_OK } ] );
|
||||
}
|
||||
else {
|
||||
return $self->p->do( $req, [ sub { PE_BADCREDENTIALS } ] );
|
||||
$self->p->userNotice( 'Invalid U2F signature for '
|
||||
. $req->sessionInfo->{ $self->conf->{whatToTrace} } . ' ('
|
||||
. Crypt::U2F::Server::u2fclib_getError()
|
||||
. ')' );
|
||||
$req->error(PE_U2FFAILED);
|
||||
return $self->fail($req);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$self->p->userNotice( 'No U2F response for user'
|
||||
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
|
||||
return $self->p->do( $req, [ sub { PE_BADCREDENTIALS } ] );
|
||||
return $self->fail($req);
|
||||
}
|
||||
}
|
||||
|
||||
sub fail {
|
||||
my ( $self, $req ) = @_;
|
||||
return $self->p->sendHtml(
|
||||
$req,
|
||||
'u2fcheck',
|
||||
params => {
|
||||
PORTAL_URL => $self->conf->{portal},
|
||||
AUTH_ERROR => $req->error,
|
||||
AUTH_ERROR_TYPE => $req->error_type,
|
||||
SKIN => $self->conf->{portalSkin},
|
||||
FAILED => 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sub loadUser {
|
||||
my ( $self, $req ) = @_;
|
||||
my ( $kh, $uk );
|
||||
if ( ( $kh = $req->sessionInfo->{_u2fKeyHandle} )
|
||||
and ( $uk = $req->sessionInfo->{_u2fUserKey} ) )
|
||||
{
|
||||
$self->crypter->{keyHandle} = $kh;
|
||||
$self->crypter->{publicKey} = $uk;
|
||||
$self->crypter->{keyHandle} = decode_base64($kh);
|
||||
$self->crypter->{publicKey} = decode_base64($uk);
|
||||
unless ( $self->crypter->setKeyHandle and $self->crypter->setPublicKey )
|
||||
{
|
||||
$self->lmLog(
|
||||
|
|
|
@ -34,20 +34,42 @@ sub run {
|
|||
$req,
|
||||
{
|
||||
_u2fKeyHandle => encode_base64( $keyHandle, '' ),
|
||||
_u2fUserKey => encode_base64( $userKey, '' )
|
||||
_u2fUserKey => encode_base64( $userKey, '' )
|
||||
}
|
||||
);
|
||||
return $self->p->sendHtml(
|
||||
$req,
|
||||
'u2fregister',
|
||||
params => {
|
||||
PORTAL_URL => $self->conf->{portal},
|
||||
SKIN => $self->conf->{portalSkin},
|
||||
SUCCESS => 1
|
||||
}
|
||||
);
|
||||
return $self->p->sendHtml( $req, 'u2fregister',
|
||||
params => { SUCCESS => 1 } );
|
||||
}
|
||||
$self->p->userError( 'U2F Registration failed: '
|
||||
. Crypt::U2F::Server::Simple::lastError() );
|
||||
return $self->p->sendHtml( $req, 'u2fregister',
|
||||
params => { FAILED => 1 } );
|
||||
return $self->p->sendHtml(
|
||||
$req,
|
||||
'u2fregister',
|
||||
params => {
|
||||
PORTAL_URL => $self->conf->{portal},
|
||||
SKIN => $self->conf->{portalSkin},
|
||||
FAILED => 1
|
||||
}
|
||||
);
|
||||
}
|
||||
my $challenge = $self->crypter->registrationChallenge;
|
||||
return $self->p->sendHtml( $req, 'u2fregister',
|
||||
params => { CHALLENGE => $challenge, APPID => $self->origin } );
|
||||
return $self->p->sendHtml(
|
||||
$req,
|
||||
'u2fregister',
|
||||
params => {
|
||||
PORTAL_URL => $self->conf->{portal},
|
||||
SKIN => $self->conf->{portalSkin},
|
||||
CHALLENGE => $challenge,
|
||||
APPID => $self->origin
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
16
lemonldap-ng-portal/site/coffee/u2fcheck.coffee
Normal file
16
lemonldap-ng-portal/site/coffee/u2fcheck.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
###
|
||||
LemonLDAP::NG U2F verify script
|
||||
###
|
||||
|
||||
check = ->
|
||||
registeredKey = [
|
||||
keyHandle: window.datas.keyHandle
|
||||
version: window.datas.version
|
||||
]
|
||||
console.log 'Key: ', registeredKey
|
||||
u2f.sign window.datas.appId, window.datas.challenge, registeredKey, (data) ->
|
||||
$('#verify-data').val JSON.stringify(data)
|
||||
$('#verify-form').submit()
|
||||
|
||||
$(document).ready ->
|
||||
setTimeout check, 1000
|
29
lemonldap-ng-portal/site/htdocs/static/common/js/u2fcheck.js
Normal file
29
lemonldap-ng-portal/site/htdocs/static/common/js/u2fcheck.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Generated by CoffeeScript 1.10.0
|
||||
|
||||
/*
|
||||
LemonLDAP::NG U2F verify script
|
||||
*/
|
||||
|
||||
(function() {
|
||||
var check;
|
||||
|
||||
check = function() {
|
||||
var registeredKey;
|
||||
registeredKey = [
|
||||
{
|
||||
keyHandle: window.datas.keyHandle,
|
||||
version: window.datas.version
|
||||
}
|
||||
];
|
||||
console.log('Key: ', registeredKey);
|
||||
return u2f.sign(window.datas.appId, window.datas.challenge, registeredKey, function(data) {
|
||||
$('#verify-data').val(JSON.stringify(data));
|
||||
return $('#verify-form').submit();
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
return setTimeout(check, 1000);
|
||||
});
|
||||
|
||||
}).call(this);
|
1
lemonldap-ng-portal/site/htdocs/static/common/js/u2fcheck.min.js
vendored
Normal file
1
lemonldap-ng-portal/site/htdocs/static/common/js/u2fcheck.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
(function(){var a;a=function(){var b;b=[{keyHandle:window.datas.keyHandle,version:window.datas.version}];console.log("Key: ",b);return u2f.sign(window.datas.appId,window.datas.challenge,b,function(c){$("#verify-data").val(JSON.stringify(c));return $("#verify-form").submit()})};$(document).ready(function(){return setTimeout(a,1000)})}).call(this);
|
|
@ -82,6 +82,7 @@
|
|||
"PE80":"This address is already used",
|
||||
"PE81":"Invalid authentication attempt",
|
||||
"PE82":"Exceeded authentication timeout",
|
||||
"PE83":"U2F verification failed",
|
||||
"PM3":"The following sessions have been closed",
|
||||
"PM4":"Other active sessions",
|
||||
"PM5":"Remove other sessions",
|
||||
|
@ -187,6 +188,7 @@
|
|||
"SSOSessionInactive":"SSO session inactive",
|
||||
"submit":"Submit",
|
||||
"touchU2fDevice": "Please touch the flashing U2F device now.",
|
||||
"u2fFailed": "U2F verification failed. Retry or contact your administrator",
|
||||
"u2fPermission": "You may be prompted to allow the site permission to access your security keys. After granting permission, the device will start to blink.",
|
||||
"u2fSuccess": "Your key is registered",
|
||||
"updateCdc": "Update Common Domain Cookie",
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"PE80":"Cette adresse est déjà utilisée",
|
||||
"PE81":"Tentative d'authentification invalide",
|
||||
"PE82":"Délai d'authentification dépassé",
|
||||
"PE83":"La vérification U2F a échoué",
|
||||
"PM3":"Les sessions suivantes ont été fermées",
|
||||
"PM4":"Autres sessions ouvertes",
|
||||
"PM5":"Fermer les autres sessions",
|
||||
|
@ -187,6 +188,7 @@
|
|||
"SSOSessionInactive":"Session SSO inactive",
|
||||
"submit":"Envoyer",
|
||||
"touchU2fDevice": "Poser votre doigt sur le périphérique U2F",
|
||||
"u2fFailed": "La vérification U2F a échoué, réessayez ou contactez votre administrateur",
|
||||
"u2fPermission": "Il est possible qu'on vous demande d'autoriser le site à accéder à votre clef. Après votre accord, la clef clignotera.",
|
||||
"u2fSuccess": "Votre clef est enregistrée",
|
||||
"updateCdc": "Mise à jour du cookie de domaine commun",
|
||||
|
|
37
lemonldap-ng-portal/site/templates/bootstrap/u2fcheck.tpl
Normal file
37
lemonldap-ng-portal/site/templates/bootstrap/u2fcheck.tpl
Normal file
|
@ -0,0 +1,37 @@
|
|||
<TMPL_INCLUDE NAME="header.tpl">
|
||||
|
||||
<TMPL_IF NAME="AUTH_ERROR">
|
||||
<div class="message message-<TMPL_VAR NAME="AUTH_ERROR_TYPE"> alert"><span trmsg="<TMPL_VAR NAME="AUTH_ERROR">"></span></div>
|
||||
</TMPL_IF>
|
||||
|
||||
<TMPL_IF NAME="FAILED">
|
||||
<p trspan="u2fFailed"></p>
|
||||
</TMPL_IF>
|
||||
|
||||
<TMPL_IF NAME="CHALLENGE">
|
||||
<div class="message message-positive alert"><span trspan="touchU2fDevice"></span></div>
|
||||
<form id="verify-form" action="/u2fcheck" method="post">
|
||||
<input type="hidden" id="verify-data" name="signature" value="">
|
||||
<input type="hidden" id="token" name="token" value="<TMPL_VAR NAME="TOKEN">">
|
||||
</form>
|
||||
<script type="application/init">
|
||||
<TMPL_VAR NAME="CHALLENGE">
|
||||
</script>
|
||||
<!-- //if:jsminified
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/u2f-api.min.js"></script>
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/u2fcheck.min.js"></script>
|
||||
//else -->
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/u2f-api.js"></script>
|
||||
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/u2fcheck.js"></script>
|
||||
<!-- //endif -->
|
||||
</TMPL_IF>
|
||||
|
||||
<div class="buttons">
|
||||
<a href="<TMPL_VAR NAME="PORTAL_URL">" class="btn btn-primary" role="button">
|
||||
<span class="glyphicon glyphicon-home"></span>
|
||||
<span trspan="goToPortal">Go to portal</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<TMPL_INCLUDE NAME="footer.tpl">
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<TMPL_INCLUDE NAME="header.tpl">
|
||||
|
||||
<TMPL_IF NAME="FAILED">
|
||||
FAILED
|
||||
<div class="message message-warning alert"><span trspan="u2fFailed"></span></div>
|
||||
</TMPL_IF>
|
||||
|
||||
<TMPL_IF NAME="CHALLENGE">
|
||||
<p trspan="touchU2fDevice">Please touch the flashing U2F device now.</p>
|
||||
<div class="message message-positive alert"><span trspan="touchU2fDevice"></span></div>
|
||||
<p trspan="u2fPermission">You may be prompted to allow the site permission to access your security keys. After granting permission, the device will start to blink.</p>
|
||||
<form id="bind-form" action="#" method="post">
|
||||
<input type="hidden" id="bind-data" name="registration" value="">
|
||||
|
@ -23,7 +23,14 @@
|
|||
</TMPL_IF>
|
||||
|
||||
<TMPL_IF NAME="SUCCESS">
|
||||
<h3 trspan="u2fSuccess">Success</h3>
|
||||
<div class="message message-positive alert"><span trspan="u2fSuccess"></span></div>
|
||||
</TMPL_IF>
|
||||
|
||||
<div class="buttons">
|
||||
<a href="<TMPL_VAR NAME="PORTAL_URL">" class="btn btn-primary" role="button">
|
||||
<span class="glyphicon glyphicon-home"></span>
|
||||
<span trspan="goToPortal">Go to portal</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<TMPL_INCLUDE NAME="footer.tpl">
|
||||
|
|
Loading…
Reference in New Issue
Block a user