Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.17% covered (warning)
73.17%
30 / 41
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthenticationSubscriber
73.17% covered (warning)
73.17%
30 / 41
50.00% covered (danger)
50.00%
2 / 4
11.93
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getSubscribedEvents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 onAuthFailure
66.67% covered (warning)
66.67%
14 / 21
0.00% covered (danger)
0.00%
0 / 1
4.59
 onAuthSuccess
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
4.77
1<?php
2
3namespace App\Domain\User\Symfony\EventSubscriber\Security;
4
5use App\Domain\User\Exception\ExceededLoginAttemptsException;
6use App\Domain\User\Model\LoginAttempt;
7use App\Domain\User\Repository\LoginAttempt as LoginAttemptRepository;
8use App\Domain\User\Repository\User as UserRepository;
9use Symfony\Component\EventDispatcher\EventDispatcherInterface;
10use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11use Symfony\Component\HttpFoundation\RequestStack;
12use Symfony\Component\Security\Core\AuthenticationEvents;
13use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
14use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
15
16class AuthenticationSubscriber implements EventSubscriberInterface
17{
18    protected RequestStack $requestStack;
19    protected LoginAttemptRepository $loginAttemptRepository;
20    protected UserRepository $userRepository;
21    protected int $maxAttempts;
22    protected EventDispatcherInterface $dispatcher;
23
24    public function __construct(
25        RequestStack $requestStack,
26        LoginAttemptRepository $loginAttemptRepository,
27        UserRepository $userRepository,
28        int $maxAttempts,
29        EventDispatcherInterface $dispatcher,
30    ) {
31        $this->requestStack           = $requestStack;
32        $this->loginAttemptRepository = $loginAttemptRepository;
33        $this->userRepository         = $userRepository;
34        $this->maxAttempts            = $maxAttempts;
35        $this->dispatcher             = $dispatcher;
36    }
37
38    public static function getSubscribedEvents(): array
39    {
40        return [
41            AuthenticationEvents::AUTHENTICATION_FAILURE => 'onAuthFailure',
42            AuthenticationEvents::AUTHENTICATION_SUCCESS => 'onAuthSuccess',
43        ];
44    }
45
46    public function onAuthFailure(AuthenticationFailureEvent $event)
47    {
48        $ip      = $this->requestStack->getCurrentRequest()->getClientIp();
49        $email   = $event->getAuthenticationToken()->getUsername();
50        $attempt = $this->loginAttemptRepository->findOneOrNullByIpAndEmail($ip, $email);
51
52        // If this is the first login attempt, create new entity
53        if (null === $attempt) {
54            $attempt = new LoginAttempt();
55            $attempt->setAttempts(0);
56            $attempt->setIp($ip);
57            $attempt->setEmail($email);
58            $this->loginAttemptRepository->insert($attempt);
59        }
60
61        $n = $attempt->getAttempts();
62
63        $attempt->setAttempts($attempt->getAttempts() + 1);
64
65        if ($attempt->getAttempts() > $this->maxAttempts) {
66            $user = $this->userRepository->findOneOrNullByEmail($email);
67            // Disable this user if it exists and the maximum number of login attempts was exceeded
68            if ($user) {
69                $user->setEnabled(false);
70                $attempt->setAttempts(0);
71                $this->loginAttemptRepository->update($attempt);
72                $this->userRepository->update($user);
73
74                throw new ExceededLoginAttemptsException();
75            }
76        }
77
78        $this->loginAttemptRepository->update($attempt);
79        // Exponential wait time for wrong passwords
80        sleep($n);
81    }
82
83    public function onAuthSuccess(AuthenticationSuccessEvent $event)
84    {
85        if (!$event->getAuthenticationToken()) {
86            return;
87        }
88
89        $user = $event->getAuthenticationToken()->getUser();
90        if (is_string($user)) {
91            return;
92        }
93
94        $ip      = $this->requestStack->getCurrentRequest()->getClientIp();
95        $email   = $event->getAuthenticationToken()->getUsername();
96        $attempt = $this->loginAttemptRepository->findOneOrNullByIpAndEmail($ip, $email);
97        // If the attempt exists, we reset the number of attempts to zero on successful login.
98        if ($attempt) {
99            $attempt->setAttempts(0);
100            $this->loginAttemptRepository->update($attempt);
101        }
102    }
103}