2017-02-04 08:55:47 +01:00
|
|
|
# Self U2F registration
|
2018-02-19 14:15:29 +01:00
|
|
|
package Lemonldap::NG::Portal::2F::Register::U2F;
|
2017-02-02 22:48:32 +01:00
|
|
|
|
|
|
|
use strict;
|
|
|
|
use Mouse;
|
2018-03-29 21:27:35 +02:00
|
|
|
use JSON qw(from_json to_json);
|
|
|
|
|
2019-02-12 18:21:38 +01:00
|
|
|
our $VERSION = '2.1.0';
|
2017-02-02 22:48:32 +01:00
|
|
|
|
2018-10-12 10:04:03 +02:00
|
|
|
extends 'Lemonldap::NG::Portal::Main::Plugin',
|
|
|
|
'Lemonldap::NG::Portal::Lib::U2F';
|
2017-02-02 22:48:32 +01:00
|
|
|
|
2017-02-04 08:55:47 +01:00
|
|
|
# INITIALIZATION
|
2017-02-03 18:14:13 +01:00
|
|
|
|
2018-03-08 20:36:32 +01:00
|
|
|
has prefix => ( is => 'rw', default => 'u' );
|
|
|
|
|
2018-03-15 07:04:52 +01:00
|
|
|
has template => ( is => 'ro', default => 'u2fregister' );
|
|
|
|
|
2018-03-15 22:35:59 +01:00
|
|
|
has logo => ( is => 'rw', default => 'u2f.png' );
|
|
|
|
|
2017-02-02 22:48:32 +01:00
|
|
|
sub init {
|
|
|
|
my ($self) = @_;
|
2017-02-04 08:55:47 +01:00
|
|
|
return 0 unless $self->SUPER::init;
|
2017-02-02 22:48:32 +01:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2017-02-04 08:55:47 +01:00
|
|
|
# RUNNING METHODS
|
|
|
|
|
|
|
|
# Main method
|
2017-02-02 22:48:32 +01:00
|
|
|
sub run {
|
2018-03-15 07:04:52 +01:00
|
|
|
my ( $self, $req, $action ) = @_;
|
2019-04-09 21:48:59 +02:00
|
|
|
my $user = $req->userData->{ $self->conf->{whatToTrace} };
|
|
|
|
unless ($user) {
|
|
|
|
return $self->p->sendError( $req,
|
|
|
|
'No ' . $self->conf->{whatToTrace} . ' found in user data', 500 );
|
|
|
|
}
|
2017-02-02 22:48:32 +01:00
|
|
|
|
2017-02-08 14:01:02 +01:00
|
|
|
if ( $action eq 'register' ) {
|
2018-04-05 19:08:29 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
# Read existing 2FDevices
|
|
|
|
$self->logger->debug("Looking for 2F Devices ...");
|
|
|
|
my $_2fDevices;
|
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
|
|
|
return $self->p->sendError( $req, "Corrupted session", 500 );
|
|
|
|
}
|
|
|
|
}
|
2018-04-05 19:08:29 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
else {
|
2018-04-05 19:08:29 +02:00
|
|
|
$self->logger->debug("No 2F Device found");
|
2018-04-07 13:22:06 +02:00
|
|
|
$_2fDevices = [];
|
2018-04-05 19:08:29 +02:00
|
|
|
}
|
|
|
|
|
2018-04-10 16:15:14 +02:00
|
|
|
# Check if user can register one more 2F device
|
2018-04-07 13:22:06 +02:00
|
|
|
my $size = @$_2fDevices;
|
2018-04-05 19:08:29 +02:00
|
|
|
my $maxSize = $self->conf->{max2FDevices};
|
2018-04-07 13:22:06 +02:00
|
|
|
$self->logger->debug("Registered 2F Device(s) : $size / $maxSize");
|
2018-04-05 19:08:29 +02:00
|
|
|
if ( $size >= $maxSize ) {
|
|
|
|
$self->userLogger->error("Max number of 2F devices is reached !!!");
|
|
|
|
return $self->p->sendError( $req, 'maxNumberof2FDevicesReached',
|
|
|
|
400 );
|
|
|
|
}
|
|
|
|
|
2017-02-08 14:01:02 +01:00
|
|
|
my $challenge = $self->crypter->registrationChallenge;
|
2018-03-19 22:35:39 +01:00
|
|
|
$self->logger->debug("Register challenge: $challenge");
|
|
|
|
return [
|
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'Content-Length' => length($challenge),
|
|
|
|
],
|
|
|
|
[$challenge]
|
|
|
|
];
|
2017-02-08 14:01:02 +01:00
|
|
|
}
|
2018-08-18 18:29:14 +02:00
|
|
|
|
2018-08-13 23:14:42 +02:00
|
|
|
elsif ( $action eq 'registration' ) {
|
2018-03-17 20:37:31 +01:00
|
|
|
my ( $resp, $challenge );
|
2018-03-31 00:16:36 +02:00
|
|
|
$self->logger->debug('Registration response');
|
2018-03-06 22:50:40 +01:00
|
|
|
unless ($resp = $req->param('registration')
|
2018-04-03 21:17:15 +02:00
|
|
|
and $challenge = $req->param('challenge') )
|
2018-03-06 22:50:40 +01:00
|
|
|
{
|
2017-02-08 14:01:02 +01:00
|
|
|
return $self->p->sendError( $req, 'Missing registration parameter',
|
|
|
|
400 );
|
|
|
|
}
|
2017-02-15 07:41:50 +01:00
|
|
|
$self->logger->debug("Get registration data $resp");
|
2018-03-06 22:50:40 +01:00
|
|
|
$self->logger->debug("Get challenge $challenge");
|
2018-03-29 23:11:46 +02:00
|
|
|
eval { $challenge = from_json($challenge)->{challenge} };
|
2018-03-06 22:50:40 +01:00
|
|
|
if ($@) {
|
|
|
|
$self->userLogger->error("Bad challenge: $@");
|
|
|
|
return $self->p->sendError( $req, 'Bad challenge', 400 );
|
|
|
|
}
|
|
|
|
my $c = $self->crypter;
|
|
|
|
if ( $c->setChallenge($challenge) ) {
|
2018-03-17 20:37:31 +01:00
|
|
|
my ( $keyHandle, $userKey ) = $c->registrationVerify($resp);
|
|
|
|
if ( $keyHandle and $userKey ) {
|
2018-03-29 23:11:46 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
# Read existing 2FDevices
|
|
|
|
$self->logger->debug("Looking for 2F Devices ...");
|
|
|
|
my $_2fDevices;
|
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json(
|
|
|
|
$req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 }
|
|
|
|
);
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error(
|
|
|
|
"Corrupted session (_2fDevices): $@");
|
|
|
|
return $self->p->sendError( $req, "Corrupted session",
|
|
|
|
500 );
|
|
|
|
}
|
|
|
|
}
|
2018-04-03 00:01:01 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
else {
|
2018-03-29 23:11:46 +02:00
|
|
|
$self->logger->debug("No 2F Device found");
|
2018-04-07 13:22:06 +02:00
|
|
|
$_2fDevices = [];
|
2018-03-29 23:11:46 +02:00
|
|
|
}
|
2018-04-04 23:50:33 +02:00
|
|
|
|
2018-03-29 23:11:46 +02:00
|
|
|
my $keyName = $req->param('keyName');
|
2018-04-03 21:17:15 +02:00
|
|
|
my $epoch = time();
|
|
|
|
|
2018-08-31 19:10:35 +02:00
|
|
|
# Set default name if empty, check characters and truncate name if too long
|
2018-04-03 17:15:38 +02:00
|
|
|
$keyName ||= $epoch;
|
2018-08-31 19:10:35 +02:00
|
|
|
unless ( $keyName =~ /^[\w]+$/ ) {
|
|
|
|
$self->userLogger->error('U2F name with bad character(s)');
|
|
|
|
return $self->p->sendError( $req, 'badName', 200 );
|
|
|
|
}
|
2018-04-05 19:43:06 +02:00
|
|
|
$keyName =
|
|
|
|
substr( $keyName, 0, $self->conf->{max2FDevicesNameLength} );
|
2018-03-31 00:16:36 +02:00
|
|
|
$self->logger->debug("Key name : $keyName");
|
2018-04-03 00:01:01 +02:00
|
|
|
|
2018-04-07 13:22:06 +02:00
|
|
|
push @{$_2fDevices},
|
2018-03-29 23:11:46 +02:00
|
|
|
{
|
|
|
|
type => 'U2F',
|
|
|
|
name => $keyName,
|
|
|
|
_userKey => $self->encode_base64url( $userKey, '' ),
|
|
|
|
_keyHandle => $keyHandle,
|
2018-04-03 17:15:38 +02:00
|
|
|
epoch => $epoch
|
2018-03-29 23:11:46 +02:00
|
|
|
};
|
|
|
|
$self->logger->debug(
|
|
|
|
"Append 2F Device : { type => 'U2F', name => $keyName }");
|
|
|
|
$self->p->updatePersistentSession( $req,
|
2018-04-07 13:22:06 +02:00
|
|
|
{ _2fDevices => to_json($_2fDevices) } );
|
2019-04-09 21:48:59 +02:00
|
|
|
$self->userLogger->notice(
|
|
|
|
"U2F key registration of $keyName succeeds for $user");
|
2018-03-29 23:11:46 +02:00
|
|
|
|
2018-03-06 22:50:40 +01:00
|
|
|
return [
|
2018-03-19 22:35:39 +01:00
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'Content-Length' => 12,
|
|
|
|
],
|
2018-03-06 22:50:40 +01:00
|
|
|
['{"result":1}']
|
|
|
|
];
|
|
|
|
}
|
2017-02-03 07:23:39 +01:00
|
|
|
}
|
2017-02-08 14:01:02 +01:00
|
|
|
my $err = Crypt::U2F::Server::Simple::lastError();
|
2017-02-15 15:16:59 +01:00
|
|
|
$self->userLogger->warn("U2F Registration failed: $err");
|
2017-02-08 14:01:02 +01:00
|
|
|
return $self->p->sendError( $req, $err, 200 );
|
2017-02-02 22:48:32 +01:00
|
|
|
}
|
2018-02-21 09:23:41 +01:00
|
|
|
|
2018-03-18 22:20:05 +01:00
|
|
|
elsif ( $action eq 'verify' ) {
|
2018-03-19 22:35:39 +01:00
|
|
|
$self->logger->debug('Verification challenge req');
|
2017-02-08 14:01:02 +01:00
|
|
|
my ( $err, $error ) = $self->loadUser($req);
|
|
|
|
if ( $err == -1 ) {
|
|
|
|
return $self->p->sendError( $req, "U2F error: $error", 200 );
|
|
|
|
}
|
|
|
|
elsif ( $err == 0 ) {
|
|
|
|
return $self->p->sendError( $req, "noU2FKeyFound" );
|
|
|
|
}
|
2018-04-20 16:15:26 +02:00
|
|
|
|
|
|
|
# Get a challenge (from first key)
|
|
|
|
my $data = eval {
|
2018-07-05 22:56:16 +02:00
|
|
|
from_json( $req->data->{crypter}->[0]->authenticationChallenge );
|
2018-04-20 16:15:26 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error( Crypt::U2F::Server::u2fclib_getError() );
|
|
|
|
return $self->p->sendError( $req, "U2F error: $error", 200 );
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get registered keys
|
|
|
|
my @rk;
|
2018-07-05 22:56:16 +02:00
|
|
|
foreach ( @{ $req->data->{crypter} } ) {
|
2018-04-20 16:15:26 +02:00
|
|
|
my $k = push @rk,
|
|
|
|
{ keyHandle => $_->{keyHandle}, version => $data->{version} };
|
|
|
|
}
|
|
|
|
|
2018-07-05 22:56:16 +02:00
|
|
|
# Serialize data
|
2019-02-07 09:27:56 +01:00
|
|
|
$data = to_json( {
|
2018-04-20 16:15:26 +02:00
|
|
|
challenge => $data->{challenge},
|
|
|
|
appId => $data->{appId},
|
|
|
|
registeredKeys => \@rk
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2018-03-19 22:35:39 +01:00
|
|
|
return [
|
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/json',
|
2018-04-20 16:15:26 +02:00
|
|
|
'Content-Length' => length($data),
|
2018-03-19 22:35:39 +01:00
|
|
|
],
|
2018-04-20 16:15:26 +02:00
|
|
|
[$data]
|
2018-03-19 22:35:39 +01:00
|
|
|
];
|
2017-02-08 14:01:02 +01:00
|
|
|
}
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-03-18 22:20:05 +01:00
|
|
|
elsif ( $action eq 'signature' ) {
|
2018-03-19 22:35:39 +01:00
|
|
|
$self->logger->debug('Verification response');
|
|
|
|
my ( $challenge, $resp );
|
|
|
|
unless ($challenge = $req->param('challenge')
|
|
|
|
and $resp = $req->param('signature') )
|
|
|
|
{
|
2017-02-08 14:01:02 +01:00
|
|
|
return $self->p->sendError( $req, 'Missing signature parameter',
|
|
|
|
400 );
|
|
|
|
}
|
|
|
|
my ( $err, $error ) = $self->loadUser($req);
|
|
|
|
if ( $err == -1 ) {
|
2018-06-21 21:35:16 +02:00
|
|
|
return $self->p->sendError( $req, "U2F loading error: $error",
|
|
|
|
500 );
|
2017-02-07 23:04:49 +01:00
|
|
|
}
|
2017-02-08 14:01:02 +01:00
|
|
|
elsif ( $err == 0 ) {
|
|
|
|
return $self->p->sendError( $req, "noU2FKeyFound" );
|
|
|
|
}
|
2018-04-20 16:15:26 +02:00
|
|
|
|
2018-03-19 22:35:39 +01:00
|
|
|
$self->logger->debug("Get verify response $resp");
|
2018-04-20 16:15:26 +02:00
|
|
|
my $data = eval { JSON::from_json($resp) };
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("U2F response error: $@");
|
2018-04-20 16:35:38 +02:00
|
|
|
return $self->p->sendError( $req, "U2FAnswerError" );
|
2018-04-20 16:15:26 +02:00
|
|
|
}
|
|
|
|
my $crypter;
|
2018-07-05 22:56:16 +02:00
|
|
|
foreach ( @{ $req->data->{crypter} } ) {
|
2018-04-20 16:15:26 +02:00
|
|
|
$crypter = $_ if ( $_->{keyHandle} eq $data->{keyHandle} );
|
|
|
|
}
|
|
|
|
unless ($crypter) {
|
|
|
|
$self->userLogger->error("Unregistered U2F key");
|
|
|
|
return $self->p->sendError( $req, "U2FKeyUnregistered" );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( not $crypter->setChallenge($challenge) ) {
|
|
|
|
$self->logger->error(
|
|
|
|
$@ ? $@ : Crypt::U2F::Server::Simple::lastError() );
|
|
|
|
|
|
|
|
return $self->p->sendError( $req, "U2FServerError" );
|
|
|
|
}
|
|
|
|
|
|
|
|
my $res = ( $crypter->authenticationVerify($resp) ? 1 : 0 );
|
2017-02-08 14:01:02 +01:00
|
|
|
return [
|
2018-03-19 22:35:39 +01:00
|
|
|
200,
|
|
|
|
[ 'Content-Type' => 'application/json', 'Content-Length' => 12, ],
|
2017-02-08 14:01:02 +01:00
|
|
|
[qq'{"result":$res}']
|
|
|
|
];
|
|
|
|
}
|
2018-03-18 22:20:05 +01:00
|
|
|
|
2018-04-02 23:19:56 +02:00
|
|
|
elsif ( $action eq 'delete' ) {
|
2018-08-18 18:29:14 +02:00
|
|
|
|
|
|
|
# Check if unregistration is allowed
|
|
|
|
unless ( $self->conf->{u2fUserCanRemoveKey} ) {
|
|
|
|
return $self->p->sendError( $req, 'notAuthorized', 200 );
|
|
|
|
}
|
|
|
|
|
2018-08-30 19:45:35 +02:00
|
|
|
my $epoch = $req->param('epoch')
|
|
|
|
or return $self->p->sendError( $req, '"epoch" parameter is missing',
|
|
|
|
400 );
|
2018-04-03 00:01:01 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
# Read existing 2FDevices
|
|
|
|
$self->logger->debug("Looking for 2F Devices ...");
|
|
|
|
my $_2fDevices;
|
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
|
|
|
return $self->p->sendError( $req, "Corrupted session", 500 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->debug("No 2F Device found");
|
|
|
|
$_2fDevices = [];
|
|
|
|
}
|
2018-04-03 00:01:01 +02:00
|
|
|
|
2019-04-26 23:00:17 +02:00
|
|
|
# Delete U2F device
|
|
|
|
@$_2fDevices = grep { $_->{epoch} ne $epoch } @$_2fDevices;
|
2018-04-03 00:01:01 +02:00
|
|
|
$self->logger->debug(
|
|
|
|
"Delete 2F Device : { type => 'U2F', epoch => $epoch }");
|
2018-04-02 23:19:56 +02:00
|
|
|
$self->p->updatePersistentSession( $req,
|
2019-05-11 20:18:43 +02:00
|
|
|
{ _2fDevices => to_json($_2fDevices) } );
|
2018-03-18 22:20:05 +01:00
|
|
|
$self->userLogger->notice('U2F key unregistration succeed');
|
2018-03-19 22:35:39 +01:00
|
|
|
return [
|
|
|
|
200,
|
|
|
|
[ 'Content-Type' => 'application/json', 'Content-Length' => 12, ],
|
|
|
|
['{"result":1}']
|
|
|
|
];
|
2018-03-18 22:20:05 +01:00
|
|
|
}
|
2018-08-20 11:36:23 +02:00
|
|
|
else {
|
|
|
|
$self->logger->error("Unknown U2F action -> $action");
|
|
|
|
return $self->p->sendError( $req, 'unknownAction', 400 );
|
|
|
|
}
|
|
|
|
|
2017-02-08 14:01:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
sub loadUser {
|
|
|
|
my ( $self, $req ) = @_;
|
2018-04-10 11:06:06 +02:00
|
|
|
$self->logger->debug("Loading user U2F Devices ...");
|
2018-04-10 11:07:07 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
# Read existing 2FDevices
|
|
|
|
$self->logger->debug("Looking for 2F Devices ...");
|
2018-04-20 16:15:26 +02:00
|
|
|
my ( $kh, $uk, $_2fDevices );
|
|
|
|
my @u2fs = ();
|
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices}, { allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
|
|
|
return $self->p->sendError( $req, "Corrupted session", 500 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
2018-04-10 16:15:14 +02:00
|
|
|
$self->logger->debug("No 2F Device found");
|
2018-04-11 23:14:58 +02:00
|
|
|
$_2fDevices = [];
|
2018-04-10 16:15:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Reading existing U2F keys
|
2018-04-20 16:15:26 +02:00
|
|
|
foreach (@$_2fDevices) {
|
|
|
|
$self->logger->debug("Looking for registered U2F key(s) ...");
|
|
|
|
if ( $_->{type} eq 'U2F' ) {
|
|
|
|
unless ( $_->{_userKey} and $_->{_keyHandle} ) {
|
|
|
|
$self->logger->error(
|
2018-04-20 16:35:38 +02:00
|
|
|
'Missing required U2F attributes in storage ($session->{_2fDevices})'
|
2018-04-20 16:15:26 +02:00
|
|
|
);
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
$self->logger->debug( "Found U2F key -> _userKey = "
|
|
|
|
. $_->{_userKey}
|
|
|
|
. "/ _keyHandle = "
|
|
|
|
. $_->{_keyHandle} );
|
|
|
|
$_->{_userKey} = $self->decode_base64url( $_->{_userKey} );
|
|
|
|
push @u2fs, $_;
|
|
|
|
}
|
2017-02-08 14:01:02 +01:00
|
|
|
}
|
2018-04-20 16:15:26 +02:00
|
|
|
|
|
|
|
# Manage multi u2f keys
|
|
|
|
my @crypters;
|
|
|
|
if (@u2fs) {
|
|
|
|
foreach (@u2fs) {
|
|
|
|
$kh = $_->{_keyHandle};
|
|
|
|
$uk = $_->{_userKey};
|
|
|
|
my $c = $self->crypter( keyHandle => $kh, publicKey => $uk );
|
|
|
|
if ($c) {
|
2018-06-21 21:35:16 +02:00
|
|
|
$self->logger->debug("kh & uk -> OK");
|
2018-04-20 16:15:26 +02:00
|
|
|
push @crypters, $c;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->error(
|
|
|
|
'U2F error: ' . Crypt::U2F::Server::u2fclib_getError() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
unless (@crypters) {
|
|
|
|
return -1;
|
|
|
|
}
|
2018-07-05 22:56:16 +02:00
|
|
|
$req->data->{crypter} = \@crypters;
|
2018-04-20 16:15:26 +02:00
|
|
|
return 1;
|
2017-02-08 14:01:02 +01:00
|
|
|
}
|
2018-04-20 16:15:26 +02:00
|
|
|
else {
|
|
|
|
$self->userLogger->info("U2F : user not registered");
|
|
|
|
return 0;
|
|
|
|
}
|
2017-02-02 22:48:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|