Commit 0538ad1c authored by Xavier Guimard's avatar Xavier Guimard

Add External2F plugin (#1015)

parent 0595f729
......@@ -962,6 +962,19 @@ qr/^(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-zA-Z0-
'test' => qr/^[_a-zA-Z][a-zA-Z0-9_:\-]*$/,
'type' => 'keyTextContainer'
},
'ext2fActivation' => {
'default' => 0,
'type' => 'bool'
},
'ext2fAuthnLevel' => {
'type' => 'int'
},
'ext2FSendCommand' => {
'type' => 'text'
},
'ext2FValidateCommand' => {
'type' => 'text'
},
'facebookAppId' => {
'type' => 'text'
},
......
......@@ -990,6 +990,26 @@ sub attributes {
'Authentication level for users authentified by password+U2F'
},
# External second factor
ext2fActivation => {
type => 'bool',
default => 0,
documentation => 'External second factor activation',
},
ext2FSendCommand => {
type => 'text',
documentation => 'Send command of External second factor',
},
ext2FValidateCommand => {
type => 'text',
documentation => 'Validation command of External second factor',
},
ext2fAuthnLevel => {
type => 'int',
documentation =>
'Authentication level for users authentified by External second factor'
},
# Single session
notifyDeleted => {
default => 1,
......
......@@ -611,6 +611,15 @@ sub tree {
'u2fAuthnLevel'
]
},
{
title => 'external2f',
help => 'external2f.html',
form => 'simpleInputContainer',
nodes => [
'ext2fActivation', 'ext2FSendCommand',
'ext2FValidateCommand', 'ext2fAuthnLevel',
]
},
]
},
{
......
......@@ -209,6 +209,11 @@
"exportedAttr": "SOAP/REST exported attributes",
"exportedHeaders": "Exported headers",
"exportedVars": "Exported Variables",
"external2f": "External 2nd factor",
"ext2fActivation": "Activation",
"ext2fAuthnLevel": "Authentication level",
"ext2FSendCommand": "Send comand",
"ext2FValidateCommand": "Validation command",
"facebookAppId": "Facebook application ID",
"facebookAppSecret": "Facebook application secret",
"facebookAuthnLevel": "Authentication level",
......
......@@ -209,6 +209,11 @@
"exportedAttr": "Attributs exportés par le portail (SOAP/REST)",
"exportedHeaders": "En-têtes exportés",
"exportedVars": "Attributs à exporter",
"external2f": "2nd facteur externe",
"ext2fActivation": "Activation",
"ext2fAuthnLevel": "Niveau de l'authentification",
"ext2FSendCommand": "Commande pour l'envoi",
"ext2FValidateCommand": "Commande pour la validation",
"facebookAppId": "ID de l'application Facebook",
"facebookAppSecret": "Secret de l'application Facebook",
"facebookAuthnLevel": "Niveau d'authentification",
......
......@@ -81,6 +81,7 @@ lib/Lemonldap/NG/Portal/Password/Demo.pm
lib/Lemonldap/NG/Portal/Password/LDAP.pm
lib/Lemonldap/NG/Portal/Password/REST.pm
lib/Lemonldap/NG/Portal/Plugins/CDA.pm
lib/Lemonldap/NG/Portal/Plugins/External2F.pm
lib/Lemonldap/NG/Portal/Plugins/ForceAuth.pm
lib/Lemonldap/NG/Portal/Plugins/GrantSession.pm
lib/Lemonldap/NG/Portal/Plugins/History.pm
......@@ -240,6 +241,7 @@ site/templates/bootstrap/customfooter.tpl
site/templates/bootstrap/customhead.tpl
site/templates/bootstrap/customheader.tpl
site/templates/bootstrap/error.tpl
site/templates/bootstrap/ext2fcheck.tpl
site/templates/bootstrap/footer.tpl
site/templates/bootstrap/header.tpl
site/templates/bootstrap/info.tpl
......
......@@ -18,6 +18,7 @@ our @pList = (
cda => '::Plugins::CDA',
portalForceAuthn => '::Plugins::ForceAuth',
u2fActivation => '::Plugins::U2F',
ext2fActivation => '::Plugins::External2F',
grantSessionRule => '::Plugins::GrantSession',
u2fSelfRegistration => '::Register::U2F',
notification => '::Plugins::Notifications',
......
package Lemonldap::NG::Portal::Plugins::External2F;
use strict;
use Mouse;
use Lemonldap::NG::Portal::Main::Constants qw(
PE_BADCREDENTIALS
PE_ERROR
PE_FORMEMPTY
PE_NOTOKEN
PE_OK
PE_SENDRESPONSE
PE_TOKENEXPIRED
);
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Main::Plugin';
# INTERFACE
sub afterDatas { 'run' }
# INITIALIZATION
has ott => (
is => 'rw',
default => sub {
my $ott;
if ( $_[0]->{conf}->{requireToken} ) {
$ott =
$_[0]->{p}
->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
$ott->timeout( $_[0]->{conf}->{formTimeout} );
}
return $ott;
}
);
sub init {
my ($self) = @_;
$self->addUnauthRoute( ext2fcheck => 'verify', ['POST'] );
foreach (qw(ext2FSendCommand ext2FValidateCommand)) {
unless ( $self->conf->{$_} ) {
$self->error("Missing $_ parameter, aborting");
return 0;
}
}
1;
}
sub run {
my ( $self, $req ) = @_;
# Prepare command and launch it
my $cmd = $self->conf->{ext2FSendCommand};
$cmd =~ s#\$(\w+)#$req->{sessionInfo}->{$1} // ''#ge;
my $err = `$cmd 2>&1 1>/dev/null`;
$self->logger->error($err) if ( length $err );
if ($?) {
return $self->p->do( $req, [ sub { PE_ERROR } ] );
}
# Prepare form
$req->sessionInfo->{_ext2fRealSession} = $req->id;
my $token = $self->ott->createToken( $req->sessionInfo );
$req->id(0);
$self->p->rebuildCookies($req);
my $tmp = $self->p->sendHtml(
$req,
'ext2fcheck',
params => {
SKIN => $self->conf->{portalSkin},
TOKEN => $token
}
);
$self->logger->debug( 'Prepare U2F verification for '
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
$req->response($tmp);
delete $req->{authResult};
return PE_SENDRESPONSE;
}
sub verify {
my ( $self, $req ) = @_;
# Check token
my $token;
unless ( $token = $req->param('token') ) {
$self->userLogger->error('External 2F access without token');
return $self->p->do( $req, [ sub { PE_NOTOKEN } ] );
}
my $code;
unless ( $code = $req->param('code') ) {
$self->userLogger->error('External 2F: no code');
return $self->p->do( $req, [ sub { PE_FORMEMPTY } ] );
}
my $session;
unless ( $session = $self->ott->getToken($token) ) {
$self->userLogger->info('Token expired');
return $self->p->do( $req, [ sub { PE_TOKENEXPIRED } ] );
}
# Prepare command and launch it
my $cmd = $self->conf->{ext2FValidateCommand};
$cmd =~ s#\$code\b#$code#g;
$cmd =~ s#\$(\w+)#$session->{$1} // ''#ge;
my $err = `$cmd 2>&1 1>/dev/null`;
$self->userLogger->error($err) if ( length $err );
if ($?) {
return $self->p->do( $req, [ sub { PE_BADCREDENTIALS } ] );
}
$req->sessionInfo($session);
$req->id( delete $req->sessionInfo->{_ext2fRealSession} );
$self->p->rebuildCookies($req);
$req->mustRedirect(1);
$self->userLogger->notice( 'External verification for '
. $req->sessionInfo->{ $self->conf->{whatToTrace} } );
if ( my $l = $self->conf->{ext2fAuthnLevel} ) {
$self->p->updateSession( $req, { authenticationLevel => $l } );
}
return $self->p->do( $req, [ sub { PE_OK } ] );
}
1;
......@@ -82,7 +82,7 @@ sub run {
sub verify {
my ( $self, $req ) = @_;
# TODO: set sessionInfo with token
# Check token
my $token;
unless ( $token = $req->param('token') ) {
$self->userLogger->error('U2F access without token');
......@@ -94,6 +94,8 @@ sub verify {
$req->error(PE_TOKENEXPIRED);
return $self->fail($req);
}
# Check U2F signature
if ( my $resp = $req->param('signature') ) {
unless ( $self->loadUser($req) == 1 ) {
$req->error(PE_ERROR);
......
......@@ -118,6 +118,7 @@
"currentPwd":"Current password",
"date":"Date",
"enterCred":"Please enter your credentials",
"enterExt2fCode":"A code has been sent to you. Please enter it",
"enterOpenIDLogin":"Please enter your OpenID login",
"enterYubikey":"Please use your Yubikey",
"errorMsg":"Error Message",
......
......@@ -118,6 +118,7 @@
"currentPwd":"Mot de passe actuel",
"date":"Date",
"enterCred":"Merci de vous authentifier",
"enterExt2fCode":"Un code vous a été envoyé, entrez-le ici",
"enterOpenIDLogin":"Entrez votre identifiant OpenID",
"enterYubikey":"Utilisez votre Yubikey",
"errorMsg":"Message d'erreur",
......
<TMPL_INCLUDE NAME="header.tpl">
<div class="message message-positive alert"><span trspan="enterExt2fCode"></span></div>
<form action="/ext2fcheck" method="post" class="password" role="form">
<div class="form">
<div class="form-group input-group">
<input name="code" value="" class="form-control">
<input type="hidden" id="token" name="token" value="<TMPL_VAR NAME="TOKEN">">
</div>
</div>
<div class="buttons">
<button type="submit" class="btn btn-success">
<span class="glyphicon glyphicon-log-in"></span>
<span trspan="connect">Connect</span>
</button>
</div>
</form>
<TMPL_INCLUDE NAME="footer.tpl">
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment