Completed
Push — master ( 5b6d3c...e19d53 )
by Marcel
03:17
created

UserAuthenticator   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 181
Duplicated Lines 0 %

Test Coverage

Coverage 46.51%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 94
c 3
b 0
f 0
dl 0
loc 181
ccs 40
cts 86
cp 0.4651
rs 10
wmc 24

9 Methods

Rating   Name   Duplication   Size   Complexity  
A supports() 0 3 2
A transformResponse() 0 11 1
A __construct() 0 15 1
A getLoginUrl() 0 2 1
B authenticateUsingActiveDirectory() 0 54 8
A getCredentials() 0 13 1
A getUser() 0 22 5
A checkCredentials() 0 3 3
A onAuthenticationSuccess() 0 6 2
1
<?php
2
3
namespace App\Security;
4
5
use AdAuth\AdAuthInterface;
6
use AdAuth\Credentials;
7
use AdAuth\Response\AuthenticationResponse;
8
use AdAuth\SocketException;
9
use App\Entity\ActiveDirectoryUpnSuffix;
10
use App\Entity\ActiveDirectoryUser;
11
use App\Entity\User;
12
use App\Repository\ActiveDirectoryUpnSuffixRepositoryInterface;
13
use App\Repository\UserRepositoryInterface;
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\NullLogger;
16
use Symfony\Component\HttpFoundation\RedirectResponse;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\Routing\RouterInterface;
19
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
20
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
21
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
22
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
23
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
24
use Symfony\Component\Security\Core\Security;
25
use Symfony\Component\Security\Core\User\UserInterface;
26
use Symfony\Component\Security\Core\User\UserProviderInterface;
27
use Symfony\Component\Security\Csrf\CsrfToken;
28
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
29
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
30
use Symfony\Component\Security\Http\Util\TargetPathTrait;
31
32
class UserAuthenticator extends AbstractFormLoginAuthenticator {
33
34
    use TargetPathTrait;
35
36
    private $isActiveDirectoryEnabled;
37
    private $encoder;
38
    private $logger;
39
    private $adAuth;
40
    private $userCreator;
41
    private $userRepository;
42
    private $upnSuffixRepository;
43
44
    private $loginRoute;
45
    private $checkRoute;
46
    private $router;
47
    private $csrfTokenManager;
48
49 4
    public function __construct($isActiveDirectoryEnabled, $loginRoute, $checkRoute, UserPasswordEncoderInterface $encoder, UserRepositoryInterface $userRepository,
50
                                AdAuthInterface $adAuth, UserCreator $userCreator, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager,
51
                                ActiveDirectoryUpnSuffixRepositoryInterface $upnSuffixRepository, LoggerInterface $logger = null) {
52 4
        $this->isActiveDirectoryEnabled = $isActiveDirectoryEnabled;
53 4
        $this->encoder = $encoder;
54 4
        $this->userRepository = $userRepository;
55 4
        $this->adAuth = $adAuth;
56 4
        $this->userCreator = $userCreator;
57 4
        $this->loginRoute = $loginRoute;
58 4
        $this->checkRoute = $checkRoute;
59 4
        $this->router = $router;
60 4
        $this->csrfTokenManager = $csrfTokenManager;
61 4
        $this->upnSuffixRepository = $upnSuffixRepository;
62
63 4
        $this->logger = $logger ?? new NullLogger();
64 4
    }
65
66
    /**
67
     * @inheritDoc
68
     */
69 3
    protected function getLoginUrl() {
70 3
        return $this->router->generate($this->loginRoute);
71
    }
72
73
    /**
74
     * @inheritDoc
75
     */
76 3
    public function supports(Request $request) {
77 3
        return $request->attributes->get('_route') === $this->checkRoute
78 3
            && $request->isMethod('POST');
79
    }
80
81
    /**
82
     * @inheritDoc
83
     */
84 3
    public function getCredentials(Request $request) {
85
        $credentials = [
86 3
            'username' => $request->request->get('_username'),
87 3
            'password' => $request->request->get('_password'),
88 3
            'csrf_token' => $request->request->get('_csrf_token')
89
        ];
90
91 3
        $request->getSession()->set(
92 3
            Security::LAST_USERNAME,
93 3
            $credentials['username']
94
        );
95
96 3
        return $credentials;
97
    }
98
99
    /**
100
     * @inheritDoc
101
     */
102 3
    public function getUser($credentials, UserProviderInterface $userProvider) {
103 3
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
104
105 3
        if(!$this->csrfTokenManager->isTokenValid($token)) {
106
            throw new InvalidCsrfTokenException();
107
        }
108
109
        try {
110 3
            $user = $userProvider->loadUserByUsername($credentials['username']);
111
112 3
            if($user instanceof ActiveDirectoryUser) {
113 3
                $user = $this->authenticateUsingActiveDirectory(new Credentials($credentials['username'], $credentials['password']), $user);
114
            }
115
        } catch(UsernameNotFoundException $e) {
116
            $user = $this->authenticateUsingActiveDirectory(new Credentials($credentials['username'], $credentials['password']));
117
        }
118
119 3
        if($user === null) {
120
            throw new CustomUserMessageAuthenticationException('invalid_credentials');
121
        }
122
123 3
        return $user;
124
    }
125
126
    /**
127
     * @inheritDoc
128
     */
129 3
    public function checkCredentials($credentials, UserInterface $user) {
130 3
        return $this->encoder->isPasswordValid($user, $credentials['password'])
131 3
            && ((!$user instanceof User) || $user->isDeleted() === false);          // Important: check if user was deleted
132
    }
133
134
    /**
135
     * @inheritDoc
136
     */
137 3
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) {
138 3
        if($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
139 3
            return new RedirectResponse($targetPath);
140
        }
141
142
        return new RedirectResponse('/');
143
    }
