LdapLoginmodule::initialize()   F
last analyzed

Complexity

Conditions 11
Paths 1024

Size

Total Lines 62
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 11
eloc 26
c 4
b 1
f 0
nc 1024
nop 4
dl 0
loc 62
ccs 0
cts 38
cp 0
crap 132
rs 3.15

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * AppserverIo\Appserver\ServletEngine\Security\Auth\Spi\LdapLoginModule.php
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Alexandros Weigl <[email protected]>
15
 * @copyright 2017 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/appserver
18
 * @link      http://www.appserver.io
19
 */
20
21
namespace AppserverIo\Appserver\ServletEngine\Security\Auth\Spi;
22
23
use AppserverIo\Lang\String;
24
use AppserverIo\Lang\Boolean;
25
use AppserverIo\Collections\HashMap;
26
use AppserverIo\Collections\MapInterface;
27
use AppserverIo\Psr\Security\Auth\Subject;
28
use AppserverIo\Psr\Security\Auth\Login\LoginException;
29
use AppserverIo\Psr\Security\Auth\Callback\CallbackHandlerInterface;
30
use AppserverIo\Appserver\Naming\Utils\NamingDirectoryKeys;
31
use AppserverIo\Appserver\ServletEngine\RequestHandler;
32
use AppserverIo\Appserver\ServletEngine\Security\SimpleGroup;
33
use AppserverIo\Appserver\ServletEngine\Security\Utils\Util;
34
use AppserverIo\Appserver\ServletEngine\Security\Utils\ParamKeys;
35
use AppserverIo\Appserver\ServletEngine\Security\Utils\SharedStateKeys;
36
37
/**
38
 * This class provides LDAP login functionality to an openldap server.
39
 *
40
 * @author    Alexandros Weigl <[email protected]>
41
 * @copyright 2017 TechDivision GmbH <[email protected]>
42
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
43
 * @link      https://github.com/appserver-io/appserver
44
 * @link      http://www.appserver.io
45
 */
