Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
24 / 24
94.74% covered (success)
94.74%
18 / 19
92.31% covered (success)
92.31%
12 / 13
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AuthRateLimitSubscriber
100.00% covered (success)
100.00%
24 / 24
94.74% covered (success)
94.74%
18 / 19
92.31% covered (success)
92.31%
12 / 13
100.00% covered (success)
100.00%
6 / 6
11.06
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isMainRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 routeShouldBeLimited
100.00% covered (success)
100.00%
6 / 6
83.33% covered (warning)
83.33%
5 / 6
66.67% covered (warning)
66.67%
2 / 3
100.00% covered (success)
100.00%
1 / 1
2.15
 limitShouldBeApplied
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 onKernelController
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getSubscribedEvents
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Auth\Infrastructure\EventSubscriber;
4
5use Symfony\Bundle\SecurityBundle\Security;
6use Symfony\Component\EventDispatcher\EventSubscriberInterface;
7use Symfony\Component\HttpFoundation\Request;
8use Symfony\Component\HttpKernel\Event\ControllerEvent;
9use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
10use Symfony\Component\HttpKernel\HttpKernelInterface;
11use Symfony\Component\HttpKernel\KernelEvents;
12use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
13
14/**
15 * This rate limit applies to the Identity 'slice' only
16 * Other slices may have their own subscribers (or not)
17 * TODO : maybe we should consider the rate limit a global feature instead or relying on each slice to implement it
18 * Although it may seem 'cleaner' to have each slice implement it, it may result in code duplication
19 * Moreover, should slices become isolated micro services, the rate limiting would probably be handled differently
20 * We should make a decision, but it's not that important at the moment
21 *
22 * @author Wilhelm Zwertvaegher
23 *
24 */
25readonly class AuthRateLimitSubscriber implements EventSubscriberInterface
26{
27    public function __construct(
28        private Security                    $security,
29        private RateLimiterFactoryInterface $registerByIpLimiter,
30        private RateLimiterFactoryInterface $apiByUserLimiter
31    ) {
32    }
33
34    public function isMainRequest(ControllerEvent $event): bool
35    {
36        return $event->getRequestType() === HttpKernelInterface::MAIN_REQUEST;
37    }
38
39    public function routeShouldBeLimited(Request $request): bool
40    {
41        $routesToLimit = [
42            'api_auth_registration',
43            'api_auth_me'
44        ];
45        $route = $request->attributes->get('_route');
46        return $route && in_array($route, $routesToLimit, true);
47    }
48
49    public function limitShouldBeApplied(ControllerEvent $event): bool
50    {
51        return $this->isMainRequest($event) && $this->routeShouldBeLimited($event->getRequest());
52    }
53
54    public function onKernelController(ControllerEvent $event): void
55    {
56        if (!$this->limitShouldBeApplied($event)) {
57            return;
58        }
59
60        $request = $event->getRequest();
61        $user = $this->security->getUser();
62
63        $factory = $this->registerByIpLimiter;
64        $key = $request->getClientIp();
65        if ($user) {
66            $factory = $this->apiByUserLimiter;
67            $key = $user->getUserIdentifier();
68        }
69
70        $limiter = $factory->create($key);
71        if (!$limiter->consume()->isAccepted()) {
72            throw new TooManyRequestsHttpException();
73        }
74    }
75
76    public static function getSubscribedEvents(): array
77    {
78        return [
79            KernelEvents::CONTROLLER => 'onKernelController',
80        ];
81    }
82}