Completed
Push — master ( eaf36c...f92548 )
by Marcel
03:19
created

UserAuthenticator   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 186
Duplicated Lines 0 %

Test Coverage

Coverage 47.73%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 96
c 3
b 0
f 0
dl 0
loc 186
ccs 42
cts 88
cp 0.4773
rs 10
wmc 25

9 Methods

Rating   Name   Duplication   Size   Complexity  
A supports() 0 3 2
A __construct() 0 15 1
A getLoginUrl() 0 2 1
A getCredentials() 0 13 1
A transformResponse() 0 11 1
A getUser() 0 27 6
A checkCredentials() 0 3 3
B authenticateUsingActiveDirectory() 0 54 8
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 6
    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 6
        $this->isActiveDirectoryEnabled = $isActiveDirectoryEnabled;
53 6
        $this->encoder = $encoder;
54 6
        $this->userRepository = $userRepository;
55 6
        $this->adAuth = $adAuth;
56 6
        $this->userCreator = $userCreator;
57 6
        $this->loginRoute = $loginRoute;
58 6
        $this->checkRoute = $checkRoute;
59 6
        $this->router = $router;
60 6
        $this->csrfTokenManager = $csrfTokenManager;
61 6
        $this->upnSuffixRepository = $upnSuffixRepository;
62
63 6
        $this->logger = $logger ?? new NullLogger();
64 6
    }
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 5
    public function supports(Request $request) {
77 5
        return $request->attributes->get('_route') === $this->checkRoute
78 5
            && $request->isMethod('POST');
79
    }
80
81
    /**
82
     * @inheritDoc
83
     */
84 5
    public function getCredentials(Request $request) {
85
        $credentials = [
86 5
            'username' => $request->request->get('_username'),
87 5
            'password' => $request->request->get('_password'),
88 5
            'csrf_token' => $request->request->get('_csrf_token')
89
        ];
90
91 5
        $request->getSession()->set(
92 5
            Security::LAST_USERNAME,
93 5
            $credentials['username']
94
        );
95
96 5
        return $credentials;
97
    }
98
99
    /**
100
     * @inheritDoc
101
     */
102 5
    public function getUser($credentials, UserProviderInterface $userProvider) {
103 5
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
104
105 5
        if(!$this->csrfTokenManager->isTokenValid($token)) {
106
            throw new InvalidCsrfTokenException();
107
        }
108
109
        try {
110
            /** @var User $user */
111 5
            $user = $userProvider->loadUserByUsername($credentials['username']);
112
113 5
            if($user->isProvisioned() === false) {
114
                throw new CustomUserMessageAuthenticationException('not_provisioned');
115
            }
116
117 5
            if($user instanceof ActiveDirectoryUser) {
118 5
                $user = $this->authenticateUsingActiveDirectory(new Credentials($credentials['username'], $credentials['password']), $user);
119
            }
120
        } catch(UsernameNotFoundException $e) {
121
            $user = $this->authenticateUsingActiveDirectory(new Credentials($credentials['username'], $credentials['password']));
122
        }
123
124 5
        if($user === null) {
125
            throw new CustomUserMessageAuthenticationException('invalid_credentials');
126
        }
127
128 5
        return $user;
129
    }
130
131
    /**
132
     * @inheritDoc
133
     */
134 5
    public function checkCredentials($credentials, UserInterface $user) {
135 5
        return $this->encoder->isPasswordValid($user, $credentials['password'])
136 5
            && ((!$user instanceof User) || $user->isDeleted() === false);          // Important: check if user was deleted
137
    }
138
139
    /**
140
     * @inheritDoc
141
     */
142 5
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) {
143 5
        if($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
144 3
            return new RedirectResponse($targetPath);
145
        }
146
147 2
        return new RedirectResponse('/');
148
    }
149
150
    protected function authenticateUsingActiveDirectory(Credentials $credentials, ActiveDirectoryUser $adUser = null) {
151
        if($this->isActiveDirectoryEnabled !== true) {
152
            return $adUser;
153
        }
154
155
        $upnSuffix = substr($credentials->getUsername(), strpos($credentials->getUsername(), '@') + 1);
156
        $upnSuffixes = array_map(function(ActiveDirectoryUpnSuffix $suffix) { return $suffix->getSuffix(); }, $this->upnSuffixRepository->findAll());
157
158
        if(count($upnSuffixes) > 0 && !in_array($upnSuffix, $upnSuffixes)) {
159
            $this->logger->debug(sprintf('UPN-Suffix "%s" is not enabled for AD sync.', $upnSuffix));
160
            return $adUser;
161
        }
162
163
        try {
164
            /** @var AuthenticationResponse $response */
165
            $response = $this->adAuth->authenticate($credentials);
166
167
            if($response->isSuccess() !== true) {
168
                $this->logger
169
                    ->notice(sprintf('Failed to authenticate "%s" using Active Directory', $credentials->getUsername()));
170
171
                // password not valid
172
                return null;
173
            }
174
175
            $info = $this->transformResponse($response);
176
177
            if($this->userCreator->canCreateUser($info)) {
178
                $user = $this->userCreator->createUser($info, $adUser);
179
180
                $user->setPassword($this->encoder->encodePassword($user, $credentials->getPassword()));
181
182
                $this->userRepository->persist($user);
183
184
                return $user;
185
            } else {
186
                $this->logger
187
                    ->notice(sprintf('User "%s" tried to authenticate but this user cannot be created from active directory', $credentials->getUsername()));
188
189
                throw new CustomUserMessageAuthenticationException('not_allowed');
190
            }
191
        } catch(SocketException $e) {
192
            $this->logger
193
                ->critical('Authentication server is not available');
194
195
            throw new CustomUserMessageAuthenticationException('server_unavailable');
196
        } catch(\Exception $e) {
197
            $this->logger->critical(
198
                'Authentication failed', [
199
                    'exception' => $e
200
                ]
201
            );
202
203
            throw new CustomUserMessageAuthenticationException('unknown_error');
204
        }
205
    }
206
207
    private function transformResponse(AuthenticationResponse $response): ActiveDirectoryUserInformation {
208
        return (new ActiveDirectoryUserInformation())
209
            ->setUsername($response->getUsername())
210
            ->setUserPrincipalName($response->getUserPrincipalName())
211
            ->setFirstname($response->getFirstname())
212
            ->setLastname($response->getLastname())
213
            ->setEmail($response->getEmail())
214
            ->setGuid($response->getGuid())
215
            ->setUniqueId($response->getUniqueId())
216
            ->setOu($response->getOu())
217
            ->setGroups($response->getGroups());
218
    }
219
}