Passed
Pull Request — master (#28)
by Tim
02:09
created

Ldap::bind()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 3
nop 3
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\Logger;
11
use SimpleSAML\Module\core\Auth\UserPassBase;
12
use Symfony\Component\Ldap\Entry;
13
use Symfony\Component\Ldap\Exception\ConnectionException;
14
use Symfony\Component\Ldap\Ldap as LdapObject;
15
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
16
17
use function array_fill_keys;
18
use function array_keys;
19
use function array_map;
20
use function array_pop;
21
use function array_values;
22
use function count;
23
use function explode;
24
use function is_array;
25
use function sprintf;
26
use function strval;
27
use function str_replace;
28
use function var_export;
29
30
/**
31
 * LDAP authentication source.
32
 *
33
 * See the ldap-entry in config-templates/authsources.php for information about
34
 * configuration of this authentication source.
35
 *
36
 * @package simplesamlphp/simplesamlphp-module-ldap
37
 */
38
39
class Ldap extends UserPassBase
40
{
41
    /**
42
     * An LDAP configuration object.
43
     */
44
    private Configuration $ldapConfig;
45
46
47
    /**
48
     * Constructor for this authentication source.
49
     *
50
     * @param array $info  Information about this authentication source.
51
     * @param array $config  Configuration.
52
     */
53
    public function __construct(array $info, array $config)
54
    {
55
        // Call the parent constructor first, as required by the interface
56
        parent::__construct($info, $config);
57
58
        $this->ldapConfig = Configuration::loadFromArray(
59
            $config,
60
            'authsources[' . var_export($this->authId, true) . ']'
61
        );
62
    }
63
64
65
    /**
66
     * Attempt to log in using the given username and password.
67
     *
68
     * @param string $username  The username the user wrote.
69
     * @param string $password  The password the user wrote.
70
     * @return array  Associative array with the users attributes.
71
     */
72
    protected function login(string $username, string $password): array
73
    {
74
        $encryption = $this->ldapConfig->getString('encryption', 'ssl');
75
        Assert::oneOf($encryption, ['none', 'ssl', 'tls']);
76
77
        $version = $this->ldapConfig->getInteger('version', 3);
78
        Assert::positiveInteger($version);
79
80
        $ldapServers = [];
81
        foreach (explode(' ', $this->ldapConfig->getString('connection_string')) as $connection_string) {
82
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
83
84
            $ldapServers[] = LdapObject::create(
85
                $this->ldapConfig->getString('extension', 'ext_ldap'),
86
                [
87
                    'connection_string' => $connection_string,
88
                    'encryption' => 'ssl',
89
                    'version' => $version,
90
                ]
91
            );
92
        }
93
94
        $searchScope = $this->ldapConfig->getString('search.scope', Query::SCOPE_SUB);
95
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
96
97
        $referrals = $this->ldapConfig->getValue('referrals', Query::DEREF_NEVER);
98
        Assert::oneOf($referrals, [Query::DEREF_ALWAYS, Query::DEREF_NEVER, Query::DEREF_FINDING, Query::DEREF_SEARCHING]);
99
100
        $timeout = $this->ldapConfig->getString('timeout', 3);
101
        $searchBase = $this->ldapConfig->getArray('search.base');
102
        $options = [
103
            'scope' => $searchScope,
104
            'timeout' => $timeout,
105
            'deref' => $referrals,
106
        ];
107
108
        $searchEnable = $this->ldapConfig->getBoolean('search.enable', false);
109
        if ($searchEnable === false) {
110
            $dnPattern = $this->ldapConfig->getString('dnpattern');
111
            $dn = str_replace('%username%', $username, $dnPattern);
112
113
            $filter = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $filter is dead and can be removed.
Loading history...
114
        } else {
115
            $searchUsername = $this->ldapConfig->getString('search.username');
116
            Assert::notWhitespaceOnly($searchUsername);
117
118
            $searchPassword = $this->ldapConfig->getString('search.password', null);
119
            Assert::nullOrnotWhitespaceOnly($searchPassword);
120
121
            $searchAttributes = $this->ldapConfig->getArray('search.attributes');
122
            $searchFilter = $this->ldapConfig->getString('search.filter', null);
123
124
            $ldap = $this->bind($ldapServers, $searchUsername, $searchPassword);
125
126
            $filter = '';
127
            foreach ($searchAttributes as $attr) {
128
                $filter .= '(' . $attr . '=' . $username . ')';
129
            }
130
            $filter = '(|' . $filter . ')';
131
132
            // Append LDAP filters if defined
133
            if ($searchFilter !== null) {
134
                $filter = "(&" . $filter . $searchFilter . ")";
135
            }
136
137
            $dn = $this->searchForDn($ldap, $searchBase, $filter, $options);
138
        }
139
140
        $ldap = $this->bind($ldapServers, $dn, $password);
141
142
        $entry = null;
143
        foreach ($searchBase as $base) {
144
            $filter = sprintf('(distinguishedName=%s)', $dn);
145
            $query = $ldap->query($base, $filter, $options);
146
            $result = $query->execute();
147
            $result = is_array($result) ? $result : $result->toArray();
148
149
            if (count($result) > 1) {
150
                throw new Error\Exception(
151
                    sprintf(
152
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
153
                        count($result),
154
                        $base,
155
                        $filter,
156
                    )
157
                );
158
            } elseif (count($result) === 1) {
159
                $entry = array_pop($result);
160
                break;
161
            } else {
162
                Logger::debug(
163
                    sprintf(
164
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
165
                        count($result),
166
                        $base,
167
                        $filter,
168
                    )
169
                );
170
            }
171
        }
172
173
        if ($entry === null) {
174
            throw new Error\UserNotFound(
175
                sprintf("User %s could not be found using search base [%s]", $dn, implode(', ', $searchBase))
176
            );
177
        }
178
179
        $attributes = $this->ldapConfig->getArray('attributes', []);
180
        if ($attributes === ['*']) {
181
            $result = $entry->getAttributes();
182
        } else {
183
            $result = array_intersect_key(
184
                $entry->getAttributes(),
185
                array_fill_keys(array_values($attributes), null)
186
            );
187
        }
188
189
        $binaries = array_intersect(
190
            array_keys($result),
191
            $this->ldapConfig->getArray('attributes.binary', []),
192
        );
193
        foreach ($binaries as $binary) {
194
            $result[$binary] = array_map('base64_encode', $result[$binary]);
195
        }
196
197
        return $result;
198
    }
199
200
201
    /**
202
     * Bind to an LDAP-server
203
     *
204
     * @param \Symfony\Component\Ldap\Ldap[] $ldapServers
205
     * @param string $username
206
     * @param string|null $password  Null for passwordless logon
207
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
208
     */
209
    private function bind(array $ldapServers, string $username, ?string $password)
210
    {
211
        foreach ($ldapServers as $ldap) {
212
            try {
213
                $ldap->bind($username, strval($password));
214
                return $ldap;
215
            } catch (ConnectionException $e) {
216
                // Try next server
217
            }
218
        }
219
220
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
221
    }
222
223
224
    /**
225
     * Search the LDAP-directory for a specific DN
226
     *
227
     * @param \Symfony\Component\Ldap\Ldap $ldap
228
     * @param array $searchBase
229
     * @param string $filter
230
     * @param array $options
231
     * @return string  The DN of the user
232
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
233
     * @throws \SimpleSAML\Error\UserNotFound if the user cannot be found using the given serach base and filter
234
     */
235
    private function searchForDn(LdapObject $ldap, array $searchBase, string $filter, array $options)
236
    {
237
        $entry = null;
238
        foreach ($searchBase as $base) {
239
            $query = $ldap->query($base, $filter, $options);
240
            $result = $query->execute();
241
            $result = is_array($result) ? $result : $result->toArray();
242
243
            if (count($result) > 1) {
244
                throw new Error\Exception(
245
                    sprintf(
246
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
247
                        count($result),
248
                        $base,
249
                        $filter,
250
                    )
251
                );
252
            } elseif (count($result) === 1) {
253
                $entry = array_pop($result);
254
                break;
255
            } else {
256
                Logger::debug(
257
                    sprintf(
258
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
259
                        count($result),
260
                        $base,
261
                        $filter,
262
                    )
263
                );
264
            }
265
        }
266
267
        if ($entry === null) {
268
            throw new Error\UserNotFound(
269
            sprintf(
270
                    "User not found using search base [%s] and filter '%s'",
271
                    implode(', ', $searchBase),
272
                    $filter
273
                )
274
            );
275
        }
276
277
        return $entry->getDn();
278
    }
279
}
280