Merge branch 'portal-multi-U2F-registration' into 'master'

Portal multi u2f registration

See merge request lemonldap-ng/lemonldap-ng!24
This commit is contained in:
Xavier Guimard 2018-04-17 22:48:37 +02:00
commit 22e5cf5bf0
18 changed files with 111 additions and 71 deletions

View File

@ -3004,19 +3004,19 @@ qr/(?:(?:https?):\/\/(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.]
'default' => 0,
'select' => [
{
'k' => '0',
'k' => 0,
'v' => 'unsecuredCookie'
},
{
'k' => '1',
'k' => 1,
'v' => 'securedCookie'
},
{
'k' => '2',
'k' => 2,
'v' => 'doubleCookie'
},
{
'k' => '3',
'k' => 3,
'v' => 'doubleCookieForSingleSession'
}
],

View File

@ -233,7 +233,6 @@ sub run {
return $self->p->sendError( $req, "Corrupted session", 500 );
}
}
else {
$self->logger->debug("No 2F Device found");
$_2fDevices = [];

View File

@ -51,23 +51,47 @@ sub init {
sub run {
my ( $self, $req, $token ) = @_;
my ( $kh, $uk );
# Check if user is registered
if ( my $res = $self->loadUser( $req, $req->sessionInfo ) ) {
return PE_ERROR if ( $res == -1 );
return PE_U2FFAILED if ( $res == 0 );
my $challenge = $req->datas->{crypter}->authenticationChallenge;
$self->ott->updateToken( $token,
__ch => JSON::from_json($challenge)->{challenge} );
# Get a challenge (from first key)
my $data = eval {
from_json( $req->datas->{crypter}->[0]->authenticationChallenge );
};
if ($@) {
$self->logger->error( Crypt::U2F::Server::u2fclib_getError() );
return PE_ERROR;
}
# Get registered keys
my @rk;
foreach ( @{ $req->datas->{crypter} } ) {
my $k = push @rk,
{ keyHandle => $_->{keyHandle}, version => $data->{version} };
}
$self->ott->updateToken( $token, __ch => $data->{challenge} );
# Serialize datas
$data = to_json(
{
challenge => $data->{challenge},
appId => $data->{appId},
registeredKeys => \@rk
}
);
my $tmp = $self->p->sendHtml(
$req,
'u2fcheck',
params => {
SKIN => $self->conf->{portalSkin},
CHALLENGE => $challenge,
TOKEN => $token
SKIN => $self->conf->{portalSkin},
DATA => $data,
TOKEN => $token
}
);
$self->logger->debug("Prepare U2F verification");
@ -91,19 +115,40 @@ sub verify {
}
$self->logger->debug("Get challenge: $challenge");
unless ( $session->{__ch} and $session->{__ch} eq $challenge ) {
$self->userLogger->error("U2F challenge changes by user !!! $session->{__ch}/$challenge");
$self->userLogger->error(
"U2F challenge changes by user !!! $session->{__ch} / $challenge"
);
$req->error(PE_BADCREDENTIALS);
return $self->fail($req);
}
delete $session->{__ch};
if ( not $req->datas->{crypter}->setChallenge($challenge) ) {
$self->logger->debug("Get signature: $resp");
my $data = eval { JSON::from_json($resp) };
if ($@) {
$self->logger->error("U2F response error: $@");
$req->error(PE_ERROR);
return $self->fail($req);
}
my $crypter;
foreach ( @{ $req->datas->{crypter} } ) {
$crypter = $_ if ( $_->{keyHandle} eq $data->{keyHandle} );
}
unless ($crypter) {
$self->userLogger->error("Unregistered U2F key");
$req->error(PE_BADCREDENTIALS);
return $self->fail($req);
}
if ( not $crypter->setChallenge($challenge) ) {
$self->logger->error(
$@ ? $@ : Crypt::U2F::Server::Simple::lastError() );
$req->error(PE_ERROR);
return $self->fail($req);
}
if ( $req->datas->{crypter}->authenticationVerify($resp) ) {
if ( $crypter->authenticationVerify($resp) ) {
$self->userLogger->info('U2F signature verified');
return PE_OK;
}
@ -113,7 +158,6 @@ sub verify {
. Crypt::U2F::Server::u2fclib_getError()
. ')' );
$req->error(PE_U2FFAILED);
$req->authResult(PE_U2FFAILED);
return $self->fail($req);
}
}
@ -160,34 +204,49 @@ sub loadUser {
$self->logger->debug("2F Device(s) found");
foreach (@$_2fDevices) {
$self->logger->debug("Reading U2F keys if exists ...");
$self->logger->debug("Looking for registered U2F key(s) ...");
if ( $_->{type} eq 'U2F' ) {
$self->logger->debug( "_userKey = " . $_->{_userKey} );
$self->logger->debug( "_keyHandle = " . $_->{_keyHandle} );
unless ( $_->{_userKey} and $_->{_userKey} ) {
$self->logger->error(
"Missing required U2F attributes in storage ($session->{_2fDevices})"
);
next;
}
$self->logger->debug( "Found U2F key -> _userKey = "
. $_->{_userKey}
. "/ _keyHandle = "
. $_->{_keyHandle} );
$_->{_userKey} = $self->decode_base64url( $_->{_userKey} );
push @u2fs, $_;
}
}
}
#### TOTO : MANAGE MULTI U2F KEYS
if ( ( $kh = $u2fs[0]{_keyHandle} )
and ( $uk = $u2fs[0]{_userKey} ) )
{
# Manage multi u2f keys
my @crypters;
if (@u2fs) {
$self->logger->debug("kh & uk -> OK");
$req->datas->{crypter} = $self->crypter(
keyHandle => $kh,
publicKey => $uk
);
unless ( $req->datas->{crypter} ) {
$self->logger->error(
'U2F error: ' . Crypt::U2F::Server::u2fclib_getError() );
foreach (@u2fs) {
$kh = $_->{_keyHandle};
$uk = $_->{_userKey};
my $c = $self->crypter( keyHandle => $kh, publicKey => $uk );
if ($c) {
push @crypters, $c;
}
else {
$self->logger->error(
'U2F error: ' . Crypt::U2F::Server::u2fclib_getError() );
}
}
unless (@crypters) {
return -1;
}
$req->datas->{crypter} = \@crypters;
return 1;
}
else {
$self->userLogger->info("U2F: user not registered");
$self->userLogger->info("U2F : user not registered");
return 0;
}
}

View File

@ -3,13 +3,8 @@ 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)
u2f.sign window.datas.appId, window.datas.challenge, window.datas.registeredKeys, (data) ->
$('#verify-data').val JSON.stringify data
$('#verify-challenge').val window.datas.challenge
$('#verify-form').submit()

View File

@ -75,7 +75,7 @@ verify = ->
u2f.sign ch.appId, ch.challenge, request, (data) ->
# Handle errors
if data.errorCode
setMsg 'unableToGetU2FKey', 'warning'
setMsg 'unableToGetKey', 'warning'
else
# 3 send response
$.ajax

View File

@ -1,4 +1,4 @@
// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 1.9.3
/*
LemonLDAP::NG U2F verify script
@ -8,15 +8,7 @@ LemonLDAP::NG U2F verify script
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) {
return u2f.sign(window.datas.appId, window.datas.challenge, window.datas.registeredKeys, function(data) {
$('#verify-data').val(JSON.stringify(data));
$('#verify-challenge').val(window.datas.challenge);
return $('#verify-form').submit();

View File

@ -1 +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));$("#verify-challenge").val(window.datas.challenge);return $("#verify-form").submit()})};$(document).ready(function(){return setTimeout(a,1000)})}).call(this);
(function(){var a;a=function(){return u2f.sign(window.datas.appId,window.datas.challenge,window.datas.registeredKeys,function(b){$("#verify-data").val(JSON.stringify(b));$("#verify-challenge").val(window.datas.challenge);return $("#verify-form").submit()})};$(document).ready(function(){return setTimeout(a,1000)})}).call(this);

View File

@ -92,7 +92,7 @@ LemonLDAP::NG U2F registration script
setMsg('touchU2fDevice', 'positive');
return u2f.sign(ch.appId, ch.challenge, request, function(data) {
if (data.errorCode) {
return setMsg('unableToGetU2FKey', 'warning');
return setMsg('unableToGetKey', 'warning');
} else {
return $.ajax({
type: "POST",

View File

@ -1 +1 @@
(function(){var a,b,c,d;c=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 c(g,"warning")}};b=function(){return $.ajax({type:"POST",url:portal+"2fregisters/u/register",data:{},dataType:"json",error:a,success:function(e){var f;f=[{challenge:e.challenge,version:e.version}];c("touchU2fDevice","positive");$("#u2fPermission").show();return u2f.register(e.appId,f,[],function(g){$("#u2fPermission").hide();if(g.errorCode){return c(g.error,"warning")}else{return $.ajax({type:"POST",url:portal+"2fregisters/u/registration",data:{registration:JSON.stringify(g),challenge:JSON.stringify(e),keyName:$("#keyName").val()},dataType:"json",success:function(h){if(h.error){return c("u2fFailed","warning")}else{if(h.result){return c("yourKeyIsRegistered","positive")}}},error:a})}})}})};d=function(){return $.ajax({type:"POST",url:portal+"2fregisters/u/verify",data:{},dataType:"json",error:a,success:function(e){var f;f=[{keyHandle:e.keyHandle,version:e.version}];c("touchU2fDevice","positive");return u2f.sign(e.appId,e.challenge,f,function(g){if(g.errorCode){return c("unableToGetU2FKey","warning")}else{return $.ajax({type:"POST",url:portal+"2fregisters/u/signature",data:{signature:JSON.stringify(g),challenge:e.challenge},dataType:"json",success:function(h){if(h.error){return c("u2fFailed","warning")}else{if(h.result){return c("yourKeyIsVerified","positive")}}},error:function(i,h,k){return console.log("error",k)}})}})}})};$(document).ready(function(){$("#u2fPermission").hide();$("#register").on("click",b);$("#verify").on("click",d);return $("#goback").attr("href",portal)})}).call(this);
(function(){var a,b,c,d;c=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 c(g,"warning")}};b=function(){return $.ajax({type:"POST",url:portal+"2fregisters/u/register",data:{},dataType:"json",error:a,success:function(e){var f;f=[{challenge:e.challenge,version:e.version}];c("touchU2fDevice","positive");$("#u2fPermission").show();return u2f.register(e.appId,f,[],function(g){$("#u2fPermission").hide();if(g.errorCode){return c(g.error,"warning")}else{return $.ajax({type:"POST",url:portal+"2fregisters/u/registration",data:{registration:JSON.stringify(g),challenge:JSON.stringify(e),keyName:$("#keyName").val()},dataType:"json",success:function(h){if(h.error){return c("u2fFailed","warning")}else{if(h.result){return c("yourKeyIsRegistered","positive")}}},error:a})}})}})};d=function(){return $.ajax({type:"POST",url:portal+"2fregisters/u/verify",data:{},dataType:"json",error:a,success:function(e){var f;f=[{keyHandle:e.keyHandle,version:e.version}];c("touchU2fDevice","positive");return u2f.sign(e.appId,e.challenge,f,function(g){if(g.errorCode){return c("unableToGetKey","warning")}else{return $.ajax({type:"POST",url:portal+"2fregisters/u/signature",data:{signature:JSON.stringify(g),challenge:e.challenge},dataType:"json",success:function(h){if(h.error){return c("u2fFailed","warning")}else{if(h.result){return c("yourKeyIsVerified","positive")}}},error:function(i,h,k){return console.log("error",k)}})}})}})};$(document).ready(function(){$("#u2fPermission").hide();$("#register").on("click",b);$("#verify").on("click",d);return $("#goback").attr("href",portal)})}).call(this);

View File

@ -82,7 +82,7 @@
"PE80":"This address is already used",
"PE81":"Invalid authentication attempt",
"PE82":"Exceeded authentication timeout",
"PE83":"U2F verification failed",
"PE83":"U2F verification failed. Retry or contact your administrator",
"PE84":"You're not authorized to access to this host",
"PE85":"The remote site ask for a newer session (and UpgradeSession plugin isn't loaded). Logout and retry",
"2FManagment":"2ndF Managment",

View File

@ -82,7 +82,7 @@
"PE80":"Esta dirección ya está utilizada",
"PE81":"Invalid authentication attempt",
"PE82":"Exceeded authentication timeout",
"PE83":"U2F verification failed",
"PE83":"U2F verification failed. Retry or contact your administrator",
"PE84":"You're not authorized to access to this host",
"PE85":"The remote site ask for a newer session (and UpgradeSession plugin isn't loaded). Logout and retry",
"2FManagment":"2ndF Managment",

View File

@ -82,7 +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é",
"PE83":"La vérification U2F a échoué. Réessayez ou contactez votre administrateur",
"PE84":"Vous n'êtes pas autorisé à accéder à ce site",
"PE85":"Le site souhaite une authentification plus récente (et le plugin UpgradeSession n'est pas chargé). Déconnectez-vous et réessayez",
"2FManagment":"Gestionnaire 2ndF",

View File

@ -82,7 +82,7 @@
"PE80":"Dit adres is al in gebruik",
"PE81":"Invalid authentication attempt",
"PE82":"Exceeded authentication timeout",
"PE83":"U2F verification failed",
"PE83":"U2F verification failed. Retry or contact your administrator",
"PE84":"You're not authorized to access to this host",
"PE85":"The remote site ask for a newer session (and UpgradeSession plugin isn't loaded). Logout and retry",
"2FManagment":"Gestionnaire 2ndF",

View File

@ -82,7 +82,7 @@
"PE80":"Este endereço já é utilizado",
"PE81":"Invalid authentication attempt",
"PE82":"Exceeded authentication timeout",
"PE83":"U2F verification failed",
"PE83":"U2F verification failed. Retry or contact your administrator",
"PE84":"You're not authorized to access to this host",
"PE85":"The remote site ask for a newer session (and UpgradeSession plugin isn't loaded). Logout and retry",
"2FManagment":"Gestionnaire 2ndF",

View File

@ -82,7 +82,7 @@
"PE80":"This address is already used",
"PE81":"Invalid authentication attempt",
"PE82":"Exceeded authentication timeout",
"PE83":"U2F verification failed",
"PE83":"U2F verification failed. Retry or contact your administrator",
"PE84":"You're not authorized to access to this host",
"PE85":"The remote site ask for a newer session (and UpgradeSession plugin isn't loaded). Logout and retry",
"2FManagment":"Gestionnaire 2ndF",

View File

@ -25,7 +25,7 @@
  
<span id="verify" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-check"></span>&nbsp;
<span trspan="verify">Verify</span>
<span trspan="register">Register</span>
</span>
</div>
</div>

View File

@ -5,12 +5,7 @@
<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">
<TMPL_IF NAME="DATA">
<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="">
@ -18,7 +13,7 @@
<input type="hidden" id="token" name="token" value="<TMPL_VAR NAME="TOKEN">">
</form>
<script type="application/init">
<TMPL_VAR NAME="CHALLENGE">
<TMPL_VAR NAME="DATA">
</script>
<!-- //if:jsminified
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/u2f-api.min.js"></script>

View File

@ -147,22 +147,22 @@ JjTJecOOS+88fK8qL1TrYv5rapIdqUI7aQ==
( $host, $url, $query ) = expectForm( $res, undef, '/u2fcheck', 'token' );
# Get challenge
ok( $res->[2]->[0] =~ /^.*"keyHandle".*$/m, ' get keyHandle' );
$data = $&;
ok( $res->[2]->[0] =~ /^.*"keyHandle":.*$/m, ' get keyHandle' );
$data = "$&";
eval { $data = JSON::from_json($data) };
ok( not($@), ' Content is JSON' )
or explain( [ $@, $data ], 'JSON content' );
# Build U2F signature
$r =
$tester->sign( $data->{appId}, $data->{challenge}, $data->{keyHandle} );
$tester->sign( $data->{appId}, $data->{challenge}, $data->{registeredKeys}->[0]->{keyHandle} );
ok( $r->is_success, ' Good challenge value' ) or diag( $r->error_message );
my $sign = JSON::to_json(
{
errorCode => 0,
signatureData => $r->signature_data,
clientData => $r->client_data,
keyHandle => $data->{keyHandle},
keyHandle => $data->{registeredKeys}->[0]->{keyHandle},
}
);
$sign =