U2F is ready for skin bootstrap (#1148)

This commit is contained in:
Xavier Guimard 2017-02-07 22:04:49 +00:00
parent 8936677deb
commit a04f5acd1d
17 changed files with 234 additions and 56 deletions

View File

@ -97,6 +97,7 @@ sub portalTab {
80 => 'PORTAL_REGISTERALREADYEXISTS',
81 => 'PE_NOTOKEN',
82 => 'PE_TOKENEXPIRED',
83 => 'PE_U2FFAILED',
};
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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' ], );

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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(

View File

@ -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;

View 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

View 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);

View 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);

View File

@ -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",

View File

@ -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",

View 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>&nbsp;
<span trspan="goToPortal">Go to portal</span>
</a>
</div>
<TMPL_INCLUDE NAME="footer.tpl">

View File

@ -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>&nbsp;
<span trspan="goToPortal">Go to portal</span>
</a>
</div>
<TMPL_INCLUDE NAME="footer.tpl">