Passed
Pull Request — master (#28)
by Tim
02:35
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
            $ldap_servers[] = 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 = '';
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
            $entry = null;
138
            foreach ($searchBase as $base) {
139
                $query = $ldap->query($base, $filter, $options);
140
                $result = $query->execute();
141
                $result = is_array($result) ? $result : $result->toArray();
142
143
                if (count($result) > 1) {
144
                    throw new Error\Exception(
145
                        sprintf(
146
                            "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
147
                            count($result),
148
                            $base,
149
                            $filter,
150
                        )
151
                    );
152
                } elseif (count($result) === 1) {
153
                    $entry = array_pop($result);
154
                    break;
155
                } else {
156
                    Logger::debug(
157
                        sprintf(
158
                            "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
159
                            count($result),
160
                            $base,
161
                            $filter,
162
                        )
163
                    );
164
                }
165
            }
166
167
            if ($entry === null) {
168
                throw new Error\UserNotFound("User not found");
169
            }
170
171
            $dn = $entry->getDn();
172
        }
173
174
        $ldap = $this->bind($ldapServers, $dn, $password);
175
176
        $entry = null;
177
        foreach ($searchBase as $base) {
178
            $query = $ldap->query($base, sprintf('(distinguishedName=%s)', $dn), $options);
179
            $result = $query->execute();
180
            $result = is_array($result) ? $result : $result->toArray();
181
182
            if (count($result) > 1) {
183
                throw new Error\Exception(
184
                    sprintf(
185
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
186
                        count($result),
187
                        $base,
188
                        $filter,
189
                    )
190
                );
191
            } elseif (count($result) === 1) {
192
                $entry = array_pop($result);
193
                break;
194
            } else {
195
                Logger::debug(
196
                    sprintf(
197
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
198
                        count($result),
199
                        $base,
200
                        $filter,
201
                    )
202
                );
203
            }
204
        }
205
206
        if ($entry === null) {
207
            throw new Error\UserNotFound("User not found");
208
        }
209
210
        $attributes = $this->ldapConfig->getArray('attributes', []);
211
        if ($attributes === ['*']) {
212
            $result = $entry->getAttributes();
213
        } else {
214
            $result = array_intersect_key(
215
                $entry->getAttributes(),
216
                array_fill_keys(array_values($attributes), null)
217
            );
218
        }
219
220
        $binaries = array_intersect(
221
            array_keys($result),
222
            $this->ldapConfig->getArray('attributes.binary', []),
223
        );
224
        foreach ($binaries as $binary) {
225
            $result[$binary] = array_map('base64_encode', $result[$binary]);
226
        }
227
228
        return $result;
229
    }
230
231
232
    /**
233
     * Bind to an LDAP-server
234
     */
235
    private function bind(array $ldapServers, string $username, ?string $password)
236
    {
237
        foreach ($ldapServers as $ldap) {
238
            try {
239
                $ldap->bind($username, strval($password));
240
                return $ldap;
241
            } catch (ConnectionException $e) {
242
                // Try next server
243
            }
244
        }
245
246
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
247
    }
248
}
249