lemonldap-ng/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/2F/Engines/Default.pm

323 lines
9.6 KiB
Perl
Raw Normal View History

2018-03-22 18:28:36 +01:00
# Default 2FA engine
#
# 2FA engine provides 3 functions and 1 interface:
# - init()
# - run($req): called during auth process after session populating
2018-03-26 10:33:04 +02:00
# - display2fRegisters($req, $session): indicates if a 2F registration is
2018-03-22 18:28:36 +01:00
# available for this user
# - /2fregisters: the URL path that displays 2F registration menu
2018-03-08 16:33:34 +01:00
package Lemonldap::NG::Portal::2F::Engines::Default;
use strict;
use Mouse;
use JSON qw(from_json to_json);
2018-03-08 16:33:34 +01:00
use Lemonldap::NG::Portal::Main::Constants qw(
2018-03-09 13:29:39 +01:00
PE_ERROR
PE_NOTOKEN
2018-03-08 16:33:34 +01:00
PE_OK
2018-03-09 13:29:39 +01:00
PE_SENDRESPONSE
PE_TOKENEXPIRED
2018-03-08 16:33:34 +01:00
);
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Main::Plugin';
2018-03-08 18:51:01 +01:00
# INITIALIZATION
has sfModules => ( is => 'rw', default => sub { [] } );
has sfRModules => ( is => 'rw', default => sub { [] } );
has ott => (
is => 'rw',
default => sub {
my $ott =
$_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
$ott->timeout( $_[0]->{conf}->{formTimeout} );
return $ott;
}
);
2018-03-08 16:33:34 +01:00
sub init {
2018-03-08 18:51:01 +01:00
my ($self) = @_;
# Set default 2F list
2018-04-06 00:10:41 +02:00
#$self->conf->{available2F} ||= 'UTOTP,TOTP,U2F,REST,Ext2F,Yubikey';
#$self->conf->{available2FSelfRegistration} ||= 'TOTP,U2F,Yubikey';
2018-03-08 18:51:01 +01:00
# Load 2F modules
for my $i ( 0 .. 1 ) {
foreach (
split /,\s*/,
2018-03-08 18:51:01 +01:00
$self->conf->{ $i ? 'available2FSelfRegistration' : 'available2F' }
)
{
my $prefix = lc($_);
$prefix =~ s/2f$//i;
# Activation parameter
my $ap = $prefix . ( $i ? '2fSelfRegistration' : '2fActivation' );
$self->logger->debug("Checking $ap");
2018-03-08 18:51:01 +01:00
# Unless $rule, skip loading
if ( $self->conf->{$ap} ) {
2018-03-08 18:51:01 +01:00
$self->logger->debug("Trying to load $_ 2F");
my $m =
$self->p->loadPlugin( $i ? "::2F::Register::$_" : "::2F::$_" )
or return 0;
# Rule and prefix may be modified by 2F module, reread them
my $rule = $self->conf->{$ap};
$prefix = $m->prefix;
# Compile rule
$rule = $self->p->HANDLER->substitute($rule);
unless ( $rule = $self->p->HANDLER->buildSub($rule) ) {
$self->error( 'External 2F rule error: '
. $self->p->HANDLER->tsv->{jail}->error );
return 0;
}
# Store module
push @{ $self->{ $i ? 'sfRModules' : 'sfModules' } },
2018-03-09 13:29:39 +01:00
{ p => $prefix, m => $m, r => $rule };
2018-03-08 18:51:01 +01:00
}
else {
$self->logger->debug(' -> not enabled');
}
2018-03-08 18:51:01 +01:00
}
}
2018-03-08 22:24:02 +01:00
# Enable REST request only if more than 1 2F module is enabled
2018-03-08 22:25:56 +01:00
if ( @{ $self->{sfModules} } > 1 ) {
2018-04-04 23:16:36 +02:00
$self->addUnauthRoute( '2fchoice' => '_choice', ['POST'] );
2018-03-08 22:24:02 +01:00
$self->addUnauthRoute( '2fchoice' => 'redirect', ['GET'] );
}
# Enable 2F registration URL only if a least 1 registration module
# is enabled
if ( @{ $self->{sfRModules} } ) {
# Registration base
2018-03-22 17:23:48 +01:00
$self->addAuthRoute( '2fregisters' => '_displayRegister', ['GET'] );
2018-04-04 23:16:36 +02:00
$self->addAuthRoute( '2fregisters' => 'register', ['POST'] );
}
2018-03-08 16:33:34 +01:00
return 1;
}
2018-03-08 18:51:01 +01:00
# RUNNING METHODS
2018-03-22 18:28:36 +01:00
# public PE_CODE run($req)
#
# run() is called at each authentication, just after sesionInfo populate
2018-03-08 18:51:01 +01:00
sub run {
my ( $self, $req ) = @_;
2018-03-08 18:51:01 +01:00
# Skip 2F unless a module has been registered
return PE_OK unless ( @{ $self->sfModules } );
2018-04-06 00:10:41 +02:00
########################################################################
2018-03-08 18:51:01 +01:00
# Search for authorizated modules for this user
my @am;
foreach my $m ( @{ $self->sfModules } ) {
$self->logger->debug(
'Looking if ' . $m->{m}->prefix . '2F is available' );
if ( $m->{r}->( $req, $req->sessionInfo ) ) {
2018-03-08 18:51:01 +01:00
$self->logger->debug(' -> OK');
push @am, $m->{m};
2018-03-08 18:51:01 +01:00
}
}
2018-04-06 00:10:41 +02:00
########################################################################
2018-03-08 18:51:01 +01:00
# If no 2F modules are authorizated, skipping 2F
# Note that a rule may forbid access after (GrantSession plugin)
return PE_OK unless (@am);
2018-03-08 18:51:01 +01:00
$self->userLogger->info( 'Second factor required for '
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
# Store user data in a token
2018-03-08 18:51:01 +01:00
$req->sessionInfo->{_2fRealSession} = $req->id;
$req->sessionInfo->{_2fUrldc} = $req->urldc;
my $token = $self->ott->createToken( $req->sessionInfo );
2018-03-09 13:29:39 +01:00
delete $req->{authResult};
# If only one 2F is authorizated, display it
unless ($#am) {
my $res = $am[0]->run( $req, $token );
2018-03-09 13:29:39 +01:00
$req->authResult($res);
2018-03-08 18:51:01 +01:00
return $res;
}
# More than 1 2F has been found, display choice
2018-03-08 22:24:02 +01:00
$self->logger->debug("Prepare 2F choice");
my $tpl = $self->p->sendHtml(
$req,
'2fchoice',
params => {
SKIN => $self->conf->{portalSkin},
TOKEN => $token,
2018-03-09 07:17:25 +01:00
MODULES => [ map { { CODE => $_->prefix, LOGO => $_->logo } } @am ],
2018-03-08 22:24:02 +01:00
}
);
2018-03-09 13:29:39 +01:00
$req->response($tpl);
return PE_SENDRESPONSE;
2018-03-08 22:24:02 +01:00
}
2018-03-26 10:33:04 +02:00
# bool public display2fRegisters($req, $session)
2018-03-22 18:28:36 +01:00
#
# Return true if at least 1 register module is available for this user. Used
# by Menu to display or not /2fregisters page
2018-03-26 10:33:04 +02:00
sub display2fRegisters {
2018-04-04 23:16:36 +02:00
my ( $self, $req, $session ) = @_;
2018-03-22 18:28:36 +01:00
foreach my $m ( @{ $self->sfRModules } ) {
2018-04-04 23:16:36 +02:00
return 1 if ( $m->{r}->( $req, $session ) );
2018-03-22 18:28:36 +01:00
}
return 0;
}
2018-03-22 17:23:48 +01:00
sub _choice {
2018-03-09 13:29:39 +01:00
my ( $self, $req ) = @_;
my $token;
# Restore session
unless ( $token = $req->param('token') ) {
$self->userLogger->error( $self->prefix . ' 2F access without token' );
$req->mustRedirect(1);
return $self->p->do( $req, [ sub { PE_NOTOKEN } ] );
}
my $session;
unless ( $session = $self->ott->getToken($token) ) {
$self->userLogger->info('Token expired');
return $self->p->do( $req, [ sub { PE_TOKENEXPIRED } ] );
}
$req->sessionInfo($session);
# New token
$token = $self->ott->createToken($session);
my $ch = $req->param('sf');
foreach my $m ( @{ $self->sfModules } ) {
if ( $m->{m}->prefix eq $ch ) {
my $res = $m->{m}->run( $req, $token );
$req->authResult($res);
return $self->p->do(
$req,
[
sub { $res }, 'controlUrl',
'buildCookie', @{ $self->p->afterDatas },
]
);
}
}
$self->userLogger->error('Bd 2F choice');
return $self->p->lmError( $req, 500 );
}
2018-03-08 22:24:02 +01:00
sub _redirect {
my ( $self, $req ) = @_;
my $arg = $req->env->{QUERY_STRING};
return [
302, [ Location => $self->conf->{portal} . ( $arg ? "?$arg" : '' ) ], []
];
2018-03-08 18:51:01 +01:00
}
2018-03-22 17:23:48 +01:00
sub _displayRegister {
my ( $self, $req, $tpl ) = @_;
# After verifying rule:
# - display template if $tpl
# - else display choice template
if ($tpl) {
my ($m) = grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules };
unless ($m) {
return $self->p->sendError( $req, 'Unexistent register module',
400 );
}
unless ( $m->{r}->( $req, $req->userData ) ) {
2018-04-03 23:03:29 +02:00
return $self->p->sendError( $req, 'Registration not authorized',
403 );
}
return $self->p->sendHtml( $req, $m->{m}->template );
}
# If only one 2F is available, redirect to it
my @am;
foreach my $m ( @{ $self->sfRModules } ) {
$self->logger->debug(
'Looking if ' . $m->{m}->prefix . '2F register is available' );
if ( $m->{r}->( $req, $req->userData ) ) {
push @am,
{
CODE => $m->{m}->prefix,
URL => '/2fregisters/' . $m->{m}->prefix,
LOGO => $m->{m}->logo,
};
}
}
if ( @am == 1 ) {
2018-03-15 22:45:03 +01:00
return [ 302, [ Location => $self->conf->{portal} . $am[0]->{URL} ],
[] ];
}
2018-04-04 23:16:36 +02:00
my $_2FDevices = eval { from_json( $req->userData->{_2FDevices},
{ allow_nonref => 1 } ); };
unless ($_2FDevices) {
2018-04-04 23:16:36 +02:00
$self->logger->debug("No 2F Device found");
$_2FDevices = [];
}
return $self->p->sendHtml(
$req,
'2fregisters',
params => {
2018-04-04 23:16:36 +02:00
SKIN => $self->conf->{portalSkin},
MODULES => \@am,
SFDEVICES => $_2FDevices,
}
);
}
# Check rule and display
sub register {
my ( $self, $req, $tpl, @args ) = @_;
# After verifying rule:
# - call register run method if $tpl
# - else give JSON list of available registers for this user
if ($tpl) {
my ($m) = grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules };
unless ($m) {
return $self->p->sendError( $req, 'Unexistent register module',
400 );
}
unless ( $m->{r}->( $req, $req->userData ) ) {
$self->userLogger->error("$tpl 2F registration refused");
return $self->p->sendError( $req, 'Registration refused', 403 );
}
return $m->{m}->run( $req, @args );
}
my @am;
foreach my $m ( @{ $self->sfRModules } ) {
$self->logger->debug(
'Looking if ' . $m->{m}->prefix . '2F register is available' );
if ( $m->{r}->( $req, $req->userData ) ) {
$self->logger->debug(' -> OK');
my $name = $m->{m}->prefix;
push @am,
{
name => $name,
logo => $m->{m}->logo,
url => "/2fregisters/$name"
};
}
}
return $self->p->sendJSONresponse( $req, \@am );
}
2018-03-08 16:33:34 +01:00
1;