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(); |
|
|
|
|
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(); |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.