144
145
    protected function authenticateUsingActiveDirectory(Credentials $credentials, ActiveDirectoryUser $adUser = null) {
146
        if($this->isActiveDirectoryEnabled !== true) {
147
            return $adUser;
148
        }
149
150
        $upnSuffix = substr($credentials->getUsername(), strpos($credentials->getUsername(), '@') + 1);
151
        $upnSuffixes = array_map(function(ActiveDirectoryUpnSuffix $suffix) { return $suffix->getSuffix(); }, $this->upnSuffixRepository->findAll());
152
153
        if(count($upnSuffixes) > 0 && !in_array($upnSuffix, $upnSuffixes)) {
154
            $this->logger->debug(sprintf('UPN-Suffix "%s" is not enabled for AD sync.', $upnSuffix));
155
            return $adUser;
156
        }
157
158
        try {
159
            /** @var AuthenticationResponse $response */
160
            $response = $this->adAuth->authenticate($credentials);
161
162
            if($response->isSuccess() !== true) {
163
                $this->logger
164
                    ->notice(sprintf('Failed to authenticate "%s" using Active Directory', $credentials->getUsername()));
165
166
                // password not valid
167
                return null;
168
            }
169
170
            $info = $this->transformResponse($response);
171
172
            if($this->userCreator->canCreateUser($info)) {
173
                $user = $this->userCreator->createUser($info, $adUser);
174
175
                $user->setPassword($this->encoder->encodePassword($user, $credentials->getPassword()));
176
177
                $this->userRepository->persist($user);
178
179
                return $user;
180
            } else {
181
                $this->logger
182
                    ->notice(sprintf('User "%s" tried to authenticate but this user cannot be created from active directory', $credentials->getUsername()));
183
184
                throw new CustomUserMessageAuthenticationException('not_allowed');
185
            }
186
        } catch(SocketException $e) {
187
            $this->logger
188
                ->critical('Authentication server is not available');
189
190
            throw new CustomUserMessageAuthenticationException('server_unavailable');
191
        } catch(\Exception $e) {
192
            $this->logger->critical(
193
                'Authentication failed', [
194
                    'exception' => $e
195
                ]
196
            );
197
198
            throw new CustomUserMessageAuthenticationException('unknown_error');
199
        }
200
    }
201
202
    private function transformResponse(AuthenticationResponse $response): ActiveDirectoryUserInformation {
203
        return (new ActiveDirectoryUserInformation())
204
            ->setUsername($response->getUsername())
205
            ->setUserPrincipalName($response->getUserPrincipalName())
206
            ->setFirstname($response->getFirstname())
207
            ->setLastname($response->getLastname())
208
            ->setEmail($response->getEmail())
209
            ->setGuid($response->getGuid())
210
            ->setUniqueId($response->getUniqueId())
211
            ->setOu($response->getOu())
212
            ->setGroups($response->getGroups());
213
    }
214
}