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

X509userCert   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 113
c 3
b 0
f 0
dl 0
loc 264
rs 10
wmc 27

5 Methods

Rating   Name   Duplication   Size   Complexity  
A authFailed() 0 21 4
A __construct() 0 31 4
C authenticate() 0 103 15
A authSuccesful() 0 5 1
A findUserByAttribute() 0 24 3
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 = [];
206
        foreach ($this->ldapusercert as $attr) {
207
            $ldap_certs[$attr] = $entry->getAttribute($attr);
208
        }
209
210
        if (empty($ldap_certs)) {
211
            Logger::error('authX509: no certificate found in LDAP for dn=' . $dn);
212
            $state['authX509.error'] = "UNKNOWNCERT";
213
            $this->authFailed($state);
214
215
            throw new Exception("Should never be reached");
216
        }
217
218
219
        $merged_ldapcerts = [];
220
        foreach ($this->ldapusercert as $attr) {
221
            $merged_ldapcerts = array_merge($merged_ldapcerts, $ldap_certs[$attr]);
222
        }
223
        $ldap_certs = $merged_ldapcerts;
224
225
        $cryptoUtils = new Utils\Crypto();
226
        foreach ($ldap_certs as $ldap_cert) {
227
            $pem = $cryptoUtils->der2pem($ldap_cert);
228
            $ldap_cert_data = openssl_x509_parse($pem);
229
            if ($ldap_cert_data === false) {
230
                Logger::error('authX509: cert in LDAP is invalid for dn=' . $dn);
231
                continue;
232
            }
233
234
            if ($ldap_cert_data === $client_cert_data) {
235
                $attributes = array_intersect_key(
236
                    $entry->getAttributes(),
237
                    array_fill_keys(array_values($this->x509attributes), null)
238
                );
239
                $state['Attributes'] = $attributes;
240
                $this->authSuccesful($state);
241
242
                throw new Exception("Should never be reached");
243
            }
244
        }
245
246
        Logger::error('authX509: no matching cert in LDAP for dn=' . $dn);
247
        $state['authX509.error'] = "UNKNOWNCERT";
248
        $this->authFailed($state);
249
250
        throw new Exception("Should never be reached");
251
    }
252
253
254
    /**
255
     * Finish a successful authentication.
256
     *
257
     * This function can be overloaded by a child authentication class that wish to perform some operations after login.
258
     *
259
     * @param array &$state Information about the current authentication.
260
     */
261
    public function authSuccesful(array &$state): void
262
    {
263
        Auth\Source::completeAuth($state);
264
265
        throw new Exception("Should never be reached");
266
    }
267
268
269
    /**
270
     * Find user in LDAP-store
271
     *
272
     * @param string $attr
273
     * @param string $value
274
     * @return \Symfony\Component\Ldap\Entry|null
275
     */
276
    public function findUserByAttribute(string $attr, string $value): ?Entry
277
    {
278
        $searchBase = $this->ldapConfig->getArray('search.base');
279
280
        $searchUsername = $this->ldapConfig->getString('search.username');
281
        Assert::notWhitespaceOnly($searchUsername);
282
283
        $searchPassword = $this->ldapConfig->getOptionalString('search.password', null);
284
        Assert::nullOrnotWhitespaceOnly($searchPassword);
285
286
        $ldap = ConnectorFactory::fromAuthSource($this->backend);
287
        $connection = new Ldap($ldap->getAdapter());
288
289
        foreach ($searchBase as $base) {
290
            $ldapUserProvider = new LdapUserProvider($connection, $base, $searchUsername, $searchPassword, [], $attr);
291
            try {
292
                return $ldapUserProvider->loadUserByIdentifier($value)->getEntry();
293
            } catch (UserNotFoundException $e) {
294
                continue;
295
            }
296
        }
297
298
        // We haven't found the user
299
        return null;
300
    }
301
}
302