|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Authserver, an OAuth2-based single-signon authentication provider written in PHP.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2017 Lars Vierbergen
|
|
|
|
*
|
|
|
|
* his program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License as
|
|
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
namespace vierbergenlars\AuthserverStatsBundle\EventListener;
|
|
|
|
|
|
|
|
use App\Entity\User;
|
|
|
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
|
|
use vierbergenlars\AuthserverStatsBundle\Event\StatsEvent;
|
|
|
|
use Symfony\Bridge\Doctrine\RegistryInterface;
|
|
|
|
use Symfony\Component\Security\Core\AuthenticationEvents;
|
|
|
|
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
|
|
|
|
use vierbergenlars\AuthserverStatsBundle\Entity\AuthenticationEntry;
|
|
|
|
use Symfony\Component\HttpFoundation\RequestStack;
|
|
|
|
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
|
|
|
|
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
|
|
|
|
use Symfony\Component\Security\Http\FirewallMapInterface;
|
|
|
|
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
|
|
|
|
use Symfony\Component\Security\Http\SecurityEvents;
|
|
|
|
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
|
|
|
|
|
|
|
class AuthenticationStatsListener implements EventSubscriberInterface
|
|
|
|
{
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @var RegistryInterface
|
|
|
|
*/
|
|
|
|
private $registry;
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @var RequestStack
|
|
|
|
*/
|
|
|
|
private $requestStack;
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @var FirewallMapInterface
|
|
|
|
*/
|
|
|
|
private $firewallMap;
|
|
|
|
|
|
|
|
public static function getSubscribedEvents()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
StatsEvent::class => [
|
|
|
|
[
|
|
|
|
'getAuthStats',
|
|
|
|
-1
|
|
|
|
],
|
|
|
|
[
|
|
|
|
'getAuthFailureIps'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
AuthenticationEvents::AUTHENTICATION_SUCCESS => 'onAuthSuccess',
|
|
|
|
AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthFailure',
|
|
|
|
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __construct(RegistryInterface $registry, RequestStack $requestStack, FirewallMapInterface $firewallMap)
|
|
|
|
{
|
|
|
|
$this->registry = $registry;
|
|
|
|
$this->requestStack = $requestStack;
|
|
|
|
$this->firewallMap = $firewallMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function insertAuthSuccess(User $user)
|
|
|
|
{
|
|
|
|
$request = $this->requestStack->getMasterRequest();
|
|
|
|
$authSuccess = new AuthenticationEntry($request->getClientIp(), true, $user);
|
|
|
|
$em = $this->registry->getManagerForClass(AuthenticationEntry::class);
|
|
|
|
$em->persist($authSuccess);
|
|
|
|
$em->flush($authSuccess);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onInteractiveLogin(InteractiveLoginEvent $event)
|
|
|
|
{
|
|
|
|
if ($this->isStatelessFirewall())
|
|
|
|
return;
|
|
|
|
$this->insertAuthSuccess($event->getAuthenticationToken()
|
|
|
|
->getUser());
|
|
|
|
}
|
|
|
|
|
|
|
|
private function isStatelessFirewall()
|
|
|
|
{
|
|
|
|
$request = $this->requestStack->getMasterRequest();
|
|
|
|
if ($this->firewallMap instanceof FirewallMap) {
|
|
|
|
$config = $this->firewallMap->getFirewallConfig($request);
|
|
|
|
/* @var $config \Symfony\Bundle\SecurityBundle\Security\FirewallConfig */
|
|
|
|
if ($config) {
|
|
|
|
if ($config->isStateless())
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onAuthSuccess(AuthenticationEvent $event)
|
|
|
|
{
|
|
|
|
if ($event->getAuthenticationToken() instanceof AnonymousToken)
|
|
|
|
return;
|
|
|
|
if (!$this->isStatelessFirewall())
|
|
|
|
return;
|
|
|
|
$this->insertAuthSuccess($event->getAuthenticationToken()
|
|
|
|
->getUser());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onAuthFailure(AuthenticationFailureEvent $event)
|
|
|
|
{
|
|
|
|
$authFailure = new AuthenticationEntry($this->requestStack->getMasterRequest()->getClientIp(), false);
|
|
|
|
$em = $this->registry->getManagerForClass(AuthenticationEntry::class);
|
|
|
|
$em->persist($authFailure);
|
|
|
|
$em->flush($authFailure);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getAuthStats(StatsEvent $event)
|
|
|
|
{
|
|
|
|
if (!$event->isEnabled('login'))
|
|
|
|
return;
|
|
|
|
|
|
|
|
$event->setMuninConfig('login', $event->getMuninConfig('login') + [
|
|
|
|
'auth_succ.label' => 'Authentication passes',
|
|
|
|
'auth_succ.type' => 'COUNTER',
|
|
|
|
'auth_fail.label' => 'Authentication failures',
|
|
|
|
'auth_fail.type' => 'COUNTER'
|
|
|
|
]);
|
|
|
|
|
|
|
|
$queryBuilder = $this->registry->getRepository(AuthenticationEntry::class)->createQueryBuilder('e');
|
|
|
|
/* @var $queryBuilder \Doctrine\ORM\QueryBuilder */
|
|
|
|
$queryBuilder->select('count(e)', 'e.success')->groupBy('e.success');
|
|
|
|
$rawStats = $queryBuilder->getQuery()->getArrayResult();
|
|
|
|
$stats = [
|
|
|
|
'login.auth_succ' => 0,
|
|
|
|
'login.auth_fail' => 0
|
|
|
|
];
|
|
|
|
foreach ($rawStats as $rawStat) {
|
|
|
|
if ($rawStat['success']) {
|
|
|
|
$stats['login.auth_succ'] += $rawStat['1'];
|
|
|
|
} else {
|
|
|
|
$stats['login.auth_fail'] += $rawStat['1'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->addStatistics($stats);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getAuthFailureIps(StatsEvent $event)
|
|
|
|
{
|
|
|
|
if (!$event->isEnabled('login_fail_ips'))
|
|
|
|
return;
|
|
|
|
$queryBuilder = $this->registry->getRepository(AuthenticationEntry::class)->createQueryBuilder('e');
|
|
|
|
/* @var $queryBuilder \Doctrine\ORM\QueryBuilder */
|
|
|
|
$queryBuilder->select('count(e) AS c', 'e.ip')
|
|
|
|
->groupBy('e.ip')
|
|
|
|
->where('e.success = false AND e.timeStamp > :time')
|
|
|
|
->setParameter('time', new \DateTime('-1 day'))
|
|
|
|
->orderBy('c', 'DESC')
|
|
|
|
->setMaxResults(20);
|
|
|
|
$rawStats = $queryBuilder->getQuery()->getArrayResult();
|
|
|
|
|
|
|
|
$config = [
|
|
|
|
'graph_title' => 'Authserver authentication failures',
|
|
|
|
'graph_vlabel' => 'Failures/day',
|
|
|
|
'graph_category' => 'authserver'
|
|
|
|
];
|
|
|
|
foreach ($rawStats as $rawStat) {
|
|
|
|
$ipHash = md5($rawStat['ip']);
|
|
|
|
$config += [
|
|
|
|
'auth_fail_' . $ipHash . '.label' => $rawStat['ip']
|
|
|
|
];
|
|
|
|
|
|
|
|
$event->addStatistic('login_fail_ips.auth_fail_' . $ipHash, $rawStat['c']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$event->setMuninConfig('login_fail_ips', $config);
|
|
|
|
}
|
|
|
|
}
|