46
class LdapLoginmodule extends UsernamePasswordLoginModule
47
{
48
49
    /**
50
     * The key for the CN in a DN.
51
     *
52
     * @var string
53
     */
54
    const CN = 'cn';
55
56
    /**
57
     * The LDAP url of the LDAP server
58
     *
59
     * @var string
60
     */
61
    protected $ldapUrl = null;
62
63
    /**
64
     * The LDAP port of the LDAP server
65
     *
66
     * @var string
67
     */
68
    protected $ldapPort = 389;
69
70
    /**
71
     * The LDAP start tls flag. Enables/disables tls requests to the LDAP server
72
     *
73
     * @var boolean
74
     */
75
    protected $ldapStartTls = null;
76
77
    /**
78
     * The LDAP servers base distinguished name
79
     *
80
     * @var string
81
     */
82
    protected $baseDN = null;
83
84
    /**
85
     * The administrator user DN with the permissions to search the LDAP directory.
86
     *
87
     * @var string
88
     */
89
    protected $bindDN = null;
90
91
    /**
92
     * The credential of the administrator user
93
     *
94
     * @var string
95
     */
96
    protected $bindCredential = null;
97
98
    /**
99
     * A search filter used to locate the context of the user to authenticate
100
     * The input username/userDN as obtained from the login module
101
     * callback will be substituted into the filter anywhere a "{0}" expression is seen.
102
     *  A common example search filter is "(uid={0})".
103
     *
104
     * @var string
105
     */
106
    protected $baseFilter = null;
107
108
    /**
109
     * The fixed DN of the context to search for user roles.
110
     *
111
     * @var string
112
     */
113
    protected $rolesDN = null;
114
115
    /**
116
     * A search filter used to locate the roles associated with the authenticated user.
117
     * The input username/userDN as obtained from the login module callback
118
     * will be substituted into the filter anywhere a "{0}" expression is
119
     * seen. The authenticated userDN will be substituted into the filter anywhere a
120
     * "{1}" is seen.  An example search filter that matches on the input username is:
121
     * "(memberUid={0})". An alternative that matches on the authenticated userDN is:
122
     * "(member={1})".
123
     *
124
     * @var string
125
     */
126
    protected $roleFilter = null;
127
128
    /**
129
     * Allow Anonymous Logins to OpenLDAP
130
     *
131
     * @var boolean
132
     */
133
    protected $allowEmptyPasswords = null;
134
135
    /**
136
     * A Hashmap storing the authenticating users roles
137
     *
138
     * @var mixed
139
     */
140
    protected $setsMap = null;
141
142
    /**
143
     * The username of the user
144
     *
145
     * @var string
146
     */
147
    protected $username = null;
148
149
    /**
150
     * The distinguished name of the user
151
     *
152
     * @var string
153
     */
154
    protected $userDN = null;
155
156
    /**
157
     * Initialize the login module. This stores the subject, callbackHandler and sharedState and options
158
     * for the login session. Subclasses should override if they need to process their own options. A call
159
     * to parent::initialize() must be made in the case of an override.
160
     *
161
     * The following parameters can by default be passed from the configuration.
162
     *
163
     * ldapUrl:             The LDAP server to connect
164
     * ldapPort:            The port which the LDAP server is running on
165
     * baseDN:              The LDAP servers base distinguished name
166
     * bindDN:              The administrator user DN with the permissions to search the LDAP directory.
167
     * bindCredential:      The credential of the administrator user
168
     * baseFilter:          A search filter used to locate the context of the user to authenticate
169
     * rolesDN:             The fixed DN of the context to search for user roles.
170
     * rolFilter:           A search filter used to locate the roles associated with the authenticated user.
171
     * ldapStartTls:        The LDAP start tls flag. Enables/disables tls requests to the LDAP server
172
     * allowEmptyPasswords: Allow/disallow anonymous Logins to OpenLDAP
173
     *
174
     *
175
     * @param \AppserverIo\Psr\Security\Auth\Subject                           $subject         The Subject to update after a successful login
176
     * @param \AppserverIo\Psr\Security\Auth\Callback\CallbackHandlerInterface $callbackHandler The callback handler that will be used to obtain the user identity and credentials
177
     * @param \AppserverIo\Collections\MapInterface                            $sharedState     A map shared between all configured login module instances
178
     * @param \AppserverIo\Collections\MapInterface                            $params          The parameters passed to the login module
179
     *
180
     * @return void
181
     */
182
    public function initialize(Subject $subject, CallbackHandlerInterface $callbackHandler, MapInterface $sharedState, MapInterface $params)
183
    {
184
185
        // call the parent method
186
        parent::initialize($subject, $callbackHandler, $sharedState, $params);
187
188
        // initialize the hash encoding to use
189
        if ($params->exists(ParamKeys::URL)) {
190
            $this->ldapUrl = $params->get(ParamKeys::URL);
191
        }
192
193
        // initialize the port of the LDAP server
194
        if ($params->exists(ParamKeys::PORT)) {
195
            $this->ldapPort = $params->get(ParamKeys::PORT);
196
        }
197
198
        // initialize the DN for th users
199
        if ($params->exists(ParamKeys::BASE_DN)) {
200
            $this->baseDN = $params->get(ParamKeys::BASE_DN);
201
        }
202
203
        // initialize the bind DN
204
        if ($params->exists(ParamKeys::BIND_DN)) {
205
            $this->bindDN= $params->get(ParamKeys::BIND_DN);
206
        }
207
208
        // initialize the credentials for the LDAP bind
209
        if ($params->exists(ParamKeys::BIND_CREDENTIAL)) {
210
            $this->bindCredential = $params->get(ParamKeys::BIND_CREDENTIAL);
211
        }
212
213
        // initialize the filter for the users
214
        if ($params->exists(ParamKeys::BASE_FILTER)) {
215
            $this->baseFilter = $params->get(ParamKeys::BASE_FILTER);
216
        }
217
218
        // initialize the DN for the roles
219
        if ($params->exists(ParamKeys::ROLES_DN)) {
220
            $this->rolesDN = $params->get(ParamKeys::ROLES_DN);
221
        }
222
223
        // initialize the filter for the roles
224
        if ($params->exists(ParamKeys::ROLE_FILTER)) {
225
            $this->roleFilter = $params->get(ParamKeys::ROLE_FILTER);
226
        }
227
228
        // initialize the flag to use START TLS or not
229
        if ($params->exists(ParamKeys::START_TLS)) {
230
            $this->ldapStartTls = Boolean::valueOf(
0 ignored issues
show
Documentation Bug introduced by
It seems like AppserverIo\Lang\Boolean..._TLS)))->booleanValue() of type boolean is incompatible with the declared type AppserverIo\Lang\Boolean of property $ldapStartTls.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
231
                new String($params->get(ParamKeys::START_TLS))
232
            )->booleanValue();
