Commit 2cce5799 authored by Xavier Guimard's avatar Xavier Guimard

Merge branch 'manager-u2f-module' into 'master'

Manager u2f module

See merge request lemonldap-ng/lemonldap-ng!17
parents 565798b9 3ec64aca
......@@ -8,12 +8,12 @@ sub types {
'authParamsText' => {
'test' => sub {
1;
}
}
},
'blackWhiteList' => {
'test' => sub {
1;
}
}
},
'bool' => {
'msgFail' => '__notABoolean__',
......@@ -36,17 +36,17 @@ sub types {
split( /\n/, $@, 0 ) )
);
return $err ? ( 1, "__badExpression__: $err" ) : 1;
}
}
},
'catAndAppList' => {
'test' => sub {
1;
}
}
},
'file' => {
'test' => sub {
1;
}
}
},
'hostname' => {
'form' => 'text',
......@@ -80,48 +80,48 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
if $_ =~ /exportedvars$/i and defined $conf->{$_}{$val};
}
return 1, "__unknownAttrOrMacro__: $val";
}
}
},
'longtext' => {
'test' => sub {
1;
}
}
},
'menuApp' => {
'test' => sub {
1;
}
}
},
'menuCat' => {
'test' => sub {
1;
}
}
},
'oidcmetadatajson' => {
'test' => sub {
1;
}
}
},
'oidcmetadatajwks' => {
'test' => sub {
1;
}
}
},
'oidcOPMetaDataNode' => {
'test' => sub {
1;
}
}
},
'oidcRPMetaDataNode' => {
'test' => sub {
1;
}
}
},
'password' => {
'msgFail' => '__malformedValue__',
'test' => sub {
1;
}
}
},
'pcre' => {
'form' => 'text',
......@@ -132,7 +132,7 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
}
};
return $@ ? ( 0, "__badRegexp__: $@" ) : 1;
}
}
},
'PerlModule' => {
'form' => 'text',
......@@ -142,17 +142,17 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
'portalskin' => {
'test' => sub {
1;
}
}
},
'portalskinbackground' => {
'test' => sub {
1;
}
}
},
'post' => {
'test' => sub {
1;
}
}
},
'RSAPrivateKey' => {
'test' => sub {
......@@ -160,7 +160,7 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
m[^(?:(?:\-+\s*BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\n]+={0,2}(?:\r?\n\-+\s*END\s+(?:RSA\s+)PRIVATE\s+KEY\s*\-+)?[\r\n]*)?$]s
? 1
: ( 1, '__badPemEncoding__' );
}
}
},
'RSAPublicKey' => {
'test' => sub {
......@@ -168,7 +168,7 @@ m[^(?:(?:\-+\s*BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\n]+=
m[^(?:(?:\-+\s*BEGIN\s+PUBLIC\s+KEY\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\n]+={0,2}(?:\r?\n\-+\s*END\s+PUBLIC\s+KEY\s*\-+)?[\r\n]*)?$]s
? 1
: ( 1, '__badPemEncoding__' );
}
}
},
'RSAPublicKeyOrCertificate' => {
'test' => sub {
......@@ -176,37 +176,37 @@ m[^(?:(?:\-+\s*BEGIN\s+PUBLIC\s+KEY\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\n]+={0,2}(?:\r?\
m[^(?:(?:\-+\s*BEGIN\s+(?:PUBLIC\s+KEY|CERTIFICATE)\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\n]+={0,2}(?:\r?\n\-+\s*END\s+(?:PUBLIC\s+KEY|CERTIFICATE)\s*\-+)?[\r\n]*)?$]s
? 1
: ( 1, '__badPemEncoding__' );
}
}
},
'rule' => {
'test' => sub {
1;
}
}
},
'samlAssertion' => {
'test' => sub {
1;
}
}
},
'samlAttribute' => {
'test' => sub {
1;
}
}
},
'samlIDPMetaDataNode' => {
'test' => sub {
1;
}
}
},
'samlService' => {
'test' => sub {
1;
}
}
},
'samlSPMetaDataNode' => {
'test' => sub {
1;
}
}
},
'select' => {
'test' => sub {
......@@ -216,19 +216,19 @@ m[^(?:(?:\-+\s*BEGIN\s+(?:PUBLIC\s+KEY|CERTIFICATE)\s*\-+\r?\n)?[a-zA-Z0-9/\+\r\
return $test
? 1
: ( 1, "Invalid value '$_[0]' for this select" );
}
}
},
'subContainer' => {
'keyTest' => qr/\w/,
'test' => sub {
1;
}
}
},
'text' => {
'msgFail' => '__malformedValue__',
'test' => sub {
1;
}
}
},
'trool' => {
'msgFail' => '__authorizedValues__: -1, 0, 1',
......@@ -1045,7 +1045,7 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
split( /\n/, $@, 0 ) )
);
return $err ? ( 1, "__badExpression__: $err" ) : 1;
}
}
},
'type' => 'keyTextContainer'
},
......@@ -1217,7 +1217,7 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
and defined $conf->{$_}{$val};
}
return 1, "__unknownAttrOrMacro__: $val";
}
}
},
'type' => 'doubleHash'
},
......@@ -1505,7 +1505,7 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][
split( /\n/, $@, 0 ) )
);
return $err ? ( 1, "__badExpression__: $err" ) : 1;
}
}
},
'type' => 'ruleContainer'
},
......@@ -3001,19 +3001,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'
}
],
......
......@@ -5,13 +5,12 @@ use utf8;
use strict;
use Mouse;
use MIME::Base64 qw(encode_base64 decode_base64);
use Crypt::U2F::Server::Simple;
#use Crypt::U2F::Server::Simple;
use Lemonldap::NG::Common::Session;
use Lemonldap::NG::Common::Conf::Constants;
use Lemonldap::NG::Common::PSGI::Constants;
use Lemonldap::NG::Common::Conf::ReConstants;
#use Lemonldap::NG::Common::IPv6;
use feature 'state';
......@@ -130,12 +129,11 @@ sub u2f {
: ( $s => $params->{$_} );
} keys %$params;
$filters{_session_kind} = $type;
# $filters{_u2fKeyHandle} = '';
push @fields, keys(%filters);
{
my %seen;
@fields = grep { !$seen{$_}++ } @fields;
# @fields = grep { !/\w+/ } @fields;
}
# For now, only one argument can be passed to
......@@ -195,7 +193,7 @@ sub u2f {
}
}
# Display sessions with U2F key registered only
# Display sessions with registered U2F key only
foreach my $session ( keys %$res ) {
delete $res->{$session}
unless ( defined $res->{$session}->{_u2fKeyHandle} and length $res->{$session}->{_u2fKeyHandle} )
......
......@@ -200,7 +200,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
session[key] = '########'
res = []
# 2. Push session keys in reuslt, grouped by categories
# 2. Push session keys in result, grouped by categories
for category, attrs of categories
subres = []
for attr in attrs
......
......@@ -151,39 +151,46 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
$scope.waiting = true
$http['delete']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
#$scope.currentScope.remove()
$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null
#$scope.currentScope.remove()
$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = true
$scope.showT = false
# Add U2F key
$scope.addU2FKey = ->
$scope.waiting = true
$http['put']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = true
$scope.showT = false
$http.get("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = transformSession response.data
$scope.showT = false
# Verify U2F key
$scope.verifyU2FKey = ->
$scope.waiting = true
$http['post']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null
#$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = true
$http.get("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = transformSession response.data
$scope.showT = false
# Open node
$scope.stoggle = (scope) ->
......@@ -231,8 +238,8 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
session[key] = $scope.localeDate value
else if key.match /^(_startTime|_updateTime)$/
session[key] = _stToStr value
else if key.match /^(_u2fKeyHandle|_u2fUserKey)$/
session[key] = '########'
#else if key.match /^(_u2fKeyHandle|_u2fUserKey)$/
# session[key] = '########'
res = []
# 2. Push session keys in result, grouped by categories
......
......@@ -178,34 +178,40 @@
$scope.waiting = true;
$http['delete'](scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
$scope.currentSession = null;
$scope.currentScope.remove();
return $scope.waiting = false;
}, function(resp) {
$scope.currentSession = null;
$scope.currentScope.remove();
return $scope.waiting = false;
});
return $scope.showT = true;
return $scope.showT = false;
};
$scope.addU2FKey = function() {
$scope.waiting = true;
$http['put'](scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
$scope.currentSession = null;
return $scope.waiting = false;
}, function(resp) {
$scope.currentSession = null;
return $scope.waiting = false;
});
return $scope.showT = true;
$scope.showT = false;
$http.get(scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
return $scope.currentSession = transformSession(response.data);
});
return $scope.showT = false;
};
$scope.verifyU2FKey = function() {
$scope.waiting = true;
$http['post'](scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
$scope.currentSession = null;
return $scope.waiting = false;
}, function(resp) {
$scope.currentSession = null;
return $scope.waiting = false;
});
return $scope.showT = true;
$scope.showT = true;
$http.get(scriptname + "u2f/" + sessionType + "/" + $scope.currentSession.id).then(function(response) {
return $scope.currentSession = transformSession(response.data);
});
return $scope.showT = false;
};
$scope.stoggle = function(scope) {
var node;
......@@ -260,8 +266,6 @@
session[key] = $scope.localeDate(value);
} else if (key.match(/^(_startTime|_updateTime)$/)) {
session[key] = _stToStr(value);
} else if (key.match(/^(_u2fKeyHandle|_u2fUserKey)$/)) {
session[key] = '########';
}
}
}
......
......@@ -15,7 +15,7 @@
<div class="navbar navbar-default">
<div class="navbar-collapse">
<ul class="nav navbar-nav" role="grid">
<li><a id="a-persistent" href="#/persistent" role="row"><i class="glyphicon glyphicon-exclamation-sign"></i> {{translate('u2fSessions')}}</a></li>
<li><a id="a-persistent" role="row"><i class="glyphicon glyphicon-exclamation-sign"></i> {{translate('u2fSessions')}}</a></li>
</ul>
</div>
</div>
......@@ -41,10 +41,10 @@
<div class="lmmenu navbar navbar-default" ng-class="{'hidden-xs':!showM}">
<div class="navbar-collapse" ng-class="{'collapse':!showM}" id="formmenu">
<ul class="nav navbar-nav">
<!--
<li ng-if="currentSession" ng-repeat="button in menu.addU2FKey" ng-include="'menubutton.html'"></li>
<li ng-if="currentSession" ng-repeat="button in menu.verifyU2FKey" ng-include="'menubutton.html'"></li>
-->
<li ng-if="currentSession" ng-repeat="button in menu.delU2FKey" ng-include="'menubutton.html'"></li>
<li ng-if="currentSession===null" ng-repeat="button in menu.home" ng-include="'menubutton.html'"></li>
......
......@@ -137,6 +137,24 @@ sub selfRegister {
}
);
}
# Get or generate master key
elsif ( $action eq 'unregister' ) {
$self->p->updatePersistentSession( $req,
{ _totp2fSecret => '' }
);
$self->userLogger->notice('TOTP unregistration succeed');
return [ 200, [ 'Content-Type' => 'application/json' ],
['{"result":1}'] ];
}
}
1;
......@@ -74,37 +74,18 @@ sub run {
return [ 200, [ 'Content-Type' => 'application/json' ], [$challenge] ];
}
if ( $action eq 'unregistration' ) {
my ( $resp, $challenge );
unless ($resp = $req->param('registration')
and $challenge = $req->param('challenge') )
{
return $self->p->sendError( $req, 'Missing registration parameter',
400 );
}
$self->logger->debug("Get unregistration data $resp");
$self->logger->debug("Get challenge $challenge");
eval { $challenge = JSON::from_json($challenge)->{challenge} };
if ($@) {
$self->userLogger->error("Bad challenge: $@");
return $self->p->sendError( $req, 'Bad challenge', 400 );
}
my $c = $self->crypter;
if ( $c->setChallenge($challenge) ) {
my ( $keyHandle, $userKey ) = $c->registrationVerify($resp);
if ( $keyHandle and $userKey ) {
$self->p->updatePersistentSession(
$req,
{
_u2fKeyHandle => '',
_u2fUserKey => ''
}
);
return [
200, [ 'Content-Type' => 'application/json' ],
['{"result":1}']
];
}
}
$self->p->updatePersistentSession(
$req,
{
_u2fKeyHandle => '',
_u2fUserKey => ''
}
);
$self->userLogger->notice('U2F key unregistration succeed');
return [
200, [ 'Content-Type' => 'application/json' ],
['{"result":1}']
];
my $err = Crypt::U2F::Server::Simple::lastError();
$self->userLogger->warn("U2F Unregistration failed: $err");
return $self->p->sendError( $req, $err, 200 );
......@@ -136,6 +117,7 @@ sub run {
}
my $res =
( $req->datas->{crypter}->authenticationVerify($resp) ? 1 : 0 );
#$self->userLogger->notice("res=$res");
return [
200, [ 'Content-Type' => 'application/json' ],
[qq'{"result":$res}']
......
......@@ -52,7 +52,7 @@ getKey = (reset) ->
verify = ->
val = $('#code').val()
unless val
setMsg 'fillTheForm', 'danger'
setMsg 'fillTheForm', 'warning'
else
$.ajax
type: "POST",
......@@ -70,10 +70,22 @@ verify = ->
setMsg data.error, 'danger'
else
setMsg 'yourKeyIsRegistered', 'success'
unregister = ->
$.ajax
type: "POST",
url: "#{portal}/totpregister/unregister"
data: {}
dataType: 'json'
error: displayError
success: (data) ->
setMsg 'yourKeyIsUnregistered', 'success'
$(document).ready ->
getKey(0)
$('#changekey').on 'click', () ->
getKey(1)
$('#verify').on 'click', () ->
verify()
$('#unregister').on 'click', () ->
unregister()
......@@ -55,43 +55,20 @@ register = ->
setMsg 'u2fRegistered', 'positive'
error: displayError
# Registration function (launched by "register" button)
# Unregistration function (launched by "unregister" button)
unregister = ->
# 1 get registration token
$.ajax
type: "POST",
url: "#{portal}u2fregister/unregister"
data: {}
dataType: 'json'
error: displayError
success: (ch) ->
# 2 build response
request = [
challenge: ch.challenge
version: ch.version
]
setMsg 'touchU2fDevice', 'positive'
$('#u2fPermission').show()
u2f.register ch.appId, request, [], (data) ->
$('#u2fPermission').hide()
# Handle errors
if data.errorCode
setMsg 'unableToGetU2FKey', 'warning'
else
# 3 send response
$.ajax
type: "POST"
url: "#{portal}u2fregister/unregistration"
data:
registration: JSON.stringify data
challenge: JSON.stringify ch
dataType: 'json'
success: (resp) ->
if resp.error
setMsg 'u2fFailed', 'warning'
else if resp.result
setMsg 'u2fUnregistered', 'positive'
error: displayError
$.ajax
type: "POST"
url: "#{portal}u2fregister/unregistration"
data: {}
dataType: 'json'
error: displayError
success: (resp) ->
if resp.error
setMsg 'u2fFailed', 'warning'
else if resp.result
setMsg 'u2fUnregistered', 'positive'
error: displayError
# Verification function (launched by "verify" button)
verify = ->
......
// Generated by CoffeeScript 1.10.0
// Generated by CoffeeScript 1.9.3
/*
LemonLDAP::NG TOTP registration script
*/
(function() {
var displayError, getKey, setMsg, token, verify;
var displayError, getKey, setMsg, token, unregister, verify;
setMsg = function(msg, level) {
$('#msg').html(window.translate(msg));
......@@ -68,7 +68,7 @@ LemonLDAP::NG TOTP registration script
var val;
val = $('#code').val();
if (!val) {
return setMsg('fillTheForm', 'danger');
return setMsg('fillTheForm', 'warning');
} else {
return $.ajax({
type: "POST",
......@@ -94,14 +94,28 @@ LemonLDAP::NG TOTP registration script
}
};
unregister = function() {
return $.ajax({
type: "POST",
url: portal + "/totpregister/unregister",
data: {},
dataType: 'json',
error: displayError,
success: function(data) {}
}, setMsg('yourKeyIsUnregistered', 'success'));
};
$(document).ready(function() {
getKey(0);
$('#changekey').on('click', function() {
return getKey(1);
});
return $('#verify').on('click', function() {
$('#verify').on('click', function() {
return verify();
});
return $('#unregister').on('click', function() {
return unregister();
});
});
}).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));if(i.digits!==6){h+="&digits="+i.digits}if(i.interval!==30){h+="&period="+i.interval}g=new QRious({element:document.getElementById("qr"),value:h,size:150});$("#serialized").text(h);if(i.newkey){d("yourNewTotpKey","