2018-03-30 21:24:34 +02:00
|
|
|
# Self TOTP registration
|
2018-02-20 13:19:52 +01:00
|
|
|
package Lemonldap::NG::Portal::2F::Register::TOTP;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use Mouse;
|
2018-03-29 21:27:35 +02:00
|
|
|
use JSON qw(from_json to_json);
|
2018-02-20 13:19:52 +01:00
|
|
|
|
2020-10-01 21:49:00 +02:00
|
|
|
our $VERSION = '2.0.10';
|
2018-02-20 13:19:52 +01:00
|
|
|
|
2020-10-31 23:43:08 +01:00
|
|
|
extends qw(
|
|
|
|
Lemonldap::NG::Portal::Main::Plugin
|
|
|
|
Lemonldap::NG::Common::TOTP
|
|
|
|
);
|
2018-02-20 13:19:52 +01:00
|
|
|
|
|
|
|
# INITIALIZATION
|
|
|
|
|
2020-10-01 21:49:00 +02:00
|
|
|
has prefix => ( is => 'rw', default => 'totp' );
|
2018-04-01 16:39:02 +02:00
|
|
|
has template => ( is => 'ro', default => 'totp2fregister' );
|
2020-10-16 23:12:52 +02:00
|
|
|
has welcome => ( is => 'ro', default => 'yourNewTotpKey' );
|
2020-10-01 21:49:00 +02:00
|
|
|
has logo => ( is => 'rw', default => 'totp.png' );
|
|
|
|
has ott => (
|
2018-02-20 13:19:52 +01:00
|
|
|
is => 'rw',
|
|
|
|
lazy => 1,
|
|
|
|
default => sub {
|
|
|
|
my $ott =
|
|
|
|
$_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
|
|
|
|
$ott->timeout( $_[0]->conf->{formTimeout} );
|
|
|
|
return $ott;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
sub init {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2018-03-15 07:04:52 +01:00
|
|
|
sub run {
|
|
|
|
my ( $self, $req, $action ) = @_;
|
2018-03-18 14:26:45 +01:00
|
|
|
my $user = $req->userData->{ $self->conf->{whatToTrace} };
|
2021-06-15 18:07:03 +02:00
|
|
|
return $self->p->sendError( $req, 'PE82', 400 )
|
2020-10-01 21:49:00 +02:00
|
|
|
unless $user;
|
2018-02-20 13:19:52 +01:00
|
|
|
|
2020-10-03 15:05:13 +02:00
|
|
|
# Check if TOTP can be updated
|
2020-10-16 23:12:52 +02:00
|
|
|
my $msg = $self->canUpdateSfa( $req, $action );
|
|
|
|
return $self->p->sendError( $req, $msg, 400 ) if $msg;
|
2020-10-03 15:05:13 +02:00
|
|
|
|
2018-02-20 13:19:52 +01:00
|
|
|
# Verification that user has a valid TOTP app
|
2018-04-11 23:31:57 +02:00
|
|
|
if ( $action eq 'verify' ) {
|
2018-02-20 13:19:52 +01:00
|
|
|
|
|
|
|
# Get form token
|
|
|
|
my $token = $req->param('token');
|
|
|
|
unless ($token) {
|
|
|
|
$self->userLogger->warn(
|
|
|
|
"TOTP registration: register try without token for $user");
|
2018-04-11 23:14:58 +02:00
|
|
|
return $self->p->sendError( $req, 'noTOTPFound', 400 );
|
2018-02-20 13:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# Verify that token exists in DB (note that "keep" flag is set to
|
|
|
|
# permit more than 1 try during token life
|
|
|
|
unless ( $token = $self->ott->getToken( $token, 1 ) ) {
|
|
|
|
$self->userLogger->notice(
|
|
|
|
"TOTP registration: token expired for $user");
|
|
|
|
return $self->p->sendError( $req, 'PE82', 400 );
|
|
|
|
}
|
|
|
|
|
|
|
|
# Token is valid, so we have the master key proposed
|
|
|
|
# ($token->{_totp2fSecret})
|
|
|
|
|
|
|
|
# Now check TOTP code to verify that user has a valid TOTP app
|
2018-03-29 23:11:46 +02:00
|
|
|
my $code = $req->param('code');
|
|
|
|
my $TOTPName = $req->param('TOTPName');
|
2018-04-03 21:17:15 +02:00
|
|
|
my $epoch = time();
|
|
|
|
|
2018-09-02 17:31:58 +02:00
|
|
|
# Set default name if empty, check characters and truncate name if too long
|
2018-04-03 21:17:15 +02:00
|
|
|
$TOTPName ||= $epoch;
|
2018-08-31 19:10:35 +02:00
|
|
|
unless ( $TOTPName =~ /^[\w]+$/ ) {
|
|
|
|
$self->userLogger->error('TOTP name with bad character(s)');
|
|
|
|
return $self->p->sendError( $req, 'badName', 200 );
|
|
|
|
}
|
2018-04-05 19:43:06 +02:00
|
|
|
$TOTPName =
|
|
|
|
substr( $TOTPName, 0, $self->conf->{max2FDevicesNameLength} );
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("TOTP name: $TOTPName");
|
2018-03-31 00:16:36 +02:00
|
|
|
|
|
|
|
unless ($code) {
|
2018-06-13 21:18:15 +02:00
|
|
|
$self->userLogger->info('TOTP registration: empty validation form');
|
2018-02-20 22:58:20 +01:00
|
|
|
return $self->p->sendError( $req, 'missingCode', 200 );
|
2018-02-20 13:19:52 +01:00
|
|
|
}
|
2018-04-03 00:01:01 +02:00
|
|
|
|
2018-02-20 13:19:52 +01:00
|
|
|
my $r = $self->verifyCode(
|
|
|
|
$self->conf->{totp2fInterval},
|
|
|
|
$self->conf->{totp2fRange},
|
2018-02-21 22:07:12 +01:00
|
|
|
$self->conf->{totp2fDigits},
|
2018-02-20 13:19:52 +01:00
|
|
|
$token->{_totp2fSecret}, $code
|
|
|
|
);
|
|
|
|
if ( $r == -1 ) {
|
2018-04-11 09:54:40 +02:00
|
|
|
return $self->p->sendError( $req, 'serverError', 500 );
|
2018-02-20 13:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# Invalid try is returned with a 200 code. Javascript will read error
|
|
|
|
# and propose to retry
|
|
|
|
elsif ( $r == 0 ) {
|
|
|
|
$self->userLogger->notice(
|
|
|
|
"TOTP registration: invalid TOTP for $user");
|
|
|
|
return $self->p->sendError( $req, 'badCode', 200 );
|
|
|
|
}
|
|
|
|
$self->logger->debug('TOTP code verified');
|
|
|
|
|
2018-02-20 18:36:34 +01:00
|
|
|
# Now code is verified, let's store the master key in persistent data
|
2018-04-11 09:54:40 +02:00
|
|
|
my $secret = '';
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-04-11 09:54:40 +02:00
|
|
|
# Reading existing 2FDevices
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Looking for 2F Devices...");
|
2018-04-11 09:54:40 +02:00
|
|
|
my $_2fDevices;
|
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
2021-06-15 18:07:03 +02:00
|
|
|
return $self->p->sendError( $req, "serverError", 500 );
|
2018-04-11 09:54:40 +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-04-10 16:15:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
# Reading existing TOTP
|
2020-10-01 21:49:00 +02:00
|
|
|
my @totp2f = grep { $_->{type} eq 'TOTP' } @$_2fDevices;
|
2018-04-10 16:15:14 +02:00
|
|
|
unless (@totp2f) {
|
2018-04-10 11:06:06 +02:00
|
|
|
$self->logger->debug("No TOTP Device found");
|
|
|
|
|
|
|
|
# Set default value
|
2018-04-10 16:15:14 +02:00
|
|
|
push @totp2f, { _secret => '' };
|
2018-04-10 11:06:06 +02:00
|
|
|
}
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-04-10 11:06:06 +02:00
|
|
|
# Loading TOTP secret
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Reading TOTP secret if exists...");
|
2020-02-16 22:42:10 +01:00
|
|
|
$secret = $_->{_secret} foreach (@totp2f);
|
2020-10-01 21:49:00 +02:00
|
|
|
return $self->p->sendError( $req, 'totpExistingKey', 200 )
|
2021-06-14 17:16:16 +02:00
|
|
|
if $secret;
|
2018-04-10 11:06:06 +02:00
|
|
|
|
2018-04-10 16:15:14 +02:00
|
|
|
### USER CAN ONLY REGISTER ONE TOTP ###
|
2018-04-11 23:14:58 +02:00
|
|
|
# Delete TOTP previously registered
|
2020-10-01 21:49:00 +02:00
|
|
|
$self->logger->debug("Looking for TOTP to delete...");
|
|
|
|
my $size = my @keep =
|
|
|
|
map { $_->{type} eq 'TOTP' ? () : $_ } @$_2fDevices;
|
2018-04-04 23:50:33 +02:00
|
|
|
|
|
|
|
# Check if user can register one more device
|
2018-04-05 19:08:29 +02:00
|
|
|
my $maxSize = $self->conf->{max2FDevices};
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Registered 2F Device(s): $size / $maxSize");
|
2018-04-05 19:08:29 +02:00
|
|
|
if ( $size >= $maxSize ) {
|
2019-07-04 07:24:50 +02:00
|
|
|
$self->userLogger->warn("Max number of 2F devices is reached");
|
2018-04-05 19:08:29 +02:00
|
|
|
return $self->p->sendError( $req, 'maxNumberof2FDevicesReached',
|
|
|
|
400 );
|
|
|
|
}
|
2018-04-04 23:50:33 +02:00
|
|
|
|
2018-04-10 16:15:14 +02:00
|
|
|
# Store TOTP secret
|
2018-04-10 11:06:06 +02:00
|
|
|
push @keep,
|
2018-03-29 23:11:46 +02:00
|
|
|
{
|
2018-04-03 00:01:01 +02:00
|
|
|
type => 'TOTP',
|
2018-03-29 23:11:46 +02:00
|
|
|
name => $TOTPName,
|
|
|
|
_secret => $token->{_totp2fSecret},
|
2018-04-03 21:17:15 +02:00
|
|
|
epoch => $epoch
|
2018-03-29 23:11:46 +02:00
|
|
|
};
|
2018-04-03 21:17:15 +02:00
|
|
|
$self->logger->debug(
|
2020-10-03 15:05:13 +02:00
|
|
|
"Append 2F Device: { type => 'TOTP', name => $TOTPName }");
|
2018-03-29 21:27:35 +02:00
|
|
|
$self->p->updatePersistentSession( $req,
|
2018-04-10 11:06:06 +02:00
|
|
|
{ _2fDevices => to_json( \@keep ) } );
|
2019-04-09 21:48:59 +02:00
|
|
|
$self->userLogger->notice(
|
|
|
|
"TOTP registration of $TOTPName succeeds for $user");
|
2018-08-20 11:36:23 +02:00
|
|
|
return [
|
|
|
|
200,
|
|
|
|
[ 'Content-Type' => 'application/json', 'Content-Length' => 12, ],
|
|
|
|
['{"result":1}']
|
|
|
|
];
|
2018-02-20 13:19:52 +01:00
|
|
|
}
|
2018-02-20 18:36:34 +01:00
|
|
|
|
2018-02-20 13:19:52 +01:00
|
|
|
# Get or generate master key
|
2018-02-20 18:36:34 +01:00
|
|
|
elsif ( $action eq 'getkey' ) {
|
2018-04-10 16:15:14 +02:00
|
|
|
my $nk = 0;
|
2018-04-09 17:25:22 +02:00
|
|
|
my $secret = '';
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-04-11 09:54:40 +02:00
|
|
|
# Read existing 2FDevices
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Looking for 2F Devices...");
|
2018-04-11 23:14:58 +02:00
|
|
|
my $_2fDevices;
|
2018-04-11 09:54:40 +02:00
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
2021-06-15 18:07:03 +02:00
|
|
|
return $self->p->sendError( $req, "serverError", 500 );
|
2018-04-11 09:54:40 +02:00
|
|
|
}
|
|
|
|
}
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-04-11 23:14:58 +02:00
|
|
|
else {
|
|
|
|
$self->logger->debug("No 2F Device found");
|
|
|
|
$_2fDevices = [];
|
|
|
|
}
|
|
|
|
|
2018-09-04 21:02:57 +02:00
|
|
|
# Looking for TOTP
|
2018-04-09 17:25:22 +02:00
|
|
|
my @totp2f = grep { $_->{type} eq "TOTP" } @$_2fDevices;
|
2018-04-10 16:15:14 +02:00
|
|
|
unless (@totp2f) {
|
2018-04-10 11:06:06 +02:00
|
|
|
$self->logger->debug("No TOTP found");
|
2018-04-09 17:25:22 +02:00
|
|
|
|
|
|
|
# Set default value
|
2018-04-10 16:15:14 +02:00
|
|
|
push @totp2f, { _secret => '' };
|
|
|
|
}
|
|
|
|
|
|
|
|
# Loading TOTP secret
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Reading TOTP secret if exists...");
|
2020-02-16 22:42:10 +01:00
|
|
|
$secret = $_->{_secret} foreach (@totp2f);
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2021-06-14 15:52:50 +02:00
|
|
|
if ($secret) {
|
|
|
|
return $self->p->sendError( $req, 'totpExistingKey', 200 );
|
2021-06-15 18:07:03 +02:00
|
|
|
}
|
|
|
|
else {
|
2018-02-20 18:36:34 +01:00
|
|
|
$secret = $self->newSecret;
|
2018-04-09 17:25:22 +02:00
|
|
|
$self->logger->debug("Generating new secret = $secret");
|
2018-04-10 16:15:14 +02:00
|
|
|
$nk = 1;
|
2018-02-20 18:36:34 +01:00
|
|
|
}
|
2018-04-10 16:15:14 +02:00
|
|
|
|
2018-02-20 18:36:34 +01:00
|
|
|
# Secret is stored in a token: we choose to not accept secret returned
|
|
|
|
# by Ajax request to avoid some attacks
|
2019-02-07 09:27:56 +01:00
|
|
|
my $token = $self->ott->createToken( {
|
2018-02-20 18:36:34 +01:00
|
|
|
_totp2fSecret => $secret,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2018-02-21 22:07:12 +01:00
|
|
|
my $issuer;
|
|
|
|
unless ( $issuer = $self->conf->{totp2fIssuer} ) {
|
|
|
|
$issuer = $self->conf->{portal};
|
|
|
|
$issuer =~ s#^https?://([^/:]+).*$#$1#;
|
|
|
|
}
|
2018-02-20 18:36:34 +01:00
|
|
|
|
|
|
|
# QR-code will be generated by a javascript, here we just send data
|
|
|
|
return $self->p->sendJSONresponse(
|
|
|
|
$req,
|
|
|
|
{
|
2018-02-21 22:07:12 +01:00
|
|
|
secret => $secret,
|
|
|
|
token => $token,
|
|
|
|
portal => $issuer,
|
|
|
|
user => $user,
|
|
|
|
newkey => $nk,
|
|
|
|
digits => $self->conf->{totp2fDigits},
|
|
|
|
interval => $self->conf->{totp2fInterval}
|
2018-02-20 18:36:34 +01:00
|
|
|
}
|
|
|
|
);
|
2018-04-10 16:15:14 +02:00
|
|
|
}
|
2018-04-03 21:17:15 +02:00
|
|
|
|
2018-04-08 22:27:12 +02:00
|
|
|
# Delete TOTP
|
2018-08-20 11:36:23 +02:00
|
|
|
elsif ( $action eq 'delete' ) {
|
2018-04-11 09:54:40 +02:00
|
|
|
|
2018-08-20 11:36:23 +02:00
|
|
|
# Check if unregistration is allowed
|
2020-02-16 22:42:10 +01:00
|
|
|
return $self->p->sendError( $req, 'notAuthorized', 400 )
|
2020-10-03 15:05:13 +02:00
|
|
|
unless $self->conf->{totp2fUserCanRemoveKey};
|
2018-08-20 11:36:23 +02:00
|
|
|
|
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-11 09:54:40 +02:00
|
|
|
# Read existing 2FDevices
|
2020-10-03 15:05:13 +02:00
|
|
|
$self->logger->debug("Loading 2F Devices...");
|
2020-10-01 21:49:00 +02:00
|
|
|
my ( $_2fDevices, $TOTPName );
|
2018-04-11 09:54:40 +02:00
|
|
|
if ( $req->userData->{_2fDevices} ) {
|
|
|
|
$_2fDevices = eval {
|
|
|
|
from_json( $req->userData->{_2fDevices},
|
|
|
|
{ allow_nonref => 1 } );
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->error("Corrupted session (_2fDevices): $@");
|
2021-06-15 18:07:03 +02:00
|
|
|
return $self->p->sendError( $req, "serverError", 500 );
|
2018-04-11 09:54:40 +02:00
|
|
|
}
|
|
|
|
}
|
2018-04-11 23:14:58 +02:00
|
|
|
else {
|
|
|
|
$self->logger->debug("No 2F Device found");
|
|
|
|
$_2fDevices = [];
|
|
|
|
}
|
|
|
|
|
2019-04-26 23:00:17 +02:00
|
|
|
# Delete TOTP 2F device
|
2020-10-01 21:49:00 +02:00
|
|
|
@$_2fDevices = map {
|
|
|
|
if ( $_->{epoch} eq $epoch ) { $TOTPName = $_->{name}; () }
|
|
|
|
else { $_ }
|
|
|
|
} @$_2fDevices;
|
2020-10-09 22:29:56 +02:00
|
|
|
if ($TOTPName) {
|
|
|
|
$self->logger->debug(
|
2020-10-03 15:05:13 +02:00
|
|
|
"Delete 2F Device: { type => 'TOTP', epoch => $epoch, name => $TOTPName }"
|
2020-10-09 22:29:56 +02:00
|
|
|
);
|
|
|
|
$self->p->updatePersistentSession( $req,
|
|
|
|
{ _2fDevices => to_json($_2fDevices) } );
|
|
|
|
$self->userLogger->notice(
|
|
|
|
"TOTP $TOTPName unregistration succeeds for $user");
|
|
|
|
return [
|
|
|
|
200,
|
|
|
|
[
|
|
|
|
'Content-Type' => 'application/json',
|
|
|
|
'Content-Length' => 12,
|
|
|
|
],
|
|
|
|
['{"result":1}']
|
|
|
|
];
|
|
|
|
}
|
|
|
|
else {
|
2020-10-12 15:16:55 +02:00
|
|
|
$self->p->sendError( $req, '2FDeviceNotFound', 400 );
|
2020-10-09 22:29:56 +02:00
|
|
|
}
|
2018-08-20 11:36:23 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->error("Unknown TOTP action -> $action");
|
|
|
|
return $self->p->sendError( $req, 'unknownAction', 400 );
|
2018-04-03 00:01:01 +02:00
|
|
|
}
|
2018-02-20 13:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|