Completed
Push — master ( cf2bb8...48f39c )
by Marek
02:05
created

OMSAML2::setIdPMetadataContents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 4
rs 10
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
introduced by
$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, will be retrieved internally by configured URL
186
     * @return bool
187
     * @throws Exception
188
     */
189
    public static function validateSignature(XMLSecurityKey $publicKey, ?SignedElement $idp_descriptor = null): bool
190
    {
191
        if (empty($idp_descriptor)) {
192
            $idp_descriptor = self::getIdpDescriptor();
193
        }
194
195
        return $idp_descriptor->validate($publicKey);
196
    }
197
198
    /**
199
     * Creates public key for verifying RSA/SHA256 signature
200
     *
201
     * @param string $path absolute path to certificate file or URL from which it can be retrieved
202
     * @param string $algorithm
203
     * @param string $type
204
     * @return XMLSecurityKey
205
     * @throws Exception
206
     */
207
    public static function getPublicKeyFromCertificate(string $path, $algorithm = XMLSecurityKey::RSA_SHA256, $type = 'public'): XMLSecurityKey
208
    {
209
        $cert_data = file_get_contents($path);
210
        $key = new XMLSecurityKey($algorithm, ['type' => $type]);
211
        $key->loadKey($cert_data, false, true);
212
        return $key;
213
    }
214
215
    /**
216
     * Create private key for verifying RSA/SHA256 signature
217
     *
218
     * @param string $path absolute path to key file or URL from which it can be retrieved
219
     * @param string $algorithm
220
     * @param string $type
221
     * @return XMLSecurityKey
222
     * @throws Exception
223
     */
224
    public static function getPrivateKeyFromFile(string $path, $algorithm = XMLSecurityKey::RSA_SHA256, $type = 'private'): XMLSecurityKey
225
    {
226
        $key_data = file_get_contents($path);
227
        $key = new XMLSecurityKey($algorithm, ['type' => $type]);
228
        $key->loadKey($key_data, false, false);
229
        return $key;
230
    }
231
232
    /**
233
     * Generates AuthRequest/AuthnRequest using provided information
234
     *
235
     * @param AbstractContainer $container
236
     * @param string $issuer
237
     * @param string $assertionConsumerServiceURL
238
     * @param string $idpLoginRedirectUrl
239
     * @param string $levelOfAssurance
240
     * @param string $requestedAuthnContextComparison
241
     * @return AuthnRequest
242
     * @throws Exception
243
     */
244
    public static function generateAuthRequest(AbstractContainer $container, string $issuer, string $assertionConsumerServiceURL, string $idpLoginRedirectUrl, string $levelOfAssurance, string $requestedAuthnContextComparison): AuthnRequest
245
    {
246
        ContainerSingleton::setContainer($container);
247
        $request = new AuthnRequest();
248
249
        $issuerImpl = new Issuer();
250
        $issuerImpl->setValue($issuer);
251
252
        $request->setIssuer($issuerImpl);
253
        $request->setId($container->generateId());
254
        $request->setAssertionConsumerServiceURL($assertionConsumerServiceURL);
255
        $request->setDestination($idpLoginRedirectUrl);
256
        $request->setRequestedAuthnContext([
257
            'AuthnContextClassRef' => [$levelOfAssurance],
258
            'Comparison' => $requestedAuthnContextComparison
259
        ]);
260
261
        return $request;
262
    }
263
264
    /**
265
     * Signs given DOMDocument (ie. AuthRequest) with $privateKey, and optionally adds certificate, if provided.
266
     * Signature type (ie. RSA/SHA256) is determined by type of XMLSecurityKey provided
267
     *
268
     * @param DOMElement $document
269
     * @param XMLSecurityKey $privateKey
270
     * @param XMLSecurityKey|null $certificate
271
     * @return DOMElement
272
     */
273
    public static function signDocument(DOMElement $document, XMLSecurityKey $privateKey = null, XMLSecurityKey $certificate = null): DOMElement
