Passed
Push — master ( ca52c8...cf0bfa )
by Jan
12:46
created

EnforceTFAOrPasswordChangeSubscriber   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 104
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 15
eloc 44
c 1
b 0
f 0
dl 0
loc 104
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
A getSubscribedEvents() 0 4 1
B redirectToSettingsIfNeeded() 0 37 8
A __construct() 0 8 1
A checkIfAllowedPath() 0 18 5
1
<?php
2
/*
3
 * Copyright (C)  2020-2022  Jan Böhmer
4
 *
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published
7
 * by the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace App\EventSubscriber\UserSystem;
20
21
use App\Entity\User;
22
use App\Services\UserSystem\EnforceTFARedirectHelper;
23
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
24
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\Session\Session;
27
use Symfony\Component\HttpFoundation\Session\SessionInterface;
28
use Symfony\Component\HttpKernel\Event\RequestEvent;
29
use Symfony\Component\HttpKernel\KernelEvents;
30
use Symfony\Component\Security\Core\Security;
31
use Symfony\Component\Security\Http\HttpUtils;
32
33
class EnforceTFAOrPasswordChangeSubscriber implements EventSubscriberInterface
34
{
35
    /**
36
     * @var string[] The routes the user is allowed to access without being redirected.
37
     *               This should be only routes related to login/logout and user settings
38
     */
39
    public const ALLOWED_ROUTES = [
40
        'user_settings',
41
        'app_login',
42
        '2fa_login',
43
        '2fa_login_check',
44
        'logout',
45
    ];
46
47
    /**
48
     * @var string The route the user will redirected to, if he needs to change this password
49
     */
50
    public const REDIRECT_TARGET = 'user_settings';
51
    private $security;
52
    private $flashBag;
53
    private $httpUtils;
54
    private $TFARedirectHelper;
55
    private $adminUrlGenerator;
56
57
    public function __construct(Security $security, SessionInterface $session, HttpUtils $httpUtils, EnforceTFARedirectHelper $TFARedirectHelper, AdminUrlGenerator $adminUrlGenerator)
58
    {
59
        /** @var Session $session */
60
        $this->security = $security;
61
        $this->flashBag = $session->getFlashBag();
62
        $this->httpUtils = $httpUtils;
63
        $this->TFARedirectHelper = $TFARedirectHelper;
64
        $this->adminUrlGenerator = $adminUrlGenerator;
65
    }
66
67
    /**
68
     * This function is called when the kernel encounters a request.
69
     * It checks if the user must change its password or add an 2FA mehtod and redirect it to the user settings page,
70
     * if needed.
71
     */
72
    public function redirectToSettingsIfNeeded(RequestEvent $event): void
73
    {
74
        $user = $this->security->getUser();
75
        $request = $event->getRequest();
76
77
        if (!$event->isMainRequest()) {
78
            return;
79
        }
80
81
        //If the user is not an User entity skip this handler
82
        if (!$user instanceof User) {
83
            return;
84
        }
85
86
        $tfa_redirect_needed = $this->TFARedirectHelper->doesUserNeedRedirectForTFAEnforcement($user);
87
88
        //Abort if we dont need to redirect the user.
89
        if (!$user->isPasswordChangeNeeded() && !$tfa_redirect_needed) {
90
            return;
91
        }
92
93
        //Check for a whitelisted URL
94
        if ($this->checkIfAllowedPath($request)) {
95
            return;
96
        }
97
98
        //Show appropriate message to user about the reason he was redirected
99
        if ($user->isPasswordChangeNeeded()) {
100
            $this->flashBag->add('warning', 'user.pw_change_needed.flash');
101
        }
102
103
        if ($tfa_redirect_needed) {
104
            $this->flashBag->add('warning', 'user.2fa_needed.flash');
105
        }
106
107
        $event->setResponse($this->httpUtils->createRedirectResponse($request,
108
            $this->adminUrlGenerator->setRoute(static::REDIRECT_TARGET)->generateUrl()
109
        ));
110
    }
111
112
    public static function getSubscribedEvents(): array
113
    {
114
        return [
115
            KernelEvents::REQUEST => 'redirectToSettingsIfNeeded',
116
        ];
117
    }
118
119
    private function checkIfAllowedPath(Request $request): bool
120
    {
121
        foreach (static::ALLOWED_ROUTES as $route) {
122
            //Check for "normal" (not ea admin routes)
123
            if ( $this->httpUtils->checkRequestPath($request, $route)) {
124
                return true;
125
            }
126
127
            //Check for routes accessed using admin context
128
            if ($this->httpUtils->checkRequestPath($request, 'admin_dashboard')
129
                && $request->query->get('routeName', '') === $route) {
130
                return true;
131
            }
132
133
134
        }
135
136
        return false;
137
    }
138
}