Ldap   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 21
eloc 110
c 6
b 1
f 0
dl 0
loc 257
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
B loginSasl() 0 87 8
A getConnector() 0 3 1
A buildSearchFilter() 0 18 3
A login() 0 3 1
A getAttributes() 0 49 5
A processAttributes() 0 14 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\ldap\Auth\Source;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Configuration;
9
use SimpleSAML\Error;
10
use SimpleSAML\Module\core\Auth\UserPassBase;
11
use SimpleSAML\Module\ldap\Connector\LdapHelpers;
12
use SimpleSAML\Module\ldap\ConnectorFactory;
13
use SimpleSAML\Module\ldap\ConnectorInterface;
14
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
15
use Symfony\Component\Ldap\Entry;
16
17
use function array_keys;
18
use function array_map;
19
use function in_array;
20
use function preg_match;
21
use function str_replace;
22
use function var_export;
23
24
/**
25
 * LDAP authentication source.
26
 *
27
 * See the ldap-entry in config-templates/authsources.php for information about
28
 * configuration of this authentication source.
29
 *
30
 * @package simplesamlphp/simplesamlphp-module-ldap
31
 */
32
33
class Ldap extends UserPassBase
34
{
35
    use LdapHelpers;
36
37
38
    /**
39
     * @var \SimpleSAML\Module\ldap\ConnectorInterface
40
     */
41
    protected ConnectorInterface $connector;
42
43
    /**
44
     * An LDAP configuration object.
45
     */
46
    protected Configuration $ldapConfig;
47
48
49
    /**
50
     * Constructor for this authentication source.
51
     *
52
     * @param array<mixed> $info  Information about this authentication source.
53
     * @param array<mixed> $config  Configuration.
54
     */
55
    public function __construct(array $info, array $config)
56
    {
57
        // Call the parent constructor first, as required by the interface
58
        parent::__construct($info, $config);
59
60
        $this->ldapConfig = Configuration::loadFromArray(
61
            $config,
62
            'authsources[' . var_export($this->authId, true) . ']',
63
        );
64
65
        $this->connector = ConnectorFactory::fromAuthSource($this->authId);
66
    }
67
68
69
    /**
70
     * Attempt to log in using SASL and the given username and password.
71
     *
72
     * @param string $username  The username the user wrote.
73
     * @param string $password  The password the user wrote.
74
     * @param array<mixed> $sasl_args  SASL options
75
     * @return array<mixed> Associative array with the users attributes.
76
     */
77
    protected function loginSasl(
78
        string $username,
79
        #[\SensitiveParameter]
80
        string $password,
81
        array $sasl_args = [],
82
    ): array {
83
        if (preg_match('/^\s*$/', $password)) {
84
            // The empty string is considered an anonymous bind to Symfony
85
            throw new Error\Error('WRONGUSERPASS');
86
        }
87
88
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
89
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
90
91
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
92
        Assert::natural($timeout);
93
94
        $attributes = $this->ldapConfig->getOptionalValue(
95
            'attributes',
96
            // If specifically set to NULL return all attributes, if not set at all return nothing (safe default)
97
            in_array('attributes', $this->ldapConfig->getOptions(), true) ? ['*'] : [],
98
        );
99
100
        $searchBase = $this->ldapConfig->getArray('search.base');
101
102
        $options = [
103
            'scope' => $searchScope,
104
            'timeout' => $timeout,
105
            'filter' => $attributes,
106
        ];
107
108
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
109
        if ($searchEnable === false) {
110
            $dnPattern = $this->ldapConfig->getString('dnpattern');
111
            $dn = str_replace('%username%', $username, $dnPattern);
112
        } else {
113
            $searchUsername = $this->ldapConfig->getOptionalString('search.username', null);
114
            Assert::nullOrNotWhitespaceOnly($searchUsername);
115
116
            $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
117
            Assert::nullOrNotWhitespaceOnly($searchPassword);
118
119
            try {
120
                $this->connector->bind($searchUsername, $searchPassword);
121
            } catch (Error\Error $e) {
122
                throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
123
            }
124
125
            $filter = $this->buildSearchFilter($username);
126
127
            try {
128
                $entry = /** @scrutinizer-ignore-type */$this->connector->search($searchBase, $filter, $options, false);
129
                $dn = $entry->getDn();
130
            } catch (Error\Exception $e) {
131
                throw new Error\Error('WRONGUSERPASS');
132
            }
133
        }
134
135
        /* Verify the credentials */
136
        if (!empty($sasl_args)) {
137
            $this->connector->saslBind(
138
                $dn,
139
                $password,
140
                $sasl_args['mech'],
141
                $sasl_args['realm'],
142
                $sasl_args['authc_id'],
143
                $sasl_args['authz_id'],
144
                $sasl_args['props'],
145
            );
146
            $dn = $this->connector->whoami();
147
        } else {
148
            $this->connector->bind($dn, $password);
149
        }
150
151
        /* If the credentials were correct, rebind using a privileged account to read attributes */
152
        $readUsername = $this->ldapConfig->getOptionalString('priv.username', null);
153
        $readPassword = $this->ldapConfig->getOptionalString('priv.password', null);
154
        if ($readUsername !== null) {
155
            $this->connector->bind($readUsername, $readPassword);
156
        }
157
158
        $options['scope'] = Query::SCOPE_BASE;
159
        $filter = '(objectClass=*)';
160
161
        $entry = $this->connector->search([$dn], $filter, $options, false);
162
163
        return $this->processAttributes(/** @scrutinizer-ignore-type */$entry);
164
    }
165
166
167
    /**
168
     * Attempt to log in using the given username and password.
169
     *
170
     * @param string $username  The username the user wrote.
171
     * @param string $password  The password the user wrote.
172
     * @return array<mixed> Associative array with the users attributes.
173
     */
174
    protected function login(string $username, #[\SensitiveParameter]string $password): array
175
    {
176
        return $this->loginSasl($username, $password);
177
    }
178
179
180
    /**
181
     * Attempt to find a user's attributes given its username.
182
     *
183
     * @param string $username  The username who's attributes we want.
184
     * @return array<mixed> Associative array with the users attributes.
185
     */
186
    public function getAttributes(string $username): array
187
    {
188
        $searchUsername = $this->ldapConfig->getOptionalString('search.username', null);
189
        Assert::nullOrNotWhitespaceOnly($searchUsername);
190
191
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
192
        Assert::nullOrNotWhitespaceOnly($searchPassword);
193
194
        try {
195
            $this->connector->bind($searchUsername, $searchPassword);
196
        } catch (Error\Error $e) {
197
            throw new Error\Exception("Unable to bind using the configured search.username and search.password.");
198
        }
199
200
        $searchEnable = $this->ldapConfig->getOptionalBoolean('search.enable', false);
201
        if ($searchEnable === false) {
202
            $dnPattern = $this->ldapConfig->getString('dnpattern');
203
            $filter = '(' . str_replace('%username%', $this->escapeFilterValue($username), $dnPattern) . ')';
204
        } else {
205
            $filter = $this->buildSearchFilter($username);
206
        }
207
208
        $searchScope = $this->ldapConfig->getOptionalString('search.scope', Query::SCOPE_SUB);
209
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
210
211
        $timeout = $this->ldapConfig->getOptionalInteger('timeout', 3);
212
        Assert::natural($timeout);
213
214
        $attributes = $this->ldapConfig->getOptionalValue(
215
            'attributes',
216
            // If specifically set to NULL return all attributes, if not set at all return nothing (safe default)
217
            in_array('attributes', $this->ldapConfig->getOptions(), true) ? ['*'] : [],
218
        );
219
220
        $searchBase = $this->ldapConfig->getArray('search.base');
221
        $options = [
222
            'scope' => $searchScope,
223
            'timeout' => $timeout,
224
            'filter' => $attributes,
225
        ];
226
227
        try {
228
            /** @var \Symfony\Component\Ldap\Entry $entry */
229
            $entry = $this->connector->search($searchBase, $filter, $options, false);
230
        } catch (Error\Exception $e) {
231
            throw new Error\Error('WRONGUSERPASS');
232
        }
233
234
        return $this->processAttributes($entry);
235
    }
236
237
238
    /**
239
     * @param \Symfony\Component\Ldap\Entry $entry
240
     * @return array<mixed>
241
     */
242
    private function processAttributes(Entry $entry): array
243
    {
244
        $result = $entry->getAttributes();
245
246
        $binaries = array_intersect(
247
            array_keys($result),
248
            $this->ldapConfig->getOptionalArray('attributes.binary', []),
0 ignored issues
show
Bug introduced by
It seems like $this->ldapConfig->getOp...butes.binary', array()) can also be of type null; however, parameter $arrays of array_intersect() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

248
            /** @scrutinizer ignore-type */ $this->ldapConfig->getOptionalArray('attributes.binary', []),
Loading history...
249
        );
250
251
        foreach ($binaries as $binary) {
252
            $result[$binary] = array_map('base64_encode', $result[$binary]);
253
        }
254
255
        return $result;
256
    }
257
258
259
    /**
260
     * @param string $username
261
     * @return string
262
     */
263
    private function buildSearchFilter(string $username): string
264
    {
265
        $searchAttributes = $this->ldapConfig->getArray('search.attributes');
266
        /** @psalm-var string|null $searchFilter */
267
        $searchFilter = $this->ldapConfig->getOptionalString('search.filter', null);
268
269
        $filter = '';
270
        foreach ($searchAttributes as $attr) {
271
            $filter .= '(' . $attr . '=' . $this->escapeFilterValue($username) . ')';
0 ignored issues
show
Bug introduced by
Are you sure $this->escapeFilterValue($username) of type string|string[] can be used in concatenation? ( Ignorable by Annotation )

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

271
            $filter .= '(' . $attr . '=' . /** @scrutinizer ignore-type */ $this->escapeFilterValue($username) . ')';
Loading history...
272
        }
273
        $filter = '(|' . $filter . ')';
274
275
        // Append LDAP filters if defined
276
        if ($searchFilter !== null) {
277
            $filter = "(&" . $filter . $searchFilter . ")";
278
        }
279
280
        return $filter;
281
    }
282
283
284
    /**
285
     * @return \SimpleSAML\Module\ldap\ConnectorInterface
286
     */
287
    public function getConnector(): ConnectorInterface
288
    {
289
        return $this->connector;
290
    }
291
}
292