You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
9.4 KiB
337 lines
9.4 KiB
package Lemonldap::NG::Manager::2ndFA;
|
|
|
|
use 5.10.0;
|
|
use utf8;
|
|
use strict;
|
|
use Mouse;
|
|
use MIME::Base64 qw(encode_base64 decode_base64);
|
|
|
|
use Lemonldap::NG::Common::Session;
|
|
use Lemonldap::NG::Common::Conf::Constants;
|
|
use Lemonldap::NG::Common::PSGI::Constants;
|
|
use Lemonldap::NG::Common::Conf::ReConstants;
|
|
|
|
use feature 'state';
|
|
|
|
extends 'Lemonldap::NG::Common::Conf::AccessLib',
|
|
'Lemonldap::NG::Common::Session::REST';
|
|
|
|
our $VERSION = '2.0.0';
|
|
|
|
#############################
|
|
# I. INITIALIZATION METHODS #
|
|
#############################
|
|
|
|
use constant defaultRoute => '2ndfa.html#/persistent';
|
|
|
|
sub addRoutes {
|
|
my ( $self, $conf ) = @_;
|
|
|
|
# Remote Procedure are defined in Lemonldap::NG::Common::Session::REST
|
|
# HTML template
|
|
$self->addRoute( '2ndfa.html', undef, ['GET'] )
|
|
|
|
->addRoute(
|
|
sfa => { ':sessionType' => 'sfa' },
|
|
['GET']
|
|
)
|
|
|
|
# DELETE 2FA DEVICE
|
|
->addRoute(
|
|
sfa => { ':sessionType' => { ':sessionId' => 'delete2FA' } },
|
|
['DELETE']
|
|
);
|
|
|
|
## ADD 2FA DEVICE
|
|
#->addRoute(
|
|
#sfa => { ':sessionType' => { ':sessionId' => 'add2FA' } },
|
|
#['PUT']
|
|
#)
|
|
|
|
## VERIFY 2FA DEVICE
|
|
#->addRoute(
|
|
#sfa => { ':sessionType' => { ':sessionId' => 'verify2FA' } },
|
|
#['POST']
|
|
#);
|
|
|
|
$self->setTypes($conf);
|
|
|
|
$self->{ipField} ||= 'ipAddr';
|
|
$self->{multiValuesSeparator} ||= '; ';
|
|
$self->{hiddenAttributes} //= "_password";
|
|
$self->{TOTPCheck} = '1';
|
|
$self->{U2FCheck} = '1';
|
|
$self->{UBKCheck} = '1';
|
|
}
|
|
|
|
###################
|
|
# II. 2FA METHODS #
|
|
###################
|
|
|
|
sub delete2FA {
|
|
|
|
my ( $self, $req, $session, $skey ) = @_;
|
|
|
|
my $mod = $self->getMod($req)
|
|
or return $self->sendError( $req, undef, 400 );
|
|
|
|
my $params = $req->parameters();
|
|
my $type = $params->{type};
|
|
my $epoch = $params->{epoch};
|
|
|
|
if ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ and $epoch ) {
|
|
$self->logger->debug(
|
|
"Call procedure delete2F with type=$type and epoch=$epoch");
|
|
return $self->delete2F( $req, $session, $skey );
|
|
}
|
|
else {
|
|
return $self->sendError( $req, undef, 400 );
|
|
}
|
|
}
|
|
|
|
#sub add2FA {
|
|
|
|
#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 );
|
|
#}
|
|
|
|
#sub verify2FA {
|
|
|
|
#my ( $self, $req, $session, $skey ) = @_;
|
|
|
|
#return $self->addU2FKey( $req, $session, $skey );
|
|
#}
|
|
|
|
########################
|
|
# III. DISPLAY METHODS #
|
|
########################
|
|
|
|
sub sfa {
|
|
my ( $self, $req, $session, $skey ) = @_;
|
|
|
|
# Case 1: only one session is required
|
|
if ($session) {
|
|
|
|
return $self->session( $req, $session, $skey );
|
|
}
|
|
|
|
my $mod = $self->getMod($req)
|
|
or return $self->sendError( $req, undef, 400 );
|
|
my $params = $req->parameters();
|
|
my $type = delete $params->{sessionType};
|
|
$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, '_2fDevices' );
|
|
if ( my $groupBy = $params->{groupBy} ) {
|
|
$groupBy =~ s/^substr\((\w+)(?:,\d+(?:,\d+)?)?\)$/$1/;
|
|
$groupBy =~ s/^_whatToTrace$/$whatToTrace/o
|
|
or push @fields, $groupBy;
|
|
}
|
|
else {
|
|
push @fields, '_utime';
|
|
}
|
|
|
|
# 2.2 Restrict query if possible: search for filters (any query arg that is
|
|
# not a keyword)
|
|
my $moduleOptions = $mod->{options};
|
|
$moduleOptions->{backend} = $mod->{module};
|
|
|
|
# Select 2FA sessions to display
|
|
if ( defined $params->{TOTPCheck}
|
|
or defined $params->{U2FCheck}
|
|
or defined $params->{UBKCheck} )
|
|
{
|
|
$self->{TOTPCheck} = delete $params->{TOTPCheck};
|
|
$self->{U2FCheck} = delete $params->{U2FCheck};
|
|
$self->{UBKCheck} = delete $params->{UBKCheck};
|
|
}
|
|
|
|
my %filters = map {
|
|
my $s = $_;
|
|
$s =~ s/\b_whatToTrace\b/$whatToTrace/o;
|
|
/^groupBy$/
|
|
? ()
|
|
: ( $s => $params->{$_} );
|
|
} keys %$params;
|
|
$filters{_session_kind} = $type;
|
|
|
|
push @fields, keys(%filters);
|
|
{
|
|
my %seen;
|
|
@fields = grep { !$seen{$_}++ } @fields;
|
|
}
|
|
|
|
# For now, only one argument can be passed to
|
|
# Lemonldap::NG::Common::Apache::Session so just the first filter is
|
|
# used
|
|
my ($firstFilter) = sort {
|
|
$filters{$a} =~ m#^[\w:]+/\d+\*?$# ? 1
|
|
: $filters{$b} =~ m#^[\w:]+/\d+\*?$# ? -1
|
|
: $a eq '_session_kind' ? 1
|
|
: $b eq '_session_kind' ? -1
|
|
: $a cmp $b
|
|
} keys %filters;
|
|
|
|
# Check if a '*' is required
|
|
my $function = 'searchOn';
|
|
$function = 'searchOnExpr'
|
|
if ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# }
|
|
( $filters{$firstFilter} ) );
|
|
$self->logger->debug(
|
|
"First filter: $firstFilter = $filters{$firstFilter} ($function)");
|
|
|
|
$res =
|
|
Lemonldap::NG::Common::Apache::Session->$function( $moduleOptions,
|
|
$firstFilter, $filters{$firstFilter}, @fields );
|
|
|
|
return $self->sendJSONresponse(
|
|
$req,
|
|
{
|
|
result => 1,
|
|
count => 0,
|
|
total => 0,
|
|
values => []
|
|
}
|
|
) unless ( $res and %$res );
|
|
|
|
delete $filters{$firstFilter}
|
|
unless ( grep { /\*/ and not m#^[\w:]+/\d+\*?$# }
|
|
( $filters{$firstFilter} ) );
|
|
foreach my $k ( keys %filters ) {
|
|
$self->logger->debug("Removing unless $k =~ /^$filters{$k}\$/");
|
|
if ( $filters{$k} =~ m#^([\w:]+)/(\d+)\*?$# ) {
|
|
my ( $net, $bits ) = ( $1, $2 );
|
|
foreach my $session ( keys %$res ) {
|
|
delete $res->{$session}
|
|
unless ( net6( $res->{$session}->{$k}, $bits ) eq $net );
|
|
}
|
|
}
|
|
else {
|
|
$filters{$k} =~ s/\./\\./g;
|
|
$filters{$k} =~ s/\*/\.\*/g;
|
|
foreach my $session ( keys %$res ) {
|
|
if ( $res->{$session}->{$k} ) {
|
|
delete $res->{$session}
|
|
unless ( $res->{$session}->{$k} =~ /^$filters{$k}$/ );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Filter 2FA sessions if needed
|
|
$self->logger->debug("Filtering 2F sessions...");
|
|
my $all = ( keys %$res );
|
|
|
|
if ( $self->{U2FCheck} eq '2' ) {
|
|
foreach my $session ( keys %$res ) {
|
|
delete $res->{$session}
|
|
unless ( defined $res->{$session}->{_2fDevices}
|
|
and $res->{$session}->{_2fDevices} =~ /"type":\s*"U2F"/s );
|
|
}
|
|
$self->logger->debug("Removing sessions unless U2F key registered");
|
|
}
|
|
if ( $self->{TOTPCheck} eq '2' ) {
|
|
foreach my $session ( keys %$res ) {
|
|
delete $res->{$session}
|
|
unless ( defined $res->{$session}->{_2fDevices}
|
|
and $res->{$session}->{_2fDevices} =~ /"type":\s*"TOTP"/s );
|
|
}
|
|
$self->logger->debug("Removing sessions unless TOTP secret registered");
|
|
}
|
|
if ( $self->{UBKCheck} eq '2' ) {
|
|
foreach my $session ( keys %$res ) {
|
|
delete $res->{$session}
|
|
unless ( defined $res->{$session}->{_2fDevices}
|
|
and $res->{$session}->{_2fDevices} =~ /"type":\s*"UBK"/s );
|
|
}
|
|
$self->logger->debug("Removing sessions unless UBK device registered");
|
|
}
|
|
|
|
my $total = ( keys %$res );
|
|
$self->logger->debug("Session(s) left : $total / $all");
|
|
|
|
if ( my $group = $req->params('groupBy') ) {
|
|
my $r;
|
|
$group =~ s/\b_whatToTrace\b/$whatToTrace/o;
|
|
|
|
# Substrings
|
|
if ( $group =~ /^substr\((\w+)(?:,(\d+)(?:,(\d+))?)?\)$/ ) {
|
|
my ( $field, $length, $start ) = ( $1, $2, $3 );
|
|
$start ||= 0;
|
|
$length = 1 if ( $length < 1 );
|
|
foreach my $k ( keys %$res ) {
|
|
$r->{ substr $res->{$k}->{$field}, $start, $length }++
|
|
if ( $res->{$k}->{$field} );
|
|
}
|
|
$group = $field;
|
|
}
|
|
|
|
# Simple field groupBy query
|
|
elsif ( $group =~ /^\w+$/ ) {
|
|
eval {
|
|
foreach my $k ( keys %$res ) {
|
|
$r->{ $res->{$k}->{$group} }++;
|
|
}
|
|
};
|
|
return $self->sendError(
|
|
$req,
|
|
qq{Use of an uninitialized attribute "$group" to group sessions},
|
|
400
|
|
) if ($@);
|
|
}
|
|
else {
|
|
return $self->sendError( $req, 'Syntax error in groupBy', 400 );
|
|
}
|
|
|
|
# Build result
|
|
$res = [
|
|
sort {
|
|
my @a = ( $a->{value} =~ /^(\d+)(?:\.(\d+))*$/ );
|
|
my @b = ( $b->{value} =~ /^(\d+)(?:\.(\d+))*$/ );
|
|
( @a and @b )
|
|
? ( $a[0] <=> $b[0]
|
|
or $a[1] <=> $b[1]
|
|
or $a[2] <=> $b[2]
|
|
or $a[3] <=> $b[3] )
|
|
: $a->{value} cmp $b->{value}
|
|
}
|
|
map { { value => $_, count => $r->{$_} } } keys %$r
|
|
];
|
|
}
|
|
|
|
# Else, $res elements will be like:
|
|
# { session => <sessionId>, userId => <_session_uid> }
|
|
else {
|
|
$res = [
|
|
sort { $a->{date} <=> $b->{date} }
|
|
map { { session => $_, userId => $res->{$_}->{_session_uid} } }
|
|
keys %$res
|
|
];
|
|
}
|
|
|
|
return $self->sendJSONresponse(
|
|
$req,
|
|
{
|
|
result => 1,
|
|
count => scalar(@$res),
|
|
total => $total,
|
|
values => $res
|
|
}
|
|
);
|
|
}
|
|
|
|
1;
|
|
|