274
    {
275
        if (empty($privateKey)) {
276
            $privateKey = self::$own_private_key;
277
        }
278
279
        $insertAfter = $document->firstChild;
280
281
        if ($document->getElementsByTagName('Issuer')->length > 0) {
282
            $insertAfter = $document->getElementsByTagName('Issuer')->item(0)->nextSibling;
283
        }
284
285
        Utils::insertSignature($privateKey, !empty($certificate) ? [$certificate] : [], $document, $insertAfter);
286
287
        return $document;
288
    }
289
290
    /**
291
     * Sets internal private-key from PEM data
292
     *
293
     * @param string $private_key_pem_string
294
     * @param string $algorithm
295
     * @return bool true if set successfully, false if operation failed (exception will not be thrown)
296
     * @throws Exception
297
     */
298
    public static function setOwnPrivateKeyData(string $private_key_pem_string, $algorithm = XMLSecurityKey::RSA_SHA256): bool
299
    {
300
        try {
301
            self::$own_private_key = new XMLSecurityKey($algorithm, ['type' => 'private']);
302
            self::$own_private_key->loadKey($private_key_pem_string, false, false);
303
            self::$own_private_key->signData("abcdef");
304
            return true;
305
        } catch (Exception $e) {
306
            self::$own_private_key = null;
307
            return false;
308
        }
309
    }
310
311
    /**
312
     * Sets internal certificate/public-key from PEM certificate data
313
     *
314
     * @param string $certificate_pem_string
315
     * @param string $algorithm
316
     * @return bool true if set successfully, false if operation failed (exception will not be thrown)
317
     * @throws Exception
318
     */
319
    public static function setOwnCertificatePublicKey(string $certificate_pem_string, $algorithm = XMLSecurityKey::RSA_SHA256): bool
320
    {
321
        try {
322
            self::$own_certificate = new XMLSecurityKey($algorithm, ['type' => 'public']);
323
            self::$own_certificate->loadKey($certificate_pem_string, false, true);
324
            return true;
325
        } catch (Exception $e) {
326
            self::$own_certificate = null;
327
            return false;
328
        }
329
    }
330
331
    /**
332
     * Returns stored certificate/public-key in form of XMLSecurityKey
333
     *
334
     * @return XMLSecurityKey
335
     */
336
    public static function getOwnCertificatePublicKey(): XMLSecurityKey
337
    {
338
        return self::$own_certificate;
339
    }
340
341
    /**
342
     * @param DOMElement $element
343
     * @return string
344
     * @throws Exception
345
     */
346
    public static function getSSORedirectUrl(DOMElement $element): string
347
    {
348
        return self::getSAMLRequestUrl($element, self::extractSSOLoginUrls()[Constants::BINDING_HTTP_REDIRECT]);
349
    }
350
351
    /**
352
     * @param DOMElement $element
353
     * @param $base_request_url
354
     * @return string
355
     */
356
    public static function getSAMLRequestUrl(DOMElement $element, $base_request_url): string
357
    {
358
        $encoded_element = $element->ownerDocument->saveXML();
359
        $encoded_element = gzdeflate($encoded_element);
360
        $encoded_element = base64_encode($encoded_element);
361
        $encoded_element = urlencode($encoded_element);
362
363
        return $base_request_url . (parse_url($base_request_url, PHP_URL_QUERY) ? '&' : '?') . 'SAMLRequest=' . $encoded_element;
364
    }
365
366
    /**
367
     * Returns associative array of found Login URLs
368
     * keys are binding constants, such as Constants::BINDING_HTTP_POST
369
     *
370
     * @param EntityDescriptor|null $idp_descriptor
371
     * @return array
372
     * @throws Exception
373
     * @see Constants
374
     *
375
     */
376
    public static function extractSSOLoginUrls(?EntityDescriptor $idp_descriptor = null): array
377
    {
378
        return self::extractSSOUrls(false, $idp_descriptor);
379
    }
380
381
382
}