|
|
|
@ -12,7 +12,7 @@ our $VERSION = '2.0.10'; |
|
|
|
|
extends 'Lemonldap::NG::Portal::Main::Plugin'; |
|
|
|
|
|
|
|
|
|
# INITIALIZATION |
|
|
|
|
use constant afterSub => { storeHistory => 'run' }; |
|
|
|
|
use constant aroundSub => { authenticate => 'check' }; |
|
|
|
|
|
|
|
|
|
has lockTimes => ( |
|
|
|
|
is => 'rw', |
|
|
|
@ -25,8 +25,15 @@ has maxAge => ( |
|
|
|
|
isa => 'Int' |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
has maxFailed => ( |
|
|
|
|
is => 'rw', |
|
|
|
|
isa => 'Int' |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
sub init { |
|
|
|
|
my ($self) = @_; |
|
|
|
|
$self->maxFailed( abs $self->conf->{bruteForceProtectionMaxFailed} ); |
|
|
|
|
|
|
|
|
|
if ( $self->conf->{disablePersistentStorage} ) { |
|
|
|
|
$self->logger->error( |
|
|
|
|
'"BruteForceProtection" plugin enabled WITHOUT persistent session storage"' |
|
|
|
@ -40,13 +47,11 @@ sub init { |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
unless ( $self->conf->{failedLoginNumber} > |
|
|
|
|
$self->conf->{bruteForceProtectionMaxFailed} ) |
|
|
|
|
{ |
|
|
|
|
unless ( $self->conf->{failedLoginNumber} > $self->maxFailed ) { |
|
|
|
|
$self->logger->error( 'Number of failed logins history (' |
|
|
|
|
. $self->conf->{failedLoginNumber} |
|
|
|
|
. ') must be higher than allowed failed logins attempt (' |
|
|
|
|
. $self->conf->{bruteForceProtectionMaxFailed} |
|
|
|
|
. $self->maxFailed |
|
|
|
|
. ')' ); |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
@ -62,21 +67,16 @@ sub init { |
|
|
|
|
split /\s*,\s*/, $self->conf->{bruteForceProtectionLockTimes}; |
|
|
|
|
|
|
|
|
|
unless ($lockTimes) { |
|
|
|
|
@{ $self->lockTimes } = ( 5, 15, 60, 300, 600 ); |
|
|
|
|
@{ $self->lockTimes } = ( 15, 30, 60, 300, 600 ); |
|
|
|
|
$lockTimes = 5; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for ( |
|
|
|
|
my $i = 1 ; |
|
|
|
|
$i <= $self->conf->{bruteForceProtectionMaxFailed} ; |
|
|
|
|
$i++ |
|
|
|
|
) |
|
|
|
|
{ |
|
|
|
|
for ( my $i = 1 ; $i < $self->maxFailed ; $i++ ) { |
|
|
|
|
unshift @{ $self->lockTimes }, 0; |
|
|
|
|
$lockTimes++; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
unless ( $lockTimes < $self->conf->{failedLoginNumber} ) { |
|
|
|
|
unless ( $lockTimes <= $self->conf->{failedLoginNumber} ) { |
|
|
|
|
$self->logger->warn( 'Number failed logins history (' |
|
|
|
|
. $self->conf->{failedLoginNumber} |
|
|
|
|
. ') must be higher than incremental lock time values plus allowed failed logins attempt (' |
|
|
|
@ -91,76 +91,83 @@ sub init { |
|
|
|
|
$self->maxAge($sum); |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
$self->maxAge( $self->conf->{bruteForceProtectionMaxAge} ); |
|
|
|
|
$self->maxAge( $self->conf->{bruteForceProtectionMaxAge} * |
|
|
|
|
( 1 + $self->maxFailed ) ); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
# RUNNING METHOD |
|
|
|
|
sub run { |
|
|
|
|
my ( $self, $req ) = @_; |
|
|
|
|
my $now = time; |
|
|
|
|
sub check { |
|
|
|
|
my ( $self, $sub, $req ) = @_; |
|
|
|
|
my $now = time; |
|
|
|
|
$self->p->setSessionInfo($req); |
|
|
|
|
$self->logger->debug("Retrieve $req->{user} logins history"); |
|
|
|
|
$self->p->setPersistentSessionInfo( $req, $req->{user} ); |
|
|
|
|
|
|
|
|
|
my $countFailed = my @failedLogins = |
|
|
|
|
map { ( $now - $_->{_utime} ) < $self->maxAge ? $_ : () } |
|
|
|
|
map { ( $now - $_->{_utime} ) <= $self->maxAge ? $_ : () } |
|
|
|
|
@{ $req->sessionInfo->{_loginHistory}->{failedLogin} }; |
|
|
|
|
$self->logger->debug( ' Failed login maxAge = ' . $self->maxAge ); |
|
|
|
|
$self->logger->debug( ' -> Failed login maxAge = ' . $self->maxAge ); |
|
|
|
|
$self->logger->debug( |
|
|
|
|
" Number of failed login(s) to take into account = $countFailed"); |
|
|
|
|
"Number of failed login(s) to take into account = $countFailed"); |
|
|
|
|
my $lastFailedLoginEpoch = $failedLogins[0]->{_utime} || undef; |
|
|
|
|
|
|
|
|
|
if ( $self->conf->{bruteForceProtectionIncrementalTempo} ) { |
|
|
|
|
my $lastFailedLoginEpoch = $failedLogins[0]->{_utime} || undef; |
|
|
|
|
|
|
|
|
|
return PE_OK unless $lastFailedLoginEpoch; |
|
|
|
|
return $sub->($req) unless $lastFailedLoginEpoch; |
|
|
|
|
|
|
|
|
|
# Delta between current attempt and last failed login |
|
|
|
|
my $delta = $now - $lastFailedLoginEpoch; |
|
|
|
|
$self->logger->debug(" -> Delta = $delta"); |
|
|
|
|
|
|
|
|
|
# Time to wait |
|
|
|
|
my $waitingTime = $self->lockTimes->[ $countFailed - 1 ] |
|
|
|
|
// $self->conf->{bruteForceProtectionMaxLockTime}; |
|
|
|
|
|
|
|
|
|
# Reach last tempo. Stop to increase waiting time |
|
|
|
|
if ( $countFailed >= scalar @{ $self->lockTimes } ) { |
|
|
|
|
$self->userLogger->warn( |
|
|
|
|
"BruteForceProtection: Last lock time has been reached"); |
|
|
|
|
$self->logger->debug("Force waitingTime to last value"); |
|
|
|
|
$waitingTime = |
|
|
|
|
$self->lockTimes->[ scalar @{ $self->lockTimes } - 1 ]; |
|
|
|
|
} |
|
|
|
|
$self->logger->debug(" -> Waiting time = $waitingTime"); |
|
|
|
|
if ( $waitingTime && $delta <= $waitingTime ) { |
|
|
|
|
$self->logger->debug("BruteForceProtection enabled"); |
|
|
|
|
$req->lockTime($waitingTime); |
|
|
|
|
|
|
|
|
|
# Delta < waitingTime => wait |
|
|
|
|
if ( $waitingTime && $delta < $waitingTime ) { |
|
|
|
|
$self->userLogger->warn("BruteForceProtection enabled"); |
|
|
|
|
$req->authResult(PE_WAIT); |
|
|
|
|
|
|
|
|
|
# Do not store failed login if last tempo or max tempo is reached |
|
|
|
|
$self->p->registerLogin( $req, $req->{user} ) |
|
|
|
|
if ( $waitingTime < $self->conf->{bruteForceProtectionMaxLockTime} |
|
|
|
|
&& $waitingTime < |
|
|
|
|
$self->lockTimes->[ scalar @{ $self->lockTimes } - 1 ] ); |
|
|
|
|
$req->lockTime( $waitingTime - $delta ); |
|
|
|
|
return PE_WAIT; |
|
|
|
|
} |
|
|
|
|
return PE_OK; |
|
|
|
|
return $sub->($req); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return PE_OK |
|
|
|
|
if ( $countFailed <= $self->conf->{bruteForceProtectionMaxFailed} ); |
|
|
|
|
return $sub->($req) |
|
|
|
|
if ( $countFailed < $self->maxFailed ); |
|
|
|
|
|
|
|
|
|
my @lastFailedLoginEpoch = (); |
|
|
|
|
my $MaxAge = $self->maxAge + 1; |
|
|
|
|
|
|
|
|
|
# Auth_N-2 failed login epoch |
|
|
|
|
foreach ( 0 .. $self->conf->{bruteForceProtectionMaxFailed} - 1 ) { |
|
|
|
|
push @lastFailedLoginEpoch, |
|
|
|
|
$req->sessionInfo->{_loginHistory}->{failedLogin}->[$_]->{_utime} |
|
|
|
|
if ( $req->sessionInfo->{_loginHistory}->{failedLogin}->[$_] ); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
# If Auth_N-MaxFailed older than MaxAge -> another try allowed |
|
|
|
|
$MaxAge = |
|
|
|
|
$lastFailedLoginEpoch[0] - |
|
|
|
|
$lastFailedLoginEpoch[ $self->conf->{bruteForceProtectionMaxFailed} - 1 ] |
|
|
|
|
if $self->conf->{bruteForceProtectionMaxFailed}; |
|
|
|
|
$self->logger->debug(" -> MaxAge = $MaxAge"); |
|
|
|
|
|
|
|
|
|
return PE_OK |
|
|
|
|
if ( $MaxAge > $self->maxAge ); |
|
|
|
|
|
|
|
|
|
# Delta between the two last failed logins -> Auth_N - Auth_N-1 |
|
|
|
|
my $delta = |
|
|
|
|
defined $lastFailedLoginEpoch[1] ? $now - $lastFailedLoginEpoch[1] : 0; |
|
|
|
|
# Delta between current attempt and last failed login |
|
|
|
|
my $delta = $lastFailedLoginEpoch ? $now - $lastFailedLoginEpoch : 0; |
|
|
|
|
$self->logger->debug(" -> Delta = $delta"); |
|
|
|
|
|
|
|
|
|
# Delta between the two last failed logins < Tempo => wait |
|
|
|
|
return PE_OK |
|
|
|
|
unless ( $delta <= $self->conf->{bruteForceProtectionTempo} ); |
|
|
|
|
# Delta < Tempo => wait |
|
|
|
|
return $sub->($req) |
|
|
|
|
unless ( $delta < $self->conf->{bruteForceProtectionTempo} |
|
|
|
|
&& $countFailed ); |
|
|
|
|
|
|
|
|
|
# Account locked |
|
|
|
|
$self->logger->debug("BruteForceProtection enabled"); |
|
|
|
|
$req->lockTime( $self->conf->{bruteForceProtectionTempo} ); |
|
|
|
|
$self->userLogger->warn("BruteForceProtection enabled"); |
|
|
|
|
$self->logger->debug( |
|
|
|
|
" -> Waiting time = $self->{conf}->{bruteForceProtectionTempo}"); |
|
|
|
|
$req->lockTime( $self->conf->{bruteForceProtectionTempo} - $delta ); |
|
|
|
|
return PE_WAIT; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|