Passed
Pull Request — master (#12)
by Tim
02:55
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 = Error\ErrorCodes::getAllErrorCodeMessages();
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Error\ErrorCo...tAllErrorCodeMessages() has been deprecated: Static method access is deprecated. Move to instance method. ( Ignorable by Annotation )

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

120
        $errorcodes = /** @scrutinizer ignore-deprecated */ Error\ErrorCodes::getAllErrorCodeMessages();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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[$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