Completed
Push — master ( 577d0b...396fc4 )
by Chad
02:10
created

setLdapCredentialsIfNeeded()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 8
nc 3
nop 2
1
<?php
2
/**
3
 * This file is part of the LdapToolsBundle package.
4
 *
5
 * (c) Chad Sikorra <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LdapTools\Bundle\LdapToolsBundle\Security;
12
13
use LdapTools\Bundle\LdapToolsBundle\Event\AuthenticationHandlerEvent;
14
use LdapTools\Bundle\LdapToolsBundle\Event\LdapLoginEvent;
15
use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserChecker;
16
use LdapTools\Bundle\LdapToolsBundle\Security\User\LdapUserProvider;
17
use LdapTools\Exception\Exception;
18
use LdapTools\Exception\LdapConnectionException;
19
use LdapTools\Operation\AuthenticationOperation;
20
use LdapTools\LdapManager;
21
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
24
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
25
use Symfony\Component\Security\Core\Exception\AuthenticationException;
26
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
27
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
28
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
29
use Symfony\Component\Security\Core\Security;
30
use Symfony\Component\Security\Core\User\UserInterface;
31
use Symfony\Component\Security\Core\User\UserProviderInterface;
32
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
33
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
34
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
35
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
36
37
/**
38
 * LDAP Guard Authenticator.
39
 *
40
 * @author Chad Sikorra <[email protected]>
41
 */
