2019-11-08 18:31:50 +01:00
|
|
|
package Lemonldap::NG::Manager::Api::2F;
|
2020-01-10 21:29:11 +01:00
|
|
|
|
|
|
|
our $VERSION = '2.0.8';
|
2019-11-08 18:31:50 +01:00
|
|
|
|
|
|
|
package Lemonldap::NG::Manager::Api;
|
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
use 5.10.0;
|
|
|
|
use utf8;
|
|
|
|
use Mouse;
|
|
|
|
use JSON;
|
|
|
|
|
|
|
|
use Lemonldap::NG::Common::Session;
|
2020-08-17 18:18:52 +02:00
|
|
|
use Lemonldap::NG::Common::Util qw/genId2F/;
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
sub getSecondFactors {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'uid is missing', 400 );
|
|
|
|
|
|
|
|
$self->logger->debug("[API] 2F for $uid requested");
|
|
|
|
|
|
|
|
$res = $self->_get2F($uid);
|
2019-12-20 11:05:16 +01:00
|
|
|
|
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return $self->sendJSONresponse( $req, $res->{secondFactors} );
|
|
|
|
}
|
|
|
|
|
|
|
|
sub getSecondFactorsByType {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $type, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'Uid is missing', 400 );
|
|
|
|
|
|
|
|
$type = $req->params('type')
|
|
|
|
or return $self->sendError( $req, 'Type is missing', 400 );
|
|
|
|
|
|
|
|
$self->logger->debug("[API] 2F for $uid with type $type requested");
|
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$res = $self->_get2F( $uid, uc $type );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return $self->sendJSONresponse( $req, $res->{secondFactors} );
|
|
|
|
}
|
|
|
|
|
|
|
|
sub getSecondFactorsById {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $id, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'uid is missing', 400 );
|
|
|
|
|
|
|
|
$id = $req->params('id')
|
|
|
|
or return $self->sendError( $req, 'id is missing', 400 );
|
|
|
|
|
|
|
|
$self->logger->debug("[API] 2F for $uid with id $id requested");
|
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$res = $self->_get2F( $uid, undef, $id );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2019-12-20 11:05:16 +01:00
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
return $self->sendError( $req, "2F id '$id' not found for user '$uid'",
|
|
|
|
404 )
|
|
|
|
unless ( scalar @{ $res->{secondFactors} } > 0 );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
return $self->sendJSONresponse( $req, @{ $res->{secondFactors} }[0] );
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
sub deleteSecondFactors {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'uid is missing', 400 );
|
|
|
|
|
|
|
|
$self->logger->debug("[API] Delete all 2F for $uid requested");
|
|
|
|
|
|
|
|
$res = $self->_delete2F($uid);
|
2019-12-20 11:05:16 +01:00
|
|
|
|
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
|
|
|
|
}
|
|
|
|
|
|
|
|
sub deleteSecondFactorsById {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $id, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'uid is missing', 400 );
|
|
|
|
|
|
|
|
$id = $req->params('id')
|
|
|
|
or return $self->sendError( $req, 'id is missing', 400 );
|
|
|
|
|
|
|
|
$self->logger->debug("[API] Delete 2F for $uid with id $id requested");
|
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$res = $self->_delete2F( $uid, undef, $id );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
return $self->sendError( $req, "2F id '$id' not found for user '$uid'",
|
|
|
|
404 )
|
|
|
|
unless ( $res->{removed} > 0 );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
|
|
|
|
}
|
|
|
|
|
|
|
|
sub deleteSecondFactorsByType {
|
|
|
|
my ( $self, $req ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $uid, $type, $res );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
$uid = $req->params('uid')
|
|
|
|
or return $self->sendError( $req, 'uid is missing', 400 );
|
|
|
|
|
|
|
|
$type = $req->params('type')
|
|
|
|
or return $self->sendError( $req, 'type is missing', 400 );
|
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"[API] Delete all 2F for $uid with type $type requested");
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$res = $self->_delete2F( $uid, uc $type );
|
2019-12-20 11:05:16 +01:00
|
|
|
|
|
|
|
return $self->sendError( $req, $res->{msg}, $res->{code} )
|
2019-12-20 11:13:15 +01:00
|
|
|
unless ( $res->{res} eq 'ok' );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return $self->sendJSONresponse( $req, { message => $res->{msg} } );
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _get2F {
|
|
|
|
my ( $self, $uid, $type, $id ) = @_;
|
2019-12-20 11:13:15 +01:00
|
|
|
my ( $res, $psessions, @secondFactors );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
if ( defined $type ) {
|
2019-12-19 16:52:51 +01:00
|
|
|
$res = $self->_checkType($type);
|
2019-12-20 11:13:15 +01:00
|
|
|
return $res if ( $res->{res} ne 'ok' );
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
|
|
|
|
2020-01-02 16:55:44 +01:00
|
|
|
$psessions = $self->_getSessions2F( $self->_getPersistentMod, 'Persistent',
|
|
|
|
'_session_uid', $uid );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2020-01-10 21:29:11 +01:00
|
|
|
foreach ( keys %$psessions ) {
|
2020-03-09 11:10:43 +01:00
|
|
|
my $devices = $self->_getDevicesFromSessionData( $psessions->{$_} );
|
2019-12-19 16:52:51 +01:00
|
|
|
foreach my $device ( @{$devices} ) {
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Check device [epoch=$device->{epoch}, type=$device->{type}, name=$device->{name}]"
|
|
|
|
);
|
|
|
|
push @secondFactors,
|
|
|
|
{
|
2020-08-17 18:18:52 +02:00
|
|
|
id => genId2F($device),
|
2019-12-20 11:13:15 +01:00
|
|
|
type => $device->{type},
|
|
|
|
name => $device->{name}
|
|
|
|
}
|
|
|
|
unless ( ( defined $type and $type ne $device->{type} )
|
2020-08-17 18:18:52 +02:00
|
|
|
or ( defined $id and $id ne genId2F($device) ) );
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
|
|
|
}
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
"Found " . scalar @secondFactors . " 2F devices for uid $uid." );
|
|
|
|
return { res => 'ok', secondFactors => [@secondFactors] };
|
2019-12-20 11:05:16 +01:00
|
|
|
}
|
|
|
|
|
2020-01-02 16:43:53 +01:00
|
|
|
sub _getPersistentMod {
|
2019-12-20 11:13:15 +01:00
|
|
|
my ($self) = @_;
|
2019-12-19 16:52:51 +01:00
|
|
|
my $mod = $self->sessionTypes->{persistent};
|
|
|
|
$mod->{options}->{backend} = $mod->{module};
|
|
|
|
return $mod;
|
|
|
|
}
|
2019-12-20 11:13:15 +01:00
|
|
|
|
2020-01-02 16:43:53 +01:00
|
|
|
sub _getSSOMod {
|
|
|
|
my ($self) = @_;
|
|
|
|
my $mod = $self->sessionTypes->{global};
|
|
|
|
$mod->{options}->{backend} = $mod->{module};
|
|
|
|
return $mod;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _getSessions2F {
|
|
|
|
my ( $self, $mod, $kind, $key, $uid ) = @_;
|
|
|
|
$self->logger->debug("Looking for sessions for uid $uid ...");
|
2020-01-02 16:55:44 +01:00
|
|
|
my $sessions =
|
|
|
|
Lemonldap::NG::Common::Apache::Session->searchOn( $mod->{options}, $key,
|
|
|
|
$uid,
|
2019-12-20 11:13:15 +01:00
|
|
|
( '_session_kind', '_session_uid', '_session_id', '_2fDevices' ) );
|
2020-01-10 21:29:11 +01:00
|
|
|
foreach ( keys %$sessions ) {
|
2020-01-02 16:43:53 +01:00
|
|
|
delete $sessions->{$_}
|
|
|
|
unless ( $sessions->{$_}->{_session_kind} eq $kind );
|
2019-12-20 11:05:16 +01:00
|
|
|
}
|
2020-01-02 16:55:44 +01:00
|
|
|
$self->logger->debug( "Found "
|
2020-01-10 21:29:11 +01:00
|
|
|
. scalar( keys %$sessions )
|
2020-01-02 16:55:44 +01:00
|
|
|
. " $kind sessions for uid $uid." );
|
2020-01-02 16:43:53 +01:00
|
|
|
|
|
|
|
return $sessions;
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
|
|
|
|
2020-01-02 16:43:53 +01:00
|
|
|
sub _getSession2F {
|
|
|
|
my ( $self, $sessionId, $mod ) = @_;
|
2019-12-19 16:52:51 +01:00
|
|
|
$self->logger->debug("Looking for session with sessionId $sessionId ...");
|
2020-01-02 16:43:53 +01:00
|
|
|
my $session = $self->getApacheSession( $mod, $sessionId );
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
|
|
|
defined $session
|
|
|
|
? "Session $sessionId found."
|
2020-01-02 16:55:44 +01:00
|
|
|
: " No session found for sessionId $sessionId"
|
|
|
|
);
|
2020-01-02 16:43:53 +01:00
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
return $session;
|
|
|
|
}
|
|
|
|
|
2020-01-02 16:43:53 +01:00
|
|
|
sub _delete2FFromSessions {
|
|
|
|
my ( $self, $uid, $type, $id, $mod, $kind, $key ) = @_;
|
2020-01-02 16:55:44 +01:00
|
|
|
my ( $sessions, $session, $devices, @keep, $removed,
|
|
|
|
$total, $module, $localStorage );
|
|
|
|
$sessions = $self->_getSessions2F( $mod, $kind, $key, $uid );
|
2020-01-10 21:29:11 +01:00
|
|
|
foreach ( keys %$sessions ) {
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2020-01-02 16:55:44 +01:00
|
|
|
$session = $self->_getSession2F( $_, $mod )
|
2019-12-20 11:13:15 +01:00
|
|
|
or return { res => 'ko', code => 500, msg => $@ };
|
2019-12-19 16:52:51 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
2020-01-02 16:43:53 +01:00
|
|
|
"Looking for 2F Device(s) attached to sessionId $_");
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
if ( $session->data->{_2fDevices} ) {
|
2020-03-09 11:10:43 +01:00
|
|
|
$devices = $self->_getDevicesFromSessionData( $session->data );
|
|
|
|
$total = scalar @$devices;
|
2020-01-02 16:55:44 +01:00
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
$self->logger->debug(
|
2020-01-02 16:43:53 +01:00
|
|
|
"Found $total 2F devices attached to sessionId $_");
|
2020-01-02 16:55:44 +01:00
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
@keep = ();
|
|
|
|
while (@$devices) {
|
|
|
|
my $element = shift @$devices;
|
2020-01-02 16:55:44 +01:00
|
|
|
if (
|
|
|
|
( defined $type or defined $id )
|
|
|
|
and ( ( defined $type and $type ne $element->{type} )
|
2020-08-17 18:18:52 +02:00
|
|
|
or ( defined $id and $id ne genId2F($element) ) )
|
2020-01-02 16:55:44 +01:00
|
|
|
)
|
|
|
|
{
|
2020-01-02 16:43:53 +01:00
|
|
|
push @keep, $element;
|
2020-01-02 16:55:44 +01:00
|
|
|
}
|
|
|
|
else {
|
2020-08-17 18:18:52 +02:00
|
|
|
$removed->{ genId2F($element) } = "removed";
|
2020-01-02 16:43:53 +01:00
|
|
|
}
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
2020-01-02 16:55:44 +01:00
|
|
|
if ( ( $total - scalar @keep ) > 0 ) {
|
2019-12-20 11:13:15 +01:00
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
# Update session
|
2020-01-02 16:55:44 +01:00
|
|
|
$self->logger->debug( "Removing "
|
|
|
|
. ( $total - scalar @keep )
|
|
|
|
. " 2F device(s) attached to sessionId $_ ..." );
|
2019-12-19 16:52:51 +01:00
|
|
|
$session->data->{_2fDevices} = to_json( \@keep );
|
2020-01-10 21:29:11 +01:00
|
|
|
$session->update( $session->data );
|
2020-01-24 14:50:16 +01:00
|
|
|
|
2020-01-02 16:43:53 +01:00
|
|
|
# Delete from local cache
|
2020-01-02 16:55:44 +01:00
|
|
|
if ( $session->{options}->{localStorage} ) {
|
2020-01-02 16:43:53 +01:00
|
|
|
$module = $session->{options}->{localStorage};
|
|
|
|
eval "use $module;";
|
2020-01-02 16:55:44 +01:00
|
|
|
$localStorage =
|
|
|
|
$module->new(
|
|
|
|
$session->{options}->{localStorageOptions} );
|
|
|
|
if ( $localStorage->get($_) ) {
|
|
|
|
$self->logger->debug(
|
|
|
|
"Delete local cache for session $_");
|
2020-01-02 16:43:53 +01:00
|
|
|
$localStorage->remove($_);
|
|
|
|
}
|
2019-12-20 11:13:15 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->debug(
|
2020-01-02 16:43:53 +01:00
|
|
|
"No matching 2F devices attached to sessionId $_ were selected for removal."
|
2019-12-20 11:13:15 +01:00
|
|
|
);
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
2019-12-20 11:13:15 +01:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->debug(
|
2020-01-02 16:43:53 +01:00
|
|
|
"No 2F devices attached to sessionId $_ were found.");
|
2019-12-19 16:52:51 +01:00
|
|
|
}
|
|
|
|
}
|
2020-01-02 16:43:53 +01:00
|
|
|
|
2020-01-02 16:55:44 +01:00
|
|
|
return { res => 'ok', removed => $removed };
|
2020-01-02 16:43:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
sub _delete2F {
|
|
|
|
my ( $self, $uid, $type, $id ) = @_;
|
2020-01-02 16:55:44 +01:00
|
|
|
my ( $res, $removed, $count );
|
2020-01-02 16:43:53 +01:00
|
|
|
if ( defined $type ) {
|
|
|
|
$res = $self->_checkType($type);
|
|
|
|
return $res if ( $res->{res} ne 'ok' );
|
|
|
|
}
|
|
|
|
|
2020-01-02 16:55:44 +01:00
|
|
|
$res =
|
|
|
|
$self->_delete2FFromSessions( $uid, $type, $id, $self->_getPersistentMod,
|
|
|
|
'Persistent', '_session_uid' );
|
2020-01-02 16:43:53 +01:00
|
|
|
return $res if ( $res->{res} ne 'ok' );
|
2020-01-10 20:11:41 +01:00
|
|
|
$removed = $res->{removed} || {};
|
2020-01-02 16:43:53 +01:00
|
|
|
|
2020-01-24 14:50:16 +01:00
|
|
|
my $whatToTrace = Lemonldap::NG::Handler::PSGI::Main->tsv->{whatToTrace};
|
2020-01-02 16:55:44 +01:00
|
|
|
$res =
|
|
|
|
$self->_delete2FFromSessions( $uid, $type, $id, $self->_getSSOMod, 'SSO',
|
2020-01-24 14:50:16 +01:00
|
|
|
$whatToTrace );
|
2020-01-02 16:43:53 +01:00
|
|
|
return $res if ( $res->{res} ne 'ok' );
|
2020-01-10 20:11:41 +01:00
|
|
|
$res->{removed} ||= {};
|
2020-01-02 16:43:53 +01:00
|
|
|
|
2020-01-02 16:55:44 +01:00
|
|
|
# merge results
|
2020-01-10 20:11:41 +01:00
|
|
|
$removed = { %$removed, %{ $res->{removed} } };
|
2020-01-10 21:29:11 +01:00
|
|
|
$count = scalar( keys %$removed );
|
2020-01-02 16:43:53 +01:00
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
return {
|
2019-12-20 11:13:15 +01:00
|
|
|
res => 'ok',
|
2020-01-02 16:43:53 +01:00
|
|
|
removed => $count,
|
|
|
|
msg => $count > 0
|
|
|
|
? "Successful operation: " . $count . " 2F were removed"
|
2019-12-20 11:13:15 +01:00
|
|
|
: "No operation performed"
|
2019-12-19 16:52:51 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-03-09 11:10:43 +01:00
|
|
|
sub _getDevicesFromSessionData {
|
|
|
|
my ( $self, $sessiondata ) = @_;
|
|
|
|
if ( $sessiondata->{_2fDevices} ) {
|
|
|
|
my $devices;
|
|
|
|
eval { $devices = from_json( $sessiondata->{_2fDevices} ); };
|
|
|
|
if ($@) {
|
|
|
|
$self->logger->warn("Error deserializing _2fDevices: $@");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if ( ref($devices) eq "ARRAY" ) {
|
|
|
|
return $devices;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$self->logger->warn(
|
|
|
|
"Error deserializing _2fDevices: not a JSON Array");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2019-12-19 16:52:51 +01:00
|
|
|
sub _checkType {
|
|
|
|
my ( $self, $type ) = @_;
|
|
|
|
|
2019-12-20 11:13:15 +01:00
|
|
|
return {
|
|
|
|
res => "ko",
|
2020-04-01 20:46:30 +02:00
|
|
|
code => 400,
|
2019-12-20 11:13:15 +01:00
|
|
|
msg =>
|
|
|
|
"Invalid input: Type \"$type\" does not exist. Allowed values for type are: \"U2F\", \"TOTP\" or \"UBK\""
|
|
|
|
}
|
|
|
|
unless ( $type =~ /\b(?:U2F|TOTP|UBK)\b/ );
|
2019-12-19 16:52:51 +01:00
|
|
|
|
|
|
|
return { res => "ok" };
|
2019-11-08 18:31:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
1;
|