233
        }
234
235
        // initialize the flag to allow empty passwords or not
236
        if ($params->exists(ParamKeys::ALLOW_EMPTY_PASSWORDS)) {
237
            $this->allowEmptyPasswords = Boolean::valueOf(
0 ignored issues
show
Documentation Bug introduced by
It seems like AppserverIo\Lang\Boolean...ORDS)))->booleanValue() of type boolean is incompatible with the declared type AppserverIo\Lang\Boolean of property $allowEmptyPasswords.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
238
                new String($params->get(ParamKeys::ALLOW_EMPTY_PASSWORDS))
239
            )->booleanValue();
240
        }
241
242
        // initialialize the hash map for the roles
243
        $this->setsMap = new HashMap();
244
    }
245
246
    /**
247
     * Perform the authentication of username and password through LDAP.
248
     *
249
     * @return boolean TRUE when login has been successfull, else FALSE
250
     * @throws \AppserverIo\Psr\Security\Auth\Login\LoginException Is thrown if an error during login occured
251
     */
252
    public function login()
253
    {
254
255
        // initialize the flag for the successfull login
256
        $this->loginOk = false;
257
258
        // array containing the username and password from the user's input
259
        list ($this->username, $password) = $this->getUsernameAndPassword();
260
261
        // query whether or not password AND username are set
262
        if ($this->username === null && $password === null) {
263
            $this->identity = $this->unauthenticatedIdentity;
264
        }
265
266
        // try to create a identity based on the given username
267
        if ($this->identity === null) {
268
            try {
269
                $this->identity = $this->createIdentity($this->username);
270
            } catch (\Exception $e) {
271
                throw new LoginException(
272
                    sprintf('Failed to create principal: %s', $e->getMessage())
273
                );
274
            }
275
        }
276
277
        // connect to the LDAP server
278
        $ldapConnection = $this->ldapConnect();
279
280
        // replace the placeholder  with the actual username of the user
281
        $this->baseFilter = preg_replace(
282
            '/\{0\}/',
283
            ldap_escape($this->username, '', LDAP_ESCAPE_FILTER),
284
            $this->baseFilter
285
        );
286
287
        // try to load the user from the LDAP server
288
        $search = ldap_search($ldapConnection, $this->baseDN, $this->baseFilter);
289
        $entry = ldap_first_entry($ldapConnection, $search);
290
        $this->userDN = ldap_get_dn($ldapConnection, $entry);
0 ignored issues
show
Documentation Bug introduced by
It seems like ldap_get_dn($ldapConnection, $entry) of type string is incompatible with the declared type AppserverIo\Lang\String of property $userDN.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
291
292
        // query whether or not the user is available
293
        if (!isset($this->userDN)) {
294
            throw new LoginException('User not found in LDAP directory');
295
        }
296
297
        // bind the authenticating user to the LDAP directory
298
        if ((ldap_bind($ldapConnection, $this->userDN, $password)) === false) {
299
            throw new LoginException('Username or password wrong');
300
        }
301
302
        // query whether or not password stacking has been activated
303
        if ($this->getUseFirstPass()) {
304
            // add the username and password to the shared state map
305
            $this->sharedState->add(SharedStateKeys::LOGIN_NAME, $this->username);
0 ignored issues
show
Bug introduced by
The method add() does not exist on AppserverIo\Collections\MapInterface. It seems like you code against a sub-type of said class. However, the method does not exist in AppserverIo\Properties\PropertiesInterface or AppserverIo\Collections\AbstractMap or AppserverIo\Collections\SortedMapInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

305
            $this->sharedState->/** @scrutinizer ignore-call */ 
306
                                add(SharedStateKeys::LOGIN_NAME, $this->username);
Loading history...
306
            $this->sharedState->add(SharedStateKeys::LOGIN_PASSWORD, $this->credential);
307
        }
308
309
        // set the login flag to TRUE and return
310
        return $this->loginOk = true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->loginOk = true returns the type true which is incompatible with the documented return type AppserverIo\Lang\Boolean.
Loading history...
311
    }
