Commit ec856641 authored by Xavier Guimard's avatar Xavier Guimard

Merge branch 'manager-u2f-module_add-u2f-key' into 'master'

Manager u2f module add u2f key

See merge request !16
parents 2c8c7047 575620d0
.gitignore
node_modules
e2e-tests/conf/
e2e-tests/conf
lemonldap-ng-common/MYMETA.json
lemonldap-ng-common/MYMETA.yml
lemonldap-ng-common/Makefile
......
......@@ -52,7 +52,7 @@ sub delSession {
return $self->sendJSONresponse( $req, { result => 1 } );
}
sub delU2FKey {
sub deleteU2FKey {
my ( $self, $req ) = @_;
return $self->sendJSONresponse( $req, { result => 1 } )
if ( $self->{demoMode} );
......@@ -77,6 +77,56 @@ sub delU2FKey {
return $self->sendJSONresponse( $req, { result => 1 } );
}
sub addU2FKey {
my ( $self, $req ) = @_;
return $self->sendJSONresponse( $req, { result => 1 } )
if ( $self->{demoMode} );
my $mod = $self->getMod($req)
or return $self->sendError( $req, undef, 400 );
my $id = $req->params('sessionId')
or return $self->sendError( $req, 'sessionId is missing', 400 );
# Try to read session
my $session = $self->getApacheSession( $mod, $id )
or return $self->sendError( $req, undef, 400 );
# Delete U2F key attributs and update session
$session->data->{_u2fKeyHandle} = 'TOF';
$session->data->{_u2fUserKey} = 'TOF';
$session->update( \%{ $session->data } );
Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id );
if ( $session->error ) {
return $self->sendError( $req, $session->error, 200 );
}
return $self->sendJSONresponse( $req, { result => 1 } );
}
sub verifyU2FKey {
my ( $self, $req ) = @_;
return $self->sendJSONresponse( $req, { result => 1 } )
if ( $self->{demoMode} );
my $mod = $self->getMod($req)
or return $self->sendError( $req, undef, 400 );
my $id = $req->params('sessionId')
or return $self->sendError( $req, 'sessionId is missing', 400 );
# Try to read session
my $session = $self->getApacheSession( $mod, $id )
or return $self->sendError( $req, undef, 400 );
# Delete U2F key attributs and update session
$session->data->{_u2fKeyHandle} = 'OK';
$session->data->{_u2fUserKey} = 'OK';
$session->update( \%{ $session->data } );
Lemonldap::NG::Handler::PSGI::Main->localUnlog( $req, $id );
if ( $session->error ) {
return $self->sendError( $req, $session->error, 200 );
}
return $self->sendJSONresponse( $req, { result => 1 } );
}
sub session {
my ( $self, $req, $id, $skey ) = @_;
my ( %h, $res );
......
......@@ -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',
......@@ -1036,7 +1036,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'
},
......@@ -1208,7 +1208,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'
},
......@@ -1490,7 +1490,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'
},
......@@ -2986,19 +2986,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'
}
],
......
......@@ -4,12 +4,14 @@ use 5.10.0;
use utf8;
use strict;
use Mouse;
use MIME::Base64 qw(encode_base64 decode_base64);
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 Lemonldap::NG::Common::IPv6;
use feature 'state';
......@@ -33,21 +35,27 @@ sub addRoutes {
# READ
->addRoute(
u2f => { ':sessionType' => 'sessions' },
u2f => { ':sessionType' => 'u2f' },
['GET']
)
# DELETE U2F KEY ATTRIBUTS
# DELETE U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'delU2FKey' } },
u2f => { ':sessionType' => { ':sessionId' => 'deleteU2FKey' } },
['DELETE']
);
)
# UPDATE U2F KEY ATTRIBUTS
# ->addRoute(
# u2f => { ':sessionType' => { ':sessionId' => { ':updateSession' } },
# ['PUT']
#);
# ADD U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'registerU2FKey' } },
['PUT']
)
# VERIFY U2F KEY
->addRoute(
u2f => { ':sessionType' => { ':sessionId' => 'verifyU2FKey' } },
['POST']
);
$self->setTypes($conf);
......@@ -56,9 +64,28 @@ sub addRoutes {
$self->{hiddenAttributes} //= "_password";
}
#######################
# II. DISPLAY METHODS #
#######################
############################
# II. REGISTRATION METHODS #
############################
sub registerU2FKey {
my ( $self, $req, $session, $skey ) = @_;
eval 'use Crypt::U2F::Server::Simple';
if ($@) {
$self->error("Can't load U2F library: $@");
return 0;
}
return $self->addU2FKey( $req, $session, $skey );
}
########################
# III. DISPLAY METHODS #
########################
sub u2f {
my ( $self, $req, $session, $skey ) = @_;
......@@ -72,27 +99,21 @@ sub u2f {
or return $self->sendError( $req, undef, 400 );
my $params = $req->parameters();
my $type = delete $params->{sessionType};
$type = $type eq 'global' ? 'SSO' : ucfirst($type);
$type = ucfirst($type);
my $res;
# Case 2: list of sessions
my $whatToTrace = Lemonldap::NG::Handler::PSGI::Main->tsv->{whatToTrace};
# 2.1 Get fields to require
my @fields = ( '_httpSessionType', $self->{ipField}, $whatToTrace );
my @fields = ( '_httpSessionType', $self->{ipField}, $whatToTrace, '_u2fKeyHandle' );
if ( my $groupBy = $params->{groupBy} ) {
$groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/
or $groupBy =~ s/^net(?:4|6|)\(([\w:]+),\d+(?:,\d+)?\)$/$1/;
$groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/;
$groupBy =~ s/^_whatToTrace$/$whatToTrace/o
or push @fields, $groupBy;
}
elsif ( my $order = $params->{orderBy} ) {
$order =~ s/^net(?:4|6|)\(([\w:]+)\)$/$1/;
$order =~ s/^_whatToTrace$/$whatToTrace/o
or push @fields, split( /, /, $order );
}
else {
push @fields, '_utime';
}
......@@ -109,10 +130,12 @@ 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
......@@ -172,62 +195,15 @@ sub u2f {
}
}
# Display sessions with U2F key registered only
foreach my $session ( keys %$res ) {
delete $res->{$session}
unless ( defined $res->{$session}->{_u2fKeyHandle} and length $res->{$session}->{_u2fKeyHandle} )
}
my $total = ( keys %$res );
# 2.4 Special case doubleIp (users connected from more than 1 IP)
if ( defined $params->{doubleIp} ) {
my %r;
# 2.4.1 Store user IP addresses in %r
foreach my $id ( keys %$res ) {
my $entry = $res->{$id};
next if ( $entry->{_httpSessionType} );
$r{ $entry->{$whatToTrace} }->{ $entry->{ $self->{ipField} } }++;
}
# 2.4.2 Store sessions owned by users that has more than one IP address in $r
my $r;
$total = 0;
foreach my $k ( keys %$res ) {
my @tmp = keys %{ $r{ $res->{$k}->{$whatToTrace} } };
if ( @tmp > 1 ) {
$total += 1;
$res->{$k}->{_sessionId} = $k;
push @{ $r->{ $res->{$k}->{$whatToTrace} } }, $res->{$k};
}
}
# 2.4.3 Store these session in an array. Array elements are :
# {
# uid => whatToTraceFieldValue,
# sessions => [
# { session => <session-id-1>, date => <_utime> },
# { session => <session-id-2>, date => <_utime> },
# ]
# }
$res = [];
foreach my $uid ( sort keys %$r ) {
push @$res, {
value => $uid,
count => scalar( @{ $r->{$uid} } ),
sessions => [
map {
{
session => $_->{_sessionId},
date => $_->{_utime}
}
} @{ $r->{$uid} }
]
};
}
}
# 2.4 Order and group by
# $res will become an array ref here (except for doubleIp, already done below).
# If "groupBy" is asked, elements will be like:
# { uid => 'foo.bar', count => 3 }
elsif ( my $group = $req->params('groupBy') ) {
if ( my $group = $req->params('groupBy') ) {
my $r;
$group =~ s/\b_whatToTrace\b/$whatToTrace/o;
......@@ -243,46 +219,6 @@ sub u2f {
$group = $field;
}
# Subnets IPv4
elsif ( $group =~ /^net4\((\w+),(\d)\)$/ ) {
my $field = $1;
my $nb = $2 - 1;
foreach my $k ( keys %$res ) {
if ( $res->{$k}->{$field} =~ /^((((\d+)\.\d+)\.\d+)\.\d+)$/ ) {
my @d = ( $4, $3, $2, $1 );
$r->{ $d[$nb] }++;
}
}
$group = $field;
}
# Subnets IPv6
elsif ( $group =~ /^net6\(([\w:]+),(\d)\)$/ ) {
my $field = $1;
my $bits = $2;
foreach my $k ( keys %$res ) {
$r->{ net6( $res->{$k}->{$field}, $bits ) . "/$bits" }++
if ( isIPv6( $res->{$k}->{$field} ) );
}
}
# Both IPv4 and IPv6
elsif ( $group =~ /^net\(([\w:]+),(\d+),(\d+)\)$/ ) {
my $field = $1;
my $bits = $2;
my $nb = $3 - 1;
foreach my $k ( keys %$res ) {
if ( isIPv6( $res->{$k}->{$field} ) ) {
$r->{ net6( $res->{$k}->{$field}, $bits ) . "/$bits" }++;
}
elsif ( $res->{$k}->{$field} =~ /^((((\d+)\.\d+)\.\d+)\.\d+)$/ )
{
my @d = ( $4, $3, $2, $1 );
$r->{ $d[$nb] }++;
}
}
}
# Simple field groupBy query
elsif ( $group =~ /^\w+$/ ) {
eval {
......@@ -316,47 +252,6 @@ qq{Use of an uninitialized attribute "$group" to group sessions},
];
}
# Else if "orderBy" is asked, $res elements will be like:
# { uid => 'foo.bar', session => <sessionId> }
elsif ( my $f = $req->params('orderBy') ) {
my @fields = split /,/, $f;
my @r = map {
my $tmp = { session => $_ };
foreach my $f (@fields) {
my $s = $f;
$s =~ s/^net(?:4|6|)\(([\w:]+)\)$/$1/;
$tmp->{$s} = $res->{$_}->{$s};
}
$tmp
} keys %$res;
while ( my $f = pop @fields ) {
if ( $f =~ s/^net4\((\w+)\)$/$1/ ) {
@r = sort { cmpIPv4( $a->{$f}, $b->{$f} ); } @r;
}
elsif ( $f =~ s/^net6\(([:\w]+)\)$/$1/ ) {
@r = sort { expand6( $a->{$f} ) cmp expand6( $b->{$f} ); } @r;
}
elsif ( $f =~ s/^net\(([:\w]+)\)$/$1/ ) {
@r = sort {
my $ip1 = $a->{$f};
my $ip2 = $b->{$f};
isIPv6($ip1)
? (
isIPv6($ip2)
? expand6($ip1) cmp expand6($ip2)
: -1
)
: isIPv6($ip2) ? 1
: cmpIPv4( $ip1, $ip2 );
} @r;
}
else {
@r = sort { $a->{$f} cmp $b->{$f} } @r;
}
}
$res = [@r];
}
# Else, $res elements will be like:
# { session => <sessionId>, date => <timestamp> }
else {
......@@ -378,17 +273,4 @@ qq{Use of an uninitialized attribute "$group" to group sessions},
);
}
sub cmpIPv4 {
my @a = split /\./, $_[0];
my @b = split /\./, $_[1];
my $cmp = 0;
F: for ( my $i = 0 ; $i < 4 ; $i++ ) {
if ( $a[$i] != $b[$i] ) {
$cmp = $a[$i] <=> $b[$i];
last F;
}
}
$cmp;
}
1;
......@@ -196,6 +196,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] = '########'
res = []
# 2. Push session keys in reuslt, grouped by categories
......
......@@ -87,10 +87,18 @@ categories =
# Menu entries
menu =
session: [
delU2FKey: [
title: 'deleteU2FKey'
icon: 'trash'
]
addU2FKey: [
title: 'addU2FKey'
icon: 'plus'
]
verifyU2FKey: [
title: 'verifyU2FKey'
icon: 'check'
]
home: []
###
......@@ -138,7 +146,7 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
# SESSION MANAGEMENT
# Delete U2F key attributs
# Delete U2F key
$scope.deleteU2FKey = ->
$scope.waiting = true
$http['delete']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
......@@ -151,6 +159,32 @@ llapp.controller 'SessionsExplorerCtrl', ['$scope', '$translator', '$location',
$scope.waiting = false
$scope.showT = true
# Add U2F key
$scope.addU2FKey = ->
$scope.waiting = true
$http['put']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
$scope.showT = true
# Verify U2F key
$scope.verifyU2FKey = ->
$scope.waiting = true
$http['post']("#{scriptname}u2f/#{sessionType}/#{$scope.currentSession.id}").then (response) ->
$scope.currentSession = null
#$scope.currentScope.remove()
$scope.waiting = false
, (resp) ->
$scope.currentSession = null