Passed
Pull Request — master (#12)
by Tim
02:21
created

X509userCert::findUserByAttribute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 2
b 0
f 0
nc 3
nop 2
dl 0
loc 24
rs 9.7998
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\authX509\Auth\Source;
6
7
use Exception;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\Auth;
10
use SimpleSAML\Configuration;
11
use SimpleSAML\Error;
12
use SimpleSAML\Logger;
13
use SimpleSAML\Module\ldap\ConnectorFactory;
14
use SimpleSAML\Module\ldap\ConnectorInterface;
15
use SimpleSAML\Utils;
16
use SimpleSAML\XHTML\Template;
17
use Symfony\Component\Ldap\Entry;
18
use Symfony\Component\Ldap\Ldap;
19
use Symfony\Component\Ldap\Security\LdapUserProvider;
20
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
21
22
use function array_fill_keys;
23
use function array_key_exists;
24
use function array_merge;
25
use function array_values;
26
use function current;
27
use function openssl_x509_parse;
28
use function sprintf;
29
30
/**
31
 * This class implements x509 certificate authentication with certificate validation against an LDAP directory.
32
 *
33
 * @package SimpleSAMLphp
34
 */
35
36
class X509userCert extends Auth\Source
37
{
38
    /** @var \SimpleSAML\Module\ldap\ConnectorInterface */
39
    protected ConnectorInterface $connector;
40
41
    /**
42
     * The ldap-authsource to use
43
     * @var string
44
     */
45
    private string $backend;
46
47
    /**
48
     * The ldap-authsource config to use
49
     * @var \SimpleSAML\Configuration
50
     */
51
    private Configuration $ldapConfig;
52
53
    /**
54
     * x509 attributes to use from the certificate for searching the user in the LDAP directory.
55
     * @var array<string, string>
56
     */
57
    private array $x509attributes = ['UID' => 'uid'];
58
59
    /**
60
     * LDAP attribute containing the user certificate.
61
     * This can be set to NULL to avoid looking up the certificate in LDAP
62
     * @var array|null
63
     */
64
    private ?array $ldapusercert = ['userCertificate;binary'];
65
66
67
    /**
68
     * Constructor for this authentication source.
69
     *
70
     * All subclasses who implement their own constructor must call this constructor before using $config for anything.
71
     *
72
     * @param array $info Information about this authentication source.
73
     * @param array &$config Configuration for this authentication source.
74
     */
75
    public function __construct(array $info, array &$config)
76
    {
77
        parent::__construct($info, $config);
78
79
        if (isset($config['authX509:x509attributes'])) {
80
            $this->x509attributes = $config['authX509:x509attributes'];
81
        }
82
83
        if (array_key_exists('authX509:ldapusercert', $config)) {
84
            $this->ldapusercert = $config['authX509:ldapusercert'];
85
        }
86
87
        Assert::keyExists($config, 'backend');
88
        $this->backend = $config['backend'];
89
90
        // Get the authsources file, which should contain the backend-config
91
        $authSources = Configuration::getConfig('authsources.php');
92
93
        // Verify that the authsource config exists
94
        if (!$authSources->hasValue($this->backend)) {
95
            throw new Error\Exception(
96
                sprintf('Authsource [%s] not found in authsources.php', $this->backend)
97
            );
98
        }
99
100
        // Get just the specified authsource config values
101
        $this->ldapConfig = $authSources->getConfigItem($this->backend);
102
        $type = current($this->ldapConfig->toArray());
103
        Assert::oneOf($type, ['ldap:Ldap']);
104
105
        $this->connector = ConnectorFactory::fromAuthSource($this->backend);
106
    }
107
108
109
    /**
110
     * Finish a failed authentication.
111
     *
112
     * This function can be overloaded by a child authentication class that wish to perform some operations on failure.
113
     *
114
     * @param array &$state Information about the current authentication.
115
     */
116
    public function authFailed(&$state): void
117
    {
118
        $config = Configuration::getInstance();
119
        $errorcode = $state['authX509.error'];
120
        $errorcodes = (new Error\ErrorCodes())->getAllMessages();
121
122
        $t = new Template($config, 'authX509:X509error.twig');
123
        $httpUtils = new Utils\HTTP();
124
        $t->data['loginurl'] = $httpUtils->getSelfURL();
125
126
        if (!empty($errorcode)) {
127
            if (array_key_exists($errorcode, $errorcodes['title'])) {
128
                $t->data['errortitle'] = $errorcodes['title'][$errorcode];
129
            }
130
            if (array_key_exists($errorcode, $errorcodes['descr'])) {
131
                $t->data['errordescr'] = $errorcodes['descr'][$errorcode];
132
            }
133
        }
134
135
        $t->send();
136
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
137
    }
138
139
140
    /**
141
     * Validate certificate and login.
142
     *
143
     * This function try to validate the certificate. On success, the user is logged in without going through the login
144
     * page. On failure, The authX509:X509error.php template is loaded.
145
     *
146
     * @param array &$state Information about the current authentication.
147
     */
148
    public function authenticate(array &$state): void
149
    {
150
        if (
151
            !isset($_SERVER['SSL_CLIENT_CERT']) ||
152
            ($_SERVER['SSL_CLIENT_CERT'] == '')
153
        ) {
154
            $state['authX509.error'] = "NOCERT";
155
            $this->authFailed($state);
156
157
            throw new Exception("Should never be reached");
158
        }
159
160
        $client_cert = $_SERVER['SSL_CLIENT_CERT'];
161
        $client_cert_data = openssl_x509_parse($client_cert);
162
        if ($client_cert_data === false) {
163
            Logger::error('authX509: invalid cert');
164
            $state['authX509.error'] = "INVALIDCERT";
165
            $this->authFailed($state);
166
167
            throw new Exception("Should never be reached");
168
        }
169
170
        $entry = $dn = null;
171
        foreach ($this->x509attributes as $x509_attr => $attr) {
172
            // value is scalar
173
            if (array_key_exists($x509_attr, $client_cert_data['subject'])) {
174
                $value = $client_cert_data['subject'][$x509_attr];
175
                Logger::info('authX509: cert ' . $x509_attr . ' = ' . $value);
176
                $entry = $this->findUserByAttribute($attr, $value);
177
                if ($entry !== null) {
178
                    $dn = $attr;
179
                    break;
180
                }
181
            }
182
        }
183
184
        if ($entry === null) {
0 ignored issues
show
introduced by
The condition $entry === null is always true.
Loading history...
185
            Logger::error('authX509: cert has no matching user in LDAP.');
186
            $state['authX509.error'] = "UNKNOWNCERT";
187
            $this->authFailed($state);
188
189
            throw new Exception("Should never be reached");
190
        }
191
192
        if ($this->ldapusercert === null) {
193
            // do not check for certificate match
194
            $attributes = array_intersect_key(
195
                $entry->getAttributes(),
196
                array_fill_keys(array_values($this->x509attributes), null),
197
            );
198
199
            $state['Attributes'] = $attributes;
200
            $this->authSuccesful($state);
201
202
            throw new Exception("Should never be reached");
203
        }
204
205
        $ldap_certs = array_map([$entry, 'getAttribute'], $this->ldapusercert);
206
        if (empty($ldap_certs)) {
207
            Logger::error('authX509: no certificate found in LDAP for dn=' . $dn);
208
            $state['authX509.error'] = "UNKNOWNCERT";
209
            $this->authFailed($state);
210
211
            throw new Exception("Should never be reached");
212
        }
213
214
215
        $merged_ldapcerts = [];
216
        foreach ($this->ldapusercert as $attr) {
217
            $merged_ldapcerts = array_merge($merged_ldapcerts, $ldap_certs[0][$attr]);
218
        }
219
        $ldap_certs = $merged_ldapcerts;
220
221
        $cryptoUtils = new Utils\Crypto();
222
        foreach ($ldap_certs as $ldap_cert) {
223
            $pem = $cryptoUtils->der2pem($ldap_cert);
224
            $ldap_cert_data = openssl_x509_parse($pem);
225
            if ($ldap_cert_data === false) {
226
                Logger::error('authX509: cert in LDAP is invalid for dn=' . $dn);
227
                continue;
228
            }
229
230
            if ($ldap_cert_data === $client_cert_data) {
231
                $attributes = array_intersect_key(
232
                    $entry->getAttributes(),
233
                    array_fill_keys(array_values($this->x509attributes), null)
234
                );
235
                $state['Attributes'] = $attributes;
236
                $this->authSuccesful($state);
237
238
                throw new Exception("Should never be reached");
239
            }
240
        }
241
242
        Logger::error('authX509: no matching cert in LDAP for dn=' . $dn);
243
        $state['authX509.error'] = "UNKNOWNCERT";
244
        $this->authFailed($state);
245
246
        throw new Exception("Should never be reached");
247
    }
248
249
250
    /**
251
     * Finish a successful authentication.
252
     *
253
     * This function can be overloaded by a child authentication class that wish to perform some operations after login.
254
     *
255
     * @param array &$state Information about the current authentication.
256
     */
257
    public function authSuccesful(array &$state): void
258
    {
259
        Auth\Source::completeAuth($state);
260
261
        throw new Exception("Should never be reached");
262
    }
263
264
265
    /**
266
     * Find user in LDAP-store
267
     *
268
     * @param string $attr
269
     * @param string $value
270
     * @return \Symfony\Component\Ldap\Entry|null
271
     */
272
    public function findUserByAttribute(string $attr, string $value): ?Entry
273
    {
274
        $searchBase = $this->ldapConfig->getArray('search.base');
275
276
        $searchUsername = $this->ldapConfig->getString('search.username');
277
        Assert::notWhitespaceOnly($searchUsername);
278
279
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
280
        Assert::nullOrnotWhitespaceOnly($searchPassword);
281
282
        $ldap = ConnectorFactory::fromAuthSource($this->backend);
283
        $connection = new Ldap($ldap->getAdapter());
284
285
        foreach ($searchBase as $base) {
286
            $ldapUserProvider = new LdapUserProvider($connection, $base, $searchUsername, $searchPassword, [], $attr);
287
            try {
288
                return $ldapUserProvider->loadUserByIdentifier($value)->getEntry();
289
            } catch (UserNotFoundException $e) {
290
                continue;
291
            }
292
        }
293
294
        // We haven't found the user
295
        return null;
296
    }
297
}
298