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

272 lines
8.4 KiB
Perl
Raw Normal View History

2018-03-30 21:24:34 +02:00
# Self TOTP registration
package Lemonldap::NG::Portal::2F::Register::TOTP;
use strict;
use Mouse;
use JSON qw(from_json to_json);
our $VERSION = '2.0.0';
extends 'Lemonldap::NG::Portal::Main::Plugin', 'Lemonldap::NG::Common::TOTP';
# INITIALIZATION
has prefix => ( is => 'rw', default => 'totp' );
2018-04-01 16:39:02 +02:00
has template => ( is => 'ro', default => 'totp2fregister' );
has logo => ( is => 'rw', default => 'totp.png' );
has ott => (
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;
}
sub run {
my ( $self, $req, $action ) = @_;
2018-03-18 14:26:45 +01:00
my $user = $req->userData->{ $self->conf->{whatToTrace} };
unless ($user) {
return $self->p->sendError( $req,
'No ' . $self->conf->{whatToTrace} . ' found in user data', 500 );
}
# Verification that user has a valid TOTP app
if ( $action eq 'verify' ) {
# Get form token
my $token = $req->param('token');
unless ($token) {
$self->userLogger->warn(
"TOTP registration: register try without token for $user");
return $self->p->sendError( $req, 'Go away', 400 );
}
# 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
my $code = $req->param('code');
my $TOTPName = $req->param('TOTPName');
my $epoch = time();
2018-04-05 19:43:06 +02:00
# Set default name if empty and troncate name if too long
$TOTPName ||= $epoch;
2018-04-05 19:43:06 +02:00
$TOTPName =
substr( $TOTPName, 0, $self->conf->{max2FDevicesNameLength} );
$self->logger->debug("TOTP name : $TOTPName");
unless ($code) {
$self->logger->userInfo('TOTP registration: empty validation form');
return $self->p->sendError( $req, 'missingCode', 200 );
}
my $r = $self->verifyCode(
$self->conf->{totp2fInterval},
$self->conf->{totp2fRange},
2018-02-21 22:07:12 +01:00
$self->conf->{totp2fDigits},
$token->{_totp2fSecret}, $code
);
if ( $r == -1 ) {
return $self->p->sendError( 'serverError', 500 );
}
# 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');
# Now code is verified, let's store the master key in persistent data
2018-04-08 22:27:12 +02:00
#$self->p->updatePersistentSession( $req,
#{ _totp2fSecret => $token->{_totp2fSecret} } );
2018-04-07 13:22:06 +02:00
my $_2fDevices = eval {
2018-03-30 21:24:34 +02:00
$self->logger->debug("Looking for 2F Devices ...");
2018-04-07 13:22:06 +02:00
from_json( $req->userData->{_2fDevices}, { allow_nonref => 1 } );
};
2018-04-07 13:22:06 +02:00
unless ($_2fDevices) {
$self->logger->debug("No 2F Device found");
2018-04-07 13:22:06 +02:00
$_2fDevices = [];
}
# Check if user can register one more device
2018-04-07 13:22:06 +02:00
my $size = @$_2fDevices;
my $maxSize = $self->conf->{max2FDevices};
$self->logger->debug("Nbr 2FDevices = $size / $maxSize");
if ( $size >= $maxSize ) {
$self->userLogger->error("Max number of 2F devices is reached !!!");
return $self->p->sendError( $req, 'maxNumberof2FDevicesReached',
400 );
}
2018-04-07 13:22:06 +02:00
push @{$_2fDevices},
{
type => 'TOTP',
name => $TOTPName,
_secret => $token->{_totp2fSecret},
epoch => $epoch
};
$self->logger->debug(
2018-04-08 22:27:12 +02:00
"Append 2F Device : { type => 'TOTP', name => $TOTPName }");
$self->p->updatePersistentSession( $req,
2018-04-07 13:22:06 +02:00
{ _2fDevices => to_json($_2fDevices) } );
$self->userLogger->notice('TOTP registration succeed');
return [ 200, [ 'Content-Type' => 'application/json' ],
['{"result":1}'] ];
}
# Get or generate master key
elsif ( $action eq 'getkey' ) {
my $nk = 0;
2018-04-09 17:25:22 +02:00
my $secret = '';
my $_2fDevices = eval {
$self->logger->debug("Loading 2F Devices ...");
# Read existing 2FDevices
from_json( $req->userData->{_2fDevices}, { allow_nonref => 1 } );
};
my @totp2f = grep { $_->{type} eq "TOTP" } @$_2fDevices;
unless ( @totp2f ) {
$self->logger->debug("No 2F Device found");
# Set default value
push @totp2f, { _secret => '' } ;
}
foreach ( @totp2f ) {
$self->logger->debug("Reading TOTP secret ...");
$secret = $_->{_secret};
};
2018-03-18 14:26:45 +01:00
if ( ( $req->param('newkey') and $self->conf->{totp2fUserCanChangeKey} )
2018-04-08 22:27:12 +02:00
#or not $req->userData->{_totp2fSecret} )
2018-04-09 17:25:22 +02:00
or not $secret )
2018-03-18 14:26:45 +01:00
{
$secret = $self->newSecret;
2018-04-09 17:25:22 +02:00
$self->logger->debug("Generating new secret = $secret");
$nk = 1;
}
2018-03-18 14:26:45 +01:00
elsif ( $req->param('newkey') ) {
2018-04-03 23:03:29 +02:00
return $self->p->sendError( $req, 'notAuthorized', 200 );
2018-03-18 14:26:45 +01:00
}
elsif ( $self->conf->{totp2fDisplayExistingSecret} ) {
2018-04-09 17:25:22 +02:00
#$secret = $req->userData->{_totp2fSecret};
$self->logger->debug("User secret = $secret");
}
2018-04-09 17:25:22 +02:00
2018-03-18 14:26:45 +01:00
else {
return $self->p->sendError( $req, 'totpExistingKey', 200 );
}
# Secret is stored in a token: we choose to not accept secret returned
# by Ajax request to avoid some attacks
my $token = $self->ott->createToken(
{
_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-04-09 17:25:22 +02:00
if ( $token eq $secret ) {
return $self->p->sendError( $req, 'notAuthorized', 200 );
}
# 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}
}
);
}
# Check if unregistration is allowed
2018-04-03 23:03:29 +02:00
unless ( $self->conf->{TOTP2fUserCanChangeKey} ) {
return $self->p->sendError( $req, 'notAuthorized', 400 );
}
2018-03-13 07:14:01 +01:00
2018-04-08 22:27:12 +02:00
# Delete TOTP
#if ( $action eq 'unregister' ) {
#$self->p->updatePersistentSession( $req, { _totp2fSecret => '' } );
#$self->userLogger->notice('TOTP unregistration succeed');
#return [ 200, [ 'Content-Type' => 'application/json' ],
#['{"result":1}'] ];
#}
#elsif ( $action eq 'delete' ) {
if ( $action eq 'delete' ) {
2018-04-04 23:16:36 +02:00
my $epoch = $req->param('epoch');
2018-04-07 13:22:06 +02:00
my $_2fDevices = eval {
$self->logger->debug("Loading 2F Devices ...");
# Read existing 2FDevices
2018-04-07 13:22:06 +02:00
from_json( $req->userData->{_2fDevices}, { allow_nonref => 1 } );
};
my @keep = ();
2018-04-07 13:22:06 +02:00
while (@$_2fDevices) {
my $element = shift @$_2fDevices;
$self->logger->debug("Looking for 2F device to delete ...");
push @keep, $element unless ( $element->{epoch} eq $epoch );
}
$self->logger->debug(
"Delete 2F Device : { type => 'TOTP', epoch => $epoch }");
$self->p->updatePersistentSession( $req,
2018-04-07 13:22:06 +02:00
{ _2fDevices => to_json( \@keep ) } );
$self->userLogger->notice('TOTP deletion succeed');
return [ 200, [ 'Content-Type' => 'application/json' ],
['{"result":1}'] ];
}
}
1;