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

Ldap   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 204
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 21
eloc 112
c 2
b 0
f 0
dl 0
loc 204
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A bind() 0 12 3
F login() 0 153 17
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
        $ldapServers = [];
78
        foreach (explode(' ', $this->ldapConfig->getString('connection_string')) as $connection_string) {
79
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
80
81
            $ldap_servers[] = LdapObject::create(
82
                $this->ldapConfig->getString('extension', 'ext_ldap'),
83
                [
84
                    'connection_string' => $connection_string,
85
                    'encryption' => 'ssl',
86
                ]
87
            );
88
        }
89
90
        $searchScope = $this->ldapConfig->getString('search.scope', Query::SCOPE_SUB);
91
        Assert::oneOf($searchScope, [Query::SCOPE_BASE, Query::SCOPE_ONE, Query::SCOPE_SUB]);
92
93
        $referrals = $this->ldapConfig->getValue('referrals', Query::DEREF_NEVER);
94
        Assert::oneOf($referrals, [Query::DEREF_ALWAYS, Query::DEREF_NEVER, Query::DEREF_FINDING, Query::DEREF_SEARCHING]);
95
96
        $timeout = $this->ldapConfig->getString('timeout', 3);
97
        $searchBase = $this->ldapConfig->getArray('search.base');
98
        $options = [
99
            'scope' => $searchScope,
100
            'timeout' => $timeout,
101
            'deref' => $referrals,
102
        ];
103
104
        $searchEnable = $this->ldapConfig->getBoolean('search.enable', false);
105
        if ($searchEnable === false) {
106
            $dnPattern = $this->ldapConfig->getString('dnpattern');
107
            $dn = str_replace('%username%', $username, $dnPattern);
108
109
            $filter = '';
110
        } else {
111
            $searchUsername = $this->ldapConfig->getString('search.username');
112
            Assert::notWhitespaceOnly($searchUsername);
113
114
            $searchPassword = $this->ldapConfig->getString('search.password', null);
115
            Assert::nullOrnotWhitespaceOnly($searchPassword);
116
117
            $searchAttributes = $this->ldapConfig->getArray('search.attributes');
118
            $searchFilter = $this->ldapConfig->getString('search.filter', null);
119
120
            $ldap = $this->bind($ldapServers, $searchUsername, $searchPassword);
121
122
            $filter = '';
123
            foreach ($searchAttributes as $attr) {
124
                $filter .= '(' . $attr . '=' . $username . ')';
125
            }
126
            $filter = '(|' . $filter . ')';
127
128
            // Append LDAP filters if defined
129
            if ($searchFilter !== null) {
130
                $filter = "(&" . $filter . "" . $searchFilter . ")";
131
            }
132
133
            $entry = null;
134
            foreach ($searchBase as $base) {
135
                $query = $ldap->query($base, $filter, $options);
136
                $result = $query->execute();
137
                $result = is_array($result) ? $result : $result->toArray();
138
139
                if (count($result) > 1) {
140
                    throw new Error\Exception(
141
                        sprintf(
142
                            "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
143
                            count($result),
144
                            $base,
145
                            $filter,
146
                        )
147
                    );
148
                } elseif (count($result) === 1) {
149
                    $entry = array_pop($result);
150
                    break;
151
                } else {
152
                    Logger::debug(
153
                        sprintf(
154
                            "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
155
                            count($result),
156
                            $base,
157
                            $filter,
158
                        )
159
                    );
160
                }
161
            }
162
163
            if ($entry === null) {
164
                throw new Error\UserNotFound("User not found");
165
            }
166
167
            $dn = $entry->getDn();
168
        }
169
170
        $ldap = $this->bind($ldapServers, $dn, $password);
171
172
        $entry = null;
173
        foreach ($searchBase as $base) {
174
            $query = $ldap->query($base, sprintf('(distinguishedName=%s)', $dn), $options);
175
            $result = $query->execute();
176
            $result = is_array($result) ? $result : $result->toArray();
177
178
            if (count($result) > 1) {
179
                throw new Error\Exception(
180
                    sprintf(
181
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
182
                        count($result),
183
                        $base,
184
                        $filter,
185
                    )
186
                );
187
            } elseif (count($result) === 1) {
188
                $entry = array_pop($result);
189
                break;
190
            } else {
191
                Logger::debug(
192
                    sprintf(
193
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
194
                        count($result),
195
                        $base,
196
                        $filter,
197
                    )
198
                );
199
            }
200
        }
201
202
        if ($entry === null) {
203
            throw new Error\UserNotFound("User not found");
204
        }
205
206
        $attributes = $this->ldapConfig->getArray('attributes', []);
207
        if ($attributes === ['*']) {
208
            $result = $entry->getAttributes();
209
        } else {
210
            $result = array_intersect_key(
211
                $entry->getAttributes(),
212
                array_fill_keys(array_values($attributes), null)
213
            );
214
        }
215
216
        $binaries = array_intersect(
217
            array_keys($result),
218
            $this->ldapConfig->getArray('attributes.binary', []),
219
        );
220
        foreach ($binaries as $binary) {
221
            $result[$binary] = array_map('base64_encode', $result[$binary]);
222
        }
223
224
        return $result;
225
    }
226
227
228
    /**
229
     * Bind to an LDAP-server
230
     */
231
    private function bind(array $ldapServers, string $username, ?string $password)
232
    {
233
        foreach ($ldapServers as $ldap) {
234
            try {
235
                $ldap->bind($username, strval($password));
236
                return $ldap;
237
            } catch (ConnectionException $e) {
238
                // Try next server
239
            }
240
        }
241
242
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
243
    }
244
}
245