312
313
    /**
314
     * Returns the password for the user from the sharedMap data.
315
     *
316
     * @return void
317
     */
318
    public function getUsersPassword()
319
    {
320
        return null;
321
    }
322
323
    /**
324
     * Overridden by subclasses to return the Groups that correspond to the to the
325
     * role sets assigned to the user. Subclasses should create at least a Group
326
     * named "Roles" that contains the roles assigned to the user.
327
     *
328
     * @return array Array containing the sets of roles
329
     * @throws \AppserverIo\Psr\Security\Auth\Login\LoginException Is thrown if password can't be loaded
330
     */
331
    protected function getRoleSets()
332
    {
333
        // search and add the roles of the current user
334
        $this->rolesSearch($this->username, $this->userDN);
335
        return $this->setsMap->toArray();
336
    }
337
338
    /**
339
     * Adds a role to the hash map with the roles.
340
     *
341
     * @param string $groupName The name of the group
342
     * @param string $name      The name of the role to be added to the group
343
     *
344
     * @return void
345
     */
346
    protected function addRole($groupName, $name)
347
    {
348
349
        // load the application
350
        $application = RequestHandler::getApplicationContext();
351
352
        // query whether or not, the group already exists
353
        if ($this->setsMap->exists($groupName) === false) {
354
            $group = new SimpleGroup(new String($groupName));
355
            $this->setsMap->add($groupName, $group);
356
        } else {
357
            $group = $this->setsMap->get($groupName);
358
        }
359
360
        try {
361
            // finally add the identity to the group
362
            $group->addMember($this->createIdentity(new String($name)));
363
364
        } catch (\Exception $e) {
365
            $application->getNamingDirectory()
0 ignored issues
show
Bug introduced by
The method getNamingDirectory() does not exist on AppserverIo\Psr\Application\ApplicationInterface. It seems like you code against a sub-type of AppserverIo\Psr\Application\ApplicationInterface such as AppserverIo\Appserver\Application\Application. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

365
            $application->/** @scrutinizer ignore-call */ 
366
                          getNamingDirectory()
Loading history...
366
                        ->search(NamingDirectoryKeys::SYSTEM_LOGGER)
367
                        ->error($e->__toString());
368
        }
369
    }
370
371
    /**
372
     * Extracts the common name from a distinguished name.
373
     *
374
     * @param string $dn The distinguished name of the authenticating user
375
     *
376
     * @return array
377
     */
378
    protected function extractCNFromDN($dn)
379
    {
380
381
        // explode the DN
382
        $splitArray = explode(',', $dn);
383
        $keyValue = array();
384
385
        // iterate over all elements
386
        foreach ($splitArray as $value) {
387
            $tempArray  = explode('=', $value);
388
            $keyValue[$tempArray[0]] = array();
389
            $keyValue[$tempArray[0]][] = $tempArray[1];
390
        }
391
392
        // return the CN
393
        return $keyValue[LdapLoginmodule::CN];
394
    }
395
396
    /**
397
    * Return's the authenticated user identity.
398
    *
399
    * @return \AppserverIo\Psr\Security\PrincipalInterface the user identity
400
    */
401
    protected function getIdentity()
402
    {
403
        return $this->identity;
404
    }
405
406
    /**
407
     * Creates a new connection to the ldap server, binds to the LDAP server and returns the connection
408
     *
409
     * @return resource The LDAP connection
410
     * @throws \AppserverIo\Psr\Security\Auth\Login\LoginException Is thrown if the connection or the bind to the LDAP server failed
411
     */
412
    protected function ldapConnect()
