Issues (10)

src/OMSAML2.php (8 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace OMSAML2;
6
7
use DOMElement;
8
use Exception;
9
use RobRichards\XMLSecLibs\XMLSecurityKey;
10
use SAML2\AuthnRequest;
11
use SAML2\Compat\AbstractContainer;
12
use SAML2\Compat\ContainerSingleton;
13
use SAML2\Constants;
14
use SAML2\DOMDocumentFactory;
15
use SAML2\SignedElement;
16
use SAML2\Utils;
17
use SAML2\XML\md\EntityDescriptor;
18
use SAML2\XML\md\IDPSSODescriptor;
19
use SAML2\XML\saml\Issuer;
20
21
/**
22
 * @package OMSAML2
23
 */
24
class OMSAML2
25
{
26
    public const LOA_LOW = 'http://eidas.europa.eu/LoA/low';
27
    public const LOA_SUBSTANTIAL = 'http://eidas.europa.eu/LoA/substantial';
28
    public const LOA_HIGH = 'http://eidas.europa.eu/LoA/high';
29
30
    /**@var $idp_metadata_url string */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $idp_metadata_url at position 0 could not be parsed: Unknown type name '$idp_metadata_url' at position 0 in $idp_metadata_url.
Loading history...
31
    protected static $idp_metadata_url = null;
32
    /**@var $idp_metadata_contents string */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $idp_metadata_contents at position 0 could not be parsed: Unknown type name '$idp_metadata_contents' at position 0 in $idp_metadata_contents.
Loading history...
33
    protected static $idp_metadata_contents = null;
34
    /**@var $idp_certificate XMLSecurityKey */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $idp_certificate at position 0 could not be parsed: Unknown type name '$idp_certificate' at position 0 in $idp_certificate.
Loading history...
35
    protected static $idp_certificate = null;
36
    /**@var $own_certificate XMLSecurityKey */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $own_certificate at position 0 could not be parsed: Unknown type name '$own_certificate' at position 0 in $own_certificate.
Loading history...
37
    protected static $own_certificate = null;
38
    /**@var $own_private_key XMLSecurityKey */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $own_private_key at position 0 could not be parsed: Unknown type name '$own_private_key' at position 0 in $own_private_key.
Loading history...
39
    protected static $own_private_key = null;
40
41
    /**
42
     * Reset state of whole component to default
43
     */
44
    public static function reset(): void
45
    {
46
        self::$idp_metadata_contents = null;
47
        self::$idp_metadata_url = null;
48
        self::$own_private_key = null;
49
        self::$own_certificate = null;
50
        self::$idp_certificate = null;
51
    }
52
53
    /**
54
     * Returns associative array of found Logout URLs
55
     * keys are binding constants, such as Constants::BINDING_HTTP_REDIRECT
56
     *
57
     * @param EntityDescriptor|null $idp_descriptor
58
     * @return array
59
     * @throws Exception
60
     * @see Constants
61
     *
62
     */
63
    public static function extractSSOLogoutUrls(?EntityDescriptor $idp_descriptor = null): array
64
    {
65
        return self::extractSSOUrls(true, $idp_descriptor);
66
    }
67
68
    /**
69
     * @param bool $extract_logout_urls
70
     * @param EntityDescriptor $idp_descriptor
71
     * @return array
72
     * @throws Exception
73
     */
74
    public static function extractSSOUrls(bool $extract_logout_urls = false, ?EntityDescriptor $idp_descriptor = null): array
75
    {
76
        if (empty($idp_descriptor)) {
77
            $idp_descriptor = self::getIdpDescriptor();
78
        }
79
80
        $idp_sso_descriptor = false;
81
        if ($idp_descriptor instanceof EntityDescriptor) {
0 ignored issues
show
$idp_descriptor is always a sub-type of SAML2\XML\md\EntityDescriptor.
Loading history...
82
            foreach ($idp_descriptor->getRoleDescriptor() as $role_descriptor) {
83
                if ($role_descriptor instanceof IDPSSODescriptor) {
84
                    $idp_sso_descriptor = $role_descriptor;
85
                }
86
            }
87
        }
88
89
        $found = [];
90
91
        if ($idp_sso_descriptor instanceof IDPSSODescriptor) {
92
            foreach ($extract_logout_urls ? $idp_sso_descriptor->getSingleLogoutService() : $idp_sso_descriptor->getSingleSignOnService() as $descriptorType) {
93
                if (empty($descriptorType->getBinding())) {
94
                    continue;
95
                }
96
                $found[$descriptorType->getBinding()] = $descriptorType->getLocation();
97
            }
98
        }
99
100
        return $found;
101
    }
102
103
    /**
104
     * Returns EntityDescriptor instance or null, if metadata could not be fetched
105
     * throws exception in case of invalid or dangerous XML contents
106
     *
107
     * @param string|null $metadata_string
108
     * @return EntityDescriptor|null null if provided string or automatically retrieved string is empty
109
     * @throws Exception
110
     */
111
    public static function getIdpDescriptor(?string $metadata_string = null): ?EntityDescriptor
112
    {
113
        if (empty($metadata_string)) {
114
            $metadata_string = self::getIdPMetadataContents();
115
            if (empty($metadata_string)) {
116
                return null;
117
            }
118
        }
119
        $metadata_dom = DOMDocumentFactory::fromString($metadata_string);
120
        return new EntityDescriptor($metadata_dom->documentElement);
121
    }
122
123
    /**
124
     * Returns cached or freshly retrieved IdP metadata as a string, or null
125
     *
126
     * @param string|null $url
127
     * @return null|string
128
     * @throws Exception
129
     */
130
    public static function getIdPMetadataContents(?string $url = null): ?string
131
    {
132
        if (!empty($url)) {
133
            self::setIdPMetadataUrl($url);
134
        }
135
        if (empty(self::$idp_metadata_contents)) {
136
            if (empty(self::getIdPMetadataUrl())) {
137
                throw new Exception("IdP Metadata URL not yet configured");
138
            }
139
            $idp_metadata_contents_fresh = file_get_contents(self::getIdPMetadataUrl());
140
            self::setIdPMetadataContents($idp_metadata_contents_fresh);
141
        }
142
        return self::$idp_metadata_contents;
143
    }
144
145
    /**
146
     * Sets metadata content cache to provided string contents or null, if provided value is empty
147
     *
148
     * @param string $contents
149
     */
150
    public static function setIdPMetadataContents(?string $contents): void
151
    {
152
        $contents = trim($contents);
153
        self::$idp_metadata_contents = empty($contents) ? null : $contents;
154
    }
155
156
    /**
157
     * Retrieves currently configured IdP Metadata URL or null if current value is empty
158
     *
159
     * @return null|string
160
     */
161
    public static function getIdPMetadataUrl(): ?string
162
    {
163
        return empty(self::$idp_metadata_url) ? null : self::$idp_metadata_url;
164
    }
165
166
    /**
167
     * If provided URL is not string or is empty, will set null
168
     *
169
     * @param string $url
170
     */
171
    public static function setIdPMetadataUrl(string $url): void
172
    {
173
        $url = trim($url);
174
        if ($url != self::$idp_metadata_url) {
175
            // empty metadata contents cache if URL changes
176
            self::$idp_metadata_contents = null;
177
        }
178
        self::$idp_metadata_url = empty($url) ? null : $url;
179
    }
180
181
    /**
182
     * Validates signed element (metadata, auth-response, logout-response) signature
183
     *
184
     * @param XMLSecurityKey $publicKey
185
     * @param SignedElement|null $idp_descriptor if not provided, EntityDescriptor (IdP metadata) will be retrieved internally by configured URL
186
     * @return bool
187
     * @throws Exception
188
     */
189
    public static function validateSignature(XMLSecurityKey $publicKey = null, ?SignedElement $idp_descriptor = null): bool
190
    {
191
        if (empty($idp_descriptor)) {
192
            $idp_descriptor = self::getIdpDescriptor();
193
        }
194
195
        if (empty($publicKey)) {
196
            $publicKey = self::getIdpCertificate();
197
        }
198
199
        return $idp_descriptor->validate($publicKey);
0 ignored issues
show
It seems like $publicKey can also be of type null; however, parameter $key of SAML2\SignedElementHelper::validate() does only seem to accept RobRichards\XMLSecLibs\XMLSecurityKey, maybe add an additional type check? ( Ignorable by Annotation )

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

199
        return $idp_descriptor->validate(/** @scrutinizer ignore-type */ $publicKey);
Loading history...
It seems like $publicKey can also be of type null; however, parameter $key of SAML2\SignedElement::validate() does only seem to accept RobRichards\XMLSecLibs\XMLSecurityKey, maybe add an additional type check? ( Ignorable by Annotation )

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

199
        return $idp_descriptor->validate(/** @scrutinizer ignore-type */ $publicKey);
Loading history...
200
    }
201
202
    /**
203
     * Returns internally configured IdP certificate as XMLSecurityKey or null if not configured yet
204
     *
205
     * @return XMLSecurityKey|null
206
     */
207
    public static function getIdpCertificate(): ?XMLSecurityKey
208
    {
209
        return self::$idp_certificate;
210
    }
211
212
    /**
213
     * @param string|null $certificate_url
214
     * @param string|null $certificate_data
215
     * @param string $algorithm
216
     * @return bool
217
     */
218
    public static function setIdpCertificate(?string $certificate_url = null, ?string $certificate_data = null, $algorithm = XMLSecurityKey::RSA_SHA256): bool
219
    {
220
        try {
221
            self::$idp_certificate = new XMLSecurityKey($algorithm, ['type' => 'public']);
222
            self::$idp_certificate->loadKey(empty($certificate_data) ? file_get_contents($certificate_url) : $certificate_data, false, true);
223
            self::$idp_certificate->encryptData('abcdef');
224
            return true;
225
        } catch (Exception $e) {
226
            self::$idp_certificate = null;
227
            ContainerSingleton::getInstance()->getLogger()->critical('setIdpCertificate failed: ' . $e->getMessage(), [$e]);
228
            return false;
229
        }
230
    }
231
232
    /**
233
     * Returns stored certificate/public-key in form of XMLSecurityKey, or null if not configured
234
     *
235
     * @return null|XMLSecurityKey
236
     */
237
    public static function getOwnCertificatePublicKey(): ?XMLSecurityKey
238
    {
239
        return self::$own_certificate;
240
    }
241
242
    /**
243
     * Returns stored private-key as XMLSecurityKey instance, or null if not configured
244
     *
245
     * @return null|XMLSecurityKey
246
     */
247
    public static function getOwnPrivateKey(): ?XMLSecurityKey
248
    {
249
        return self::$own_private_key;
250
    }
251
252
    /**
253
     * Creates public key for verifying RSA/SHA256 signature
254
     *
255
     * @param string $path absolute path to certificate file or URL from which it can be retrieved
256
     * @param string $algorithm
257
     * @param string $type
258
     * @return XMLSecurityKey
259
     * @throws Exception
260
     */
261
    public static function getPublicKeyFromCertificate(string $path, $algorithm = XMLSecurityKey::RSA_SHA256, $type = 'public'): XMLSecurityKey
262
    {
263
        $cert_data = file_get_contents($path);
264
        $key = new XMLSecurityKey($algorithm, ['type' => $type]);
265
        $key->loadKey($cert_data, false, true);
266
        return $key;
267
    }
268
269
    /**
270
     * Create private key for verifying RSA/SHA256 signature
271
     *
272
     * @param string $path absolute path to key file or URL from which it can be retrieved
273
     * @param string $algorithm
274
     * @param string $type
275
     * @return XMLSecurityKey
276
     * @throws Exception
277
     */
278
    public static function getPrivateKeyFromFile(string $path, $algorithm = XMLSecurityKey::RSA_SHA256, $type = 'private'): XMLSecurityKey
279
    {
280
        $key_data = file_get_contents($path);
281
        $key = new XMLSecurityKey($algorithm, ['type' => $type]);
282
        $key->loadKey($key_data, false, false);
283
        return $key;
284
    }
285
286
    /**
287
     * Generates AuthRequest/AuthnRequest using provided information
288
     *
289
     * @param AbstractContainer $container
290
     * @param string $issuer
291
     * @param string $assertionConsumerServiceURL
292
     * @param string $idpLoginRedirectUrl
293
     * @param string $levelOfAssurance
294
     * @param string $requestedAuthnContextComparison
295
     * @return AuthnRequest
296
     * @throws Exception
297
     */
298
    public static function generateAuthRequest(AbstractContainer $container, string $issuer, string $assertionConsumerServiceURL, string $idpLoginRedirectUrl, string $levelOfAssurance, string $requestedAuthnContextComparison): AuthnRequest
299
    {
300
        ContainerSingleton::setContainer($container);
301
        $request = new AuthnRequest();
302
303
        $issuerImpl = new Issuer();
304
        $issuerImpl->setValue($issuer);
305
306
        $request->setIssuer($issuerImpl);
307
        $request->setId($container->generateId());
308
        $request->setAssertionConsumerServiceURL($assertionConsumerServiceURL);
309
        $request->setDestination($idpLoginRedirectUrl);
310
        $request->setRequestedAuthnContext([
311
            'AuthnContextClassRef' => [$levelOfAssurance],
312
            'Comparison' => $requestedAuthnContextComparison
313
        ]);
314
315
        return $request;
316
    }
317
318
    /**
319
     * Signs given DOMDocument (ie. AuthRequest) with $privateKey, and optionally adds certificate, if provided.
320
     * Signature type (ie. RSA/SHA256) is determined by type of XMLSecurityKey provided
321
     *
322
     * @param DOMElement $document
323
     * @param XMLSecurityKey $privateKey
324
     * @param XMLSecurityKey|null $certificate
325
     * @return DOMElement
326
     */
327
    public static function signDocument(DOMElement $document, XMLSecurityKey $privateKey = null, XMLSecurityKey $certificate = null): DOMElement
328
    {
329
        if (empty($privateKey)) {
330
            $privateKey = self::$own_private_key;
331
        }
332
333
        $insertAfter = $document->firstChild;
334
335
        if ($document->getElementsByTagName('Issuer')->length > 0) {
336
            $insertAfter = $document->getElementsByTagName('Issuer')->item(0)->nextSibling;
337
        }
338
339
        Utils::insertSignature($privateKey, !empty($certificate) ? [$certificate] : [], $document, $insertAfter);
340
341
        return $document;
342
    }
343
344
    /**
345
     * Sets internal private-key from PEM data
346
     *
347
     * @param string $private_key_pem_string
348
     * @param string $algorithm
349
     * @return bool true if set successfully, false if operation failed (exception will not be thrown)
350
     * @throws Exception
351
     */
352
    public static function setOwnPrivateKeyData(string $private_key_pem_string, $algorithm = XMLSecurityKey::RSA_SHA256): bool
353
    {
354
        try {
355
            self::$own_private_key = new XMLSecurityKey($algorithm, ['type' => 'private']);
356
            self::$own_private_key->loadKey($private_key_pem_string, false, false);
357
            self::$own_private_key->signData("abcdef");
358
            return true;
359
        } catch (Exception $e) {
360
            self::$own_private_key = null;
361
            ContainerSingleton::getInstance()->getLogger()->critical('setOwnPrivateKeyData failed: ' . $e->getMessage(), [$e]);
362
            return false;
363
        }
364
    }
365
366
    /**
367
     * Sets internal certificate/public-key from PEM certificate data
368
     *
369
     * @param string $certificate_pem_string
370
     * @param string $algorithm
371
     * @return bool true if set successfully, false if operation failed (exception will not be thrown)
372
     * @throws Exception
373
     */
374
    public static function setOwnCertificatePublicKey(string $certificate_pem_string, $algorithm = XMLSecurityKey::RSA_SHA256): bool
375
    {
376
        try {
377
            self::$own_certificate = new XMLSecurityKey($algorithm, ['type' => 'public']);
378
            self::$own_certificate->loadKey($certificate_pem_string, false, true);
379
            self::$own_certificate->encryptData('abcdef');
380
            return true;
381
        } catch (Exception $e) {
382
            self::$own_certificate = null;
383
            ContainerSingleton::getInstance()->getLogger()->critical('setOwnCertificatePublicKey failed: ' . $e->getMessage(), [$e]);
384
            return false;
385
        }
386
    }
387
388
    /**
389
     * @param DOMElement $element
390
     * @return string
391
     * @throws Exception
392
     */
393
    public static function getSSORedirectUrl(DOMElement $element): string
394
    {
395
        return self::getSAMLRequestUrl($element, self::extractSSOLoginUrls()[Constants::BINDING_HTTP_REDIRECT]);
396
    }
397
398
    /**
399
     * @param DOMElement $element
400
     * @param string $base_request_url
401
     * @return string
402
     */
403
    public static function getSAMLRequestUrl(DOMElement $element, $base_request_url): string
404
    {
405
        $encoded_element = $element->ownerDocument->saveXML();
406
        $encoded_element = gzdeflate($encoded_element);
407
        $encoded_element = base64_encode($encoded_element);
408
        $encoded_element = urlencode($encoded_element);
409
410
        return $base_request_url . (parse_url($base_request_url, PHP_URL_QUERY) ? '&' : '?') . 'SAMLRequest=' . $encoded_element;
411
    }
412
413
    /**
414
     * Returns associative array of found Login URLs
415
     * keys are binding constants, such as Constants::BINDING_HTTP_POST
416
     *
417
     * @param EntityDescriptor|null $idp_descriptor
418
     * @return array
419
     * @throws Exception
420
     * @see Constants
421
     *
422
     */
423
    public static function extractSSOLoginUrls(?EntityDescriptor $idp_descriptor = null): array
424
    {
425
        return self::extractSSOUrls(false, $idp_descriptor);
426
    }
427
428
429
}