264 lines
7.7 KiB
Perl
264 lines
7.7 KiB
Perl
# U2F second factor authentication
|
|
#
|
|
# This plugin handle authentications to ask U2F second factor for users that
|
|
# have registered their U2F key
|
|
package Lemonldap::NG::Portal::2F::U2F;
|
|
|
|
use strict;
|
|
use Mouse;
|
|
use JSON qw(from_json to_json);
|
|
use MIME::Base64 qw(decode_base64url);
|
|
use Lemonldap::NG::Portal::Main::Constants qw(
|
|
PE_OK
|
|
PE_ERROR
|
|
PE_U2FFAILED
|
|
PE_SENDRESPONSE
|
|
PE_BADCREDENTIALS
|
|
);
|
|
|
|
our $VERSION = '2.0.12';
|
|
|
|
extends qw(
|
|
Lemonldap::NG::Portal::Main::SecondFactor
|
|
Lemonldap::NG::Portal::Lib::U2F
|
|
);
|
|
|
|
# INITIALIZATION
|
|
|
|
has rule => ( is => 'rw' );
|
|
has prefix => ( is => 'ro', default => 'u' );
|
|
has logo => ( is => 'rw', default => 'u2f.png' );
|
|
|
|
sub init {
|
|
my ($self) = @_;
|
|
|
|
# If self registration is enabled and "activation" is just set to
|
|
# "enabled", replace the rule to detect if user has registered its key
|
|
if ( $self->conf->{u2fSelfRegistration}
|
|
and $self->conf->{u2fActivation} eq '1' )
|
|
{
|
|
$self->conf->{u2fActivation} =
|
|
'$_2fDevices && $_2fDevices =~ /"type":\s*"U2F"/s';
|
|
}
|
|
return 0
|
|
unless ( $self->Lemonldap::NG::Portal::Main::SecondFactor::init()
|
|
and $self->Lemonldap::NG::Portal::Lib::U2F::init() );
|
|
|
|
1;
|
|
}
|
|
|
|
# RUNNING METHODS
|
|
|
|
# Main method
|
|
sub run {
|
|
my ( $self, $req, $token ) = @_;
|
|
|
|
my $checkLogins = $req->param('checkLogins');
|
|
$self->logger->debug("U2F: checkLogins set") if $checkLogins;
|
|
|
|
my $stayconnected = $req->param('stayconnected');
|
|
$self->logger->debug("U2F: stayconnected set") if $stayconnected;
|
|
|
|
# Check if user is registered
|
|
if ( my $res = $self->loadUser( $req, $req->sessionInfo ) ) {
|
|
return PE_ERROR if ( $res == -1 );
|
|
return PE_U2FFAILED if ( $res == 0 );
|
|
|
|
# Get a challenge (from first key)
|
|
my $data = eval {
|
|
from_json( $req->data->{crypter}->[0]->authenticationChallenge );
|
|
};
|
|
|
|
if ($@) {
|
|
$self->logger->error( Crypt::U2F::Server::u2fclib_getError() );
|
|
return PE_ERROR;
|
|
}
|
|
|
|
# Get registered keys
|
|
my @rk =
|
|
map { { keyHandle => $_->{keyHandle}, version => $data->{version} } }
|
|
@{ $req->data->{crypter} };
|
|
|
|
$self->ott->updateToken( $token, __ch => $data->{challenge} );
|
|
|
|
$self->logger->debug("Prepare U2F verification");
|
|
$self->logger->debug( " -> Send challenge: " . $data->{challenge} );
|
|
|
|
# Serialize data
|
|
$data = to_json( {
|
|
challenge => $data->{challenge},
|
|
appId => $data->{appId},
|
|
registeredKeys => \@rk
|
|
}
|
|
);
|
|
|
|
my $tmp = $self->p->sendHtml(
|
|
$req,
|
|
'u2fcheck',
|
|
params => {
|
|
MAIN_LOGO => $self->conf->{portalMainLogo},
|
|
SKIN => $self->p->getSkin($req),
|
|
DATA => $data,
|
|
TOKEN => $token,
|
|
CHECKLOGINS => $checkLogins,
|
|
STAYCONNECTED => $stayconnected
|
|
}
|
|
);
|
|
|
|
$req->response($tmp);
|
|
return PE_SENDRESPONSE;
|
|
}
|
|
return PE_U2FFAILED;
|
|
}
|
|
|
|
sub verify {
|
|
my ( $self, $req, $session ) = @_;
|
|
my $crypter;
|
|
|
|
# Check U2F signature
|
|
if ( my $resp = $req->param('signature')
|
|
and my $challenge = $req->param('challenge') )
|
|
{
|
|
unless ( $self->loadUser( $req, $session ) == 1 ) {
|
|
$req->error(PE_ERROR);
|
|
return $self->fail($req);
|
|
}
|
|
|
|
$self->logger->debug("Get challenge: $challenge");
|
|
|
|
unless ( $session->{__ch} and $session->{__ch} eq $challenge ) {
|
|
$self->userLogger->error(
|
|
"U2F challenge changed by user: $session->{__ch} / $challenge");
|
|
$req->error(PE_BADCREDENTIALS);
|
|
return $self->fail($req);
|
|
}
|
|
delete $session->{__ch};
|
|
|
|
$self->logger->debug("Get signature: $resp");
|
|
my $data = eval { JSON::from_json($resp) };
|
|
if ($@) {
|
|
$self->logger->error("U2F response error: $@");
|
|
$req->error(PE_ERROR);
|
|
return $self->fail($req);
|
|
}
|
|
$crypter = $_
|
|
foreach grep { $_->{keyHandle} eq $data->{keyHandle} }
|
|
@{ $req->data->{crypter} };
|
|
unless ($crypter) {
|
|
$self->userLogger->error("Unregistered U2F key");
|
|
$req->error(PE_BADCREDENTIALS);
|
|
return $self->fail($req);
|
|
}
|
|
|
|
if ( not $crypter->setChallenge($challenge) ) {
|
|
$self->logger->error(
|
|
$@ ? $@ : Crypt::U2F::Server::Simple::lastError() );
|
|
$req->error(PE_ERROR);
|
|
return $self->fail($req);
|
|
}
|
|
if ( $crypter->authenticationVerify($resp) ) {
|
|
$self->userLogger->info('U2F signature verified');
|
|
return PE_OK;
|
|
}
|
|
else {
|
|
$self->userLogger->notice( 'Invalid U2F signature for '
|
|
. $session->{ $self->conf->{whatToTrace} } . ' ('
|
|
. Crypt::U2F::Server::u2fclib_getError()
|
|
. ')' );
|
|
$req->error(PE_U2FFAILED);
|
|
return $self->fail($req);
|
|
}
|
|
}
|
|
else {
|
|
$self->userLogger->notice( 'No valid U2F response for user'
|
|
. $session->{ $self->conf->{whatToTrace} } );
|
|
$req->authResult(PE_U2FFAILED);
|
|
return $self->fail($req);
|
|
}
|
|
}
|
|
|
|
sub fail {
|
|
my ( $self, $req ) = @_;
|
|
$req->response(
|
|
$self->p->sendHtml(
|
|
$req,
|
|
'u2fcheck',
|
|
params => {
|
|
MAIN_LOGO => $self->conf->{portalMainLogo},
|
|
AUTH_ERROR => $req->error,
|
|
AUTH_ERROR_TYPE => $req->error_type,
|
|
SKIN => $self->p->getSkin($req),
|
|
FAILED => 1
|
|
}
|
|
)
|
|
);
|
|
return PE_SENDRESPONSE;
|
|
}
|
|
|
|
sub loadUser {
|
|
my ( $self, $req, $session ) = @_;
|
|
my ( $kh, $uk, $_2fDevices );
|
|
my @u2fs = ();
|
|
|
|
if ( $session->{_2fDevices} ) {
|
|
$self->logger->debug("Loading 2F Devices ...");
|
|
|
|
# Read existing 2FDevices
|
|
$_2fDevices =
|
|
eval { from_json( $session->{_2fDevices}, { allow_nonref => 1 } ); };
|
|
if ($@) {
|
|
$self->logger->error("Bad encoding in _2fDevices: $@");
|
|
return PE_ERROR;
|
|
}
|
|
$self->logger->debug("2F Device(s) found");
|
|
|
|
$self->logger->debug("Looking for registered U2F key(s) ...");
|
|
foreach (@$_2fDevices) {
|
|
if ( $_->{type} eq 'U2F' ) {
|
|
unless ( $_->{_userKey} and $_->{_keyHandle} ) {
|
|
$self->logger->error(
|
|
"Missing required U2F attributes in storage ($session->{_2fDevices})"
|
|
);
|
|
next;
|
|
}
|
|
$self->logger->debug( "Found U2F key -> _userKey = "
|
|
. $_->{_userKey}
|
|
. " / _keyHandle = "
|
|
. $_->{_keyHandle} );
|
|
$_->{_userKey} = decode_base64url( $_->{_userKey} );
|
|
push @u2fs, $_;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Manage multi u2f keys
|
|
my @crypters;
|
|
if (@u2fs) {
|
|
$self->logger->debug("Generating crypter(s) with uk & kh");
|
|
|
|
foreach (@u2fs) {
|
|
$kh = $_->{_keyHandle};
|
|
$uk = $_->{_userKey};
|
|
$self->logger->debug("Append crypter with kh -> $kh");
|
|
my $c = $self->crypter( keyHandle => $kh, publicKey => $uk );
|
|
if ($c) {
|
|
push @crypters, $c;
|
|
}
|
|
else {
|
|
$self->logger->error(
|
|
'U2F error: ' . Crypt::U2F::Server::u2fclib_getError() );
|
|
}
|
|
}
|
|
return -1 unless @crypters;
|
|
|
|
$req->data->{crypter} = \@crypters;
|
|
return 1;
|
|
}
|
|
else {
|
|
$self->userLogger->info("U2F : user not registered");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
1;
|