Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
73.17% |
30 / 41 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
AuthenticationSubscriber | |
73.17% |
30 / 41 |
|
50.00% |
2 / 4 |
11.93 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getSubscribedEvents | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
onAuthFailure | |
66.67% |
14 / 21 |
|
0.00% |
0 / 1 |
4.59 | |||
onAuthSuccess | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
4.77 |
1 | <?php |
2 | |
3 | namespace App\Domain\User\Symfony\EventSubscriber\Security; |
4 | |
5 | use App\Domain\User\Exception\ExceededLoginAttemptsException; |
6 | use App\Domain\User\Model\LoginAttempt; |
7 | use App\Domain\User\Repository\LoginAttempt as LoginAttemptRepository; |
8 | use App\Domain\User\Repository\User as UserRepository; |
9 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
10 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
11 | use Symfony\Component\HttpFoundation\RequestStack; |
12 | use Symfony\Component\Security\Core\AuthenticationEvents; |
13 | use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; |
14 | use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; |
15 | |
16 | class 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 | } |