42
class LdapGuardAuthenticator extends AbstractGuardAuthenticator
43
{
44
    /**
45
     * @var LdapManager
46
     */
47
    protected $ldap;
48
49
    /**
50
     * @var LdapUserChecker
51
     */
52
    protected $userChecker;
53
54
    /**
55
     * @var string
56
     */
57
    protected $domain;
58
59
    /**
60
     * @var EventDispatcherInterface
61
     */
62
    protected $dispatcher;
63
64
    /**
65
     * @var AuthenticationSuccessHandlerInterface
66
     */
67
    protected $successHandler;
68
69
    /**
70
     * @var AuthenticationFailureHandlerInterface
71
     */
72
    protected $failureHandler;
73
74
    /**
75
     * @var AuthenticationEntryPointInterface
76
     */
77
    protected $entryPoint;
78
79
    /**
80
     * @var array
81
     */
82
    protected $options = [
83
        'hide_user_not_found_exceptions' => true,
84
        'username_parameter' => '_username',
85
        'password_parameter' => '_password',
86
        'domain_parameter' => '_ldap_domain',
87
        'post_only' => false,
88
        'remember_me' => false,
89
    ];
90
91
    /**
92
     * @param bool $hideUserNotFoundExceptions
93
     * @param LdapUserChecker $userChecker
94
     * @param LdapManager $ldap
95
     * @param AuthenticationEntryPointInterface $entryPoint
96
     * @param EventDispatcherInterface $dispatcher
97
     * @param AuthenticationSuccessHandlerInterface $successHandler
98
     * @param AuthenticationFailureHandlerInterface $failureHandler
99
     * @param array $options
100
     */
101
    public function __construct($hideUserNotFoundExceptions = true, LdapUserChecker $userChecker, LdapManager $ldap, AuthenticationEntryPointInterface $entryPoint, EventDispatcherInterface $dispatcher, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options)
102
    {
103
        $this->userChecker = $userChecker;
104
        $this->ldap = $ldap;
105
        $this->entryPoint = $entryPoint;
106
        $this->dispatcher = $dispatcher;
107
        $this->successHandler = $successHandler;
108
        $this->failureHandler = $failureHandler;
109
        $this->options['hide_user_not_found_exceptions'] = $hideUserNotFoundExceptions;
110
        $this->options = array_merge($this->options, $options);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function getCredentials(Request $request)
117
    {
118
        $credentials = [
119
            'username' => $this->getRequestParameter($this->options['username_parameter'], $request),
120
            'password' => $this->getRequestParameter($this->options['password_parameter'], $request),
121
            'ldap_domain' => $this->getRequestParameter($this->options['domain_parameter'], $request),
122
        ];
123
        if (empty($credentials['username'])) {
124
            return null;
125
        }
126
        $request->getSession()->set(Security::LAST_USERNAME, $credentials['username']);
127
128
        return $credentials;
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function getUser($credentials, UserProviderInterface $userProvider)
135
    {
136
        $domain = $this->ldap->getDomainContext();
137
138
        try {
139
            $this->switchDomainIfNeeded($credentials);
140
            $this->setLdapCredentialsIfNeeded($credentials, $userProvider);
141
            $user = $userProvider->loadUserByUsername($credentials['username']);
142
            $this->userChecker->checkPreAuth($user);
143
144
            return $user;
145
        } catch (UsernameNotFoundException $e) {
146
            $this->hideOrThrow($e);
147
        } catch (BadCredentialsException $e) {
148
            $this->hideOrThrow($e);
149
        } catch (LdapConnectionException $e) {
150
            $this->hideOrThrow($e);
151
        } catch (\Exception $e) {
152
            $this->hideOrThrow($e);
153
        } finally {
154
            $this->switchDomainBackIfNeeded($domain);
155
        }
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function checkCredentials($credentials, UserInterface $user)
162
    {
163
        $domain = $this->ldap->getDomainContext();
164
165
        try {
166
            $this->switchDomainIfNeeded($credentials);
167
            /** @var \LdapTools\Operation\AuthenticationResponse $response */
168
            $response = $this->ldap->getConnection()->execute(
169
                new AuthenticationOperation($user->getUsername(), $credentials['password'])
170
            );
171 View Code Duplication
            if (!$response->isAuthenticated()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
172
                $this->userChecker->checkLdapErrorCode(
173
                    $user,
174
                    $response->getErrorCode(),
175
                    $this->ldap->getConnection()->getConfig()->getLdapType()
176
                );
177
                throw new CustomUserMessageAuthenticationException(
178
                    $response->getErrorMessage(), [], $response->getErrorCode()
179
                );
180
            }
181
            // No way to get the token from the Guard, need to create one to pass...
182
            $token = new UsernamePasswordToken($user, $credentials['password'], 'ldap-tools', $user->getRoles());
0 ignored issues
show
Documentation introduced by
$user->getRoles() is of type array<integer,object<Sym...Core\Role\Role>|string>, but the function expects a array<integer,object<Sym...\RoleInterface>|string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
183
            $token->setAttribute('ldap_domain', isset($credentials['ldap_domain']) ? $credentials['ldap_domain'] : '');
184
            $this->dispatcher->dispatch(
185
                LdapLoginEvent::SUCCESS,
186
                new LdapLoginEvent($user, $token)
187
            );
188
        } catch (\Exception $e) {
189
            $this->hideOrThrow($e);
190
        } finally {
191
            $this->domain = $this->ldap->getDomainContext();
192
            $this->switchDomainBackIfNeeded($domain);
193
        }
194
195
        return true;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 View Code Duplication
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
    {
203
        $event = new AuthenticationHandlerEvent(
204
            $this->successHandler->onAuthenticationSuccess($request, $token),
205
            $request,
206
            null,
207
            $token,
208
            $providerKey
209
        );
210
        $this->dispatcher->dispatch(AuthenticationHandlerEvent::SUCCESS, $event);
211
212
        return $event->getResponse();
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 View Code Duplication
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
    {
220
        $event = new AuthenticationHandlerEvent(
221
            $this->failureHandler->onAuthenticationFailure($request, $exception),
222
            $request,
223
            $exception
224
        );
225
        $this->dispatcher->dispatch(AuthenticationHandlerEvent::FAILURE, $event);
226
227
        return $event->getResponse();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 View Code Duplication
    public function start(Request $request, AuthenticationException $authException = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234
    {
235
        $event = new AuthenticationHandlerEvent(
236
            $this->entryPoint->start($request, $authException),
237
            $request,
238
            $authException
239
        );
240
        $this->dispatcher->dispatch(AuthenticationHandlerEvent::START, $event);
241
242
        return $event->getResponse();
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248
    public function supportsRememberMe()
249
    {
250
        return $this->options['remember_me'];
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256
    public function createAuthenticatedToken(UserInterface $user, $providerKey)
257
    {
258
        $token = parent::createAuthenticatedToken($user, $providerKey);
259
        $token->setAttribute('ldap_domain', $this->domain);
260
261
        return $token;
262
    }
263
264
    /**
265
     * @param string $param
266
     * @param Request $request
267
     * @return string|null
268
     */
269
    protected function getRequestParameter($param, Request $request)
270
    {
271
        if ($this->options['post_only']) {
272
            $value = $request->request->get($param);
273
        } else {
274
            $value = $request->request->get($param) ?: $request->get($param);
275
        }
276
277
        return $value;
278
    }
279
280
    /**
281
     * If no LDAP credentials are in the config then attempt to use the user supplied credentials from the login. But
282
     * only if we are using the LdapUserProvider.
283
     *
284
     * @param array $credentials
285
     * @param UserProviderInterface $userProvider
286
     */
287
    protected function setLdapCredentialsIfNeeded(array $credentials, UserProviderInterface $userProvider)
288
    {
289
        // Only care about this in the context of the LDAP user provider...
290
        if (!$userProvider instanceof LdapUserProvider) {
291
            return;
292
        }
293
294
        // Only if the username/password are not defined in the config....
295
        $config = $this->ldap->getConnection()->getConfig();
296
        if (!(empty($config->getUsername()) && (empty($config->getPassword() && $config->getPassword() !== '0')))) {
297
            return;
298
        }
299
300
        $config->setUsername($credentials['username']);
301
        $config->setPassword($credentials['password']);
302
    }
303
304
    /**
305
     * If the domain needs to a different context for the request, then switch it.
306
     *
307
     * @param array $credentials
308
     */
309
    protected function switchDomainIfNeeded(array $credentials)
310
    {
311
        if (!empty($credentials['ldap_domain']) && $this->ldap->getDomainContext() !== $credentials['ldap_domain']) {
312
            $this->ldap->switchDomain($credentials['ldap_domain']);
313
        }
314
    }
315
316
    /**
317
     * If the passed domain is not the current context, then switch back to it.
318
     *
319
     * @param string $domain
320
     */
321
    protected function switchDomainBackIfNeeded($domain)
322
    {
323
        if ($domain !== $this->ldap->getDomainContext()) {
324
            $this->ldap->switchDomain($domain);
325
        }
326
    }
327
328
    /**
329
     * Determine whether or not the exception should be masked with a BadCredentials or not.
330
     *
331
     * @param \Exception $e
332
     * @throws \Exception
333
     */
334
    protected function hideOrThrow(\Exception $e)
335
    {
336
        if ($this->options['hide_user_not_found_exceptions']) {
337
            throw new BadCredentialsException('Bad credentials.', 0, $e);
338
        }
339
        // Specifically show LdapTools related exceptions, ignore others.
340
        if (!$this->options['hide_user_not_found_exceptions'] && $e instanceof Exception) {
341
            throw new CustomUserMessageAuthenticationException($e->getMessage(), [], $e->getCode());
342
        }
343
344
        throw $e;
345
    }
346
}
347