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

262 lines
7.6 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.15';
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 "activation" is just set to "enabled",
# replace the rule to detect if user has registered its key
$self->conf->{u2fActivation} = 'has2f("U2F")'
if $self->conf->{u2fActivation} eq '1';
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,
AUTH_ERROR_ROLE => $req->error_role,
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;