413
    {
414
415
        // try to connect to the LDAP server
416
        if ($ldapConnection = ldap_connect($this->ldapUrl, $this->ldapPort)) {
0 ignored issues
show
Bug introduced by
$this->ldapPort of type AppserverIo\Lang\String is incompatible with the type integer expected by parameter $port of ldap_connect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

416
        if ($ldapConnection = ldap_connect($this->ldapUrl, /** @scrutinizer ignore-type */ $this->ldapPort)) {
Loading history...
417
            // query whether or no we want to use START TLS
418
            if ($this->ldapStartTls) {
419
                ldap_start_tls($ldapConnection);
420
            }
421
422
            // set the LDAP protocol version
423
            ldap_set_option($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
424
425
            // query whether or not we want a anonymous login
426
            if ($this->allowEmptyPasswords) {
427
                $bind = ldap_bind($ldapConnection);
428
            } else {
429
                $bind = ldap_bind($ldapConnection, $this->bindDN, $this->bindCredential);
430
            }
431
432
            // query whether or not the bind succeeded
433
            if ($bind) {
434
                return $ldapConnection;
435
            }
436
437
            // throw an exception if we can't bind using the DN
438
            throw new LoginException(
439
                sprintf(
440
                    'Can\'t bind %s on LDAP server %s:%d',
441
                    $this->bindDN,
442
                    $this->ldapUrl,
443
                    $this->ldapPort
444
                )
445
            );
446
        }
447
448
        // throw an exception if we can't connect
449
        throw new LoginException(
450
            sprintf(
451
                'Can\'t connect to the LDAP server %s:%d',
452
                $this->ldapUrl,
453
                $this->ldapPort
454
            )
455
        );
456
    }
457
458
    /**
459
     * Search the authenticated user for his user groups/roles and add
460
     * their roles to the hash map with the roles.
461
     *
462
     * @param string $user   the authenticated user
463
     * @param string $userDN the DN of the authenticated user
464
     *
465
     * @return void
466
     */
467
    protected function rolesSearch($user, $userDN)
468
    {
469
470
        // query whether or not roles DN or filter has been set
471
        if ($this->rolesDN === null || $this->roleFilter === null) {
472
            return;
473
        }
474
475
        // load the default group name and the LDAP connection
476
        $groupName = Util::DEFAULT_GROUP_NAME;
477
        $ldapConnection = $this->ldapConnect();
478
479
        // replace the {0} placeholder with the username of the user
480
        $this->roleFilter = preg_replace(
481
            "/\{0\}/",
482
            ldap_escape($user, '', LDAP_ESCAPE_FILTER),
483
            $this->roleFilter
484
        );
485
486
        // replace the {1} placeholder with the distiniguished name of the user
487
        $this->roleFilter = preg_replace("/\{1\}/", "$userDN", $this->roleFilter);
488
489
        // search for the roles using the roleFilter and get the first entry
490
        $search = ldap_search($ldapConnection, $this->rolesDN, $this->roleFilter);
491
        $entry = ldap_first_entry($ldapConnection, $search);
492
493
        do {
494
            // get the distinguished name of the entry and extract the common names out of it
495
            $dn = ldap_get_dn($ldapConnection, $entry);
496
            $roleArray = $this->extractCNFromDN($dn);
0 ignored issues
show
Bug introduced by
$dn of type string is incompatible with the type AppserverIo\Lang\String expected by parameter $dn of AppserverIo\Appserver\Se...dule::extractCNFromDN(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

496
            $roleArray = $this->extractCNFromDN(/** @scrutinizer ignore-type */ $dn);
Loading history...
497
498
            // add every returned CN to the roles
499
            foreach ($roleArray as $role) {
500
                $this->addRole($groupName, $role);
0 ignored issues
show
Bug introduced by
$groupName of type string is incompatible with the type AppserverIo\Lang\String expected by parameter $groupName of AppserverIo\Appserver\Se...pLoginmodule::addRole(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

500
                $this->addRole(/** @scrutinizer ignore-type */ $groupName, $role);
Loading history...
501
            }
502
503
            // continue as long as there are entries still left from the search
504
        } while ($entry = ldap_next_entry($ldapConnection, $entry));
505
    }
506
}
507