1<?php
2/**
3 * This file is part of php-saml.
4 *
5 * (c) OneLogin Inc
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 *
10 * @package OneLogin
11 * @author  OneLogin Inc <saml-info@onelogin.com>
12 * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE
13 * @link    https://github.com/onelogin/php-saml
14 */
15
16namespace OneLogin\Saml2;
17
18use DOMDocument;
19use Exception;
20
21/**
22 * IdP Metadata Parser of OneLogin PHP Toolkit
23 */
24class IdPMetadataParser
25{
26    /**
27     * Get IdP Metadata Info from URL
28     *
29     * @param string $url                 URL where the IdP metadata is published
30     * @param string $entityId            Entity Id of the desired IdP, if no
31     *                                    entity Id is provided and the XML
32     *                                    metadata contains more than one
33     *                                    IDPSSODescriptor, the first is returned
34     * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
35     * @param string $desiredSSOBinding   Parse specific binding SSO endpoint
36     * @param string $desiredSLOBinding   Parse specific binding SLO endpoint
37     *
38     * @return array metadata info in php-saml settings format
39     */
40    public static function parseRemoteXML($url, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT)
41    {
42        $metadataInfo = array();
43
44        try {
45            $ch = curl_init($url);
46            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
47            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
48            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
49            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
50            curl_setopt($ch, CURLOPT_FAILONERROR, 1);
51
52            $xml = curl_exec($ch);
53            if ($xml !== false) {
54                $metadataInfo = self::parseXML($xml, $entityId, $desiredNameIdFormat, $desiredSSOBinding, $desiredSLOBinding);
55            } else {
56                throw new Exception(curl_error($ch), curl_errno($ch));
57            }
58        } catch (Exception $e) {
59            throw new Exception('Error on parseRemoteXML. '.$e->getMessage());
60        }
61        return $metadataInfo;
62    }
63
64    /**
65     * Get IdP Metadata Info from File
66     *
67     * @param string $filepath            File path
68     * @param string $entityId            Entity Id of the desired IdP, if no
69     *                                    entity Id is provided and the XML
70     *                                    metadata contains more than one
71     *                                    IDPSSODescriptor, the first is returned
72     * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
73     * @param string $desiredSSOBinding   Parse specific binding SSO endpoint
74     * @param string $desiredSLOBinding   Parse specific binding SLO endpoint
75     *
76     * @return array metadata info in php-saml settings format
77     */
78    public static function parseFileXML($filepath, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT)
79    {
80        $metadataInfo = array();
81
82        try {
83            if (file_exists($filepath)) {
84                $data = file_get_contents($filepath);
85                $metadataInfo = self::parseXML($data, $entityId, $desiredNameIdFormat, $desiredSSOBinding, $desiredSLOBinding);
86            }
87        } catch (Exception $e) {
88            throw new Exception('Error on parseFileXML. '.$e->getMessage());
89        }
90        return $metadataInfo;
91    }
92
93    /**
94     * Get IdP Metadata Info from URL
95     *
96     * @param string $xml                 XML that contains IdP metadata
97     * @param string $entityId            Entity Id of the desired IdP, if no
98     *                                    entity Id is provided and the XML
99     *                                    metadata contains more than one
100     *                                    IDPSSODescriptor, the first is returned
101     * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat
102     * @param string $desiredSSOBinding   Parse specific binding SSO endpoint
103     * @param string $desiredSLOBinding   Parse specific binding SLO endpoint
104     *
105     * @return array metadata info in php-saml settings format
106     *
107     * @throws Exception
108     */
109    public static function parseXML($xml, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT)
110    {
111        $metadataInfo = array();
112
113        $dom = new DOMDocument();
114        $dom->preserveWhiteSpace = false;
115        $dom->formatOutput = true;
116        try {
117            $dom = Utils::loadXML($dom, $xml);
118            if (!$dom) {
119                throw new Exception('Error parsing metadata');
120            }
121
122            $customIdPStr = '';
123            if (!empty($entityId)) {
124                $customIdPStr = '[@entityID="' . $entityId . '"]';
125            }
126            $idpDescryptorXPath = '//md:EntityDescriptor' . $customIdPStr . '/md:IDPSSODescriptor';
127
128            $idpDescriptorNodes = Utils::query($dom, $idpDescryptorXPath);
129
130            if (isset($idpDescriptorNodes) && $idpDescriptorNodes->length > 0) {
131                $metadataInfo['idp'] = array();
132
133                $idpDescriptor = $idpDescriptorNodes->item(0);
134
135                if (empty($entityId) && $idpDescriptor->parentNode->hasAttribute('entityID')) {
136                    $entityId = $idpDescriptor->parentNode->getAttribute('entityID');
137                }
138
139                if (!empty($entityId)) {
140                    $metadataInfo['idp']['entityId'] = $entityId;
141                }
142
143                $ssoNodes = Utils::query($dom, './md:SingleSignOnService[@Binding="'.$desiredSSOBinding.'"]', $idpDescriptor);
144                if ($ssoNodes->length < 1) {
145                    $ssoNodes = Utils::query($dom, './md:SingleSignOnService', $idpDescriptor);
146                }
147                if ($ssoNodes->length > 0) {
148                    $metadataInfo['idp']['singleSignOnService'] = array(
149                        'url' => $ssoNodes->item(0)->getAttribute('Location'),
150                        'binding' => $ssoNodes->item(0)->getAttribute('Binding')
151                    );
152                }
153
154                $sloNodes = Utils::query($dom, './md:SingleLogoutService[@Binding="'.$desiredSLOBinding.'"]', $idpDescriptor);
155                if ($sloNodes->length < 1) {
156                    $sloNodes = Utils::query($dom, './md:SingleLogoutService', $idpDescriptor);
157                }
158                if ($sloNodes->length > 0) {
159                    $metadataInfo['idp']['singleLogoutService'] = array(
160                        'url' => $sloNodes->item(0)->getAttribute('Location'),
161                        'binding' => $sloNodes->item(0)->getAttribute('Binding')
162                    );
163
164                    if ($sloNodes->item(0)->hasAttribute('ResponseLocation')) {
165                        $metadataInfo['idp']['singleLogoutService']['responseUrl'] = $sloNodes->item(0)->getAttribute('ResponseLocation');
166                    }
167                }
168
169                $keyDescriptorCertSigningNodes = Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "encryption"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor);
170
171                $keyDescriptorCertEncryptionNodes = Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "signing"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor);
172
173                if (!empty($keyDescriptorCertSigningNodes) || !empty($keyDescriptorCertEncryptionNodes)) {
174                    $metadataInfo['idp']['x509certMulti'] = array();
175                    if (!empty($keyDescriptorCertSigningNodes)) {
176                        $idpInfo['x509certMulti']['signing'] = array();
177                        foreach ($keyDescriptorCertSigningNodes as $keyDescriptorCertSigningNode) {
178                            $metadataInfo['idp']['x509certMulti']['signing'][] = Utils::formatCert($keyDescriptorCertSigningNode->nodeValue, false);
179                        }
180                    }
181                    if (!empty($keyDescriptorCertEncryptionNodes)) {
182                        $idpInfo['x509certMulti']['encryption'] = array();
183                        foreach ($keyDescriptorCertEncryptionNodes as $keyDescriptorCertEncryptionNode) {
184                            $metadataInfo['idp']['x509certMulti']['encryption'][] = Utils::formatCert($keyDescriptorCertEncryptionNode->nodeValue, false);
185                        }
186                    }
187
188                    $idpCertdata = $metadataInfo['idp']['x509certMulti'];
189                    if ((count($idpCertdata) == 1 and
190                         ((isset($idpCertdata['signing']) and count($idpCertdata['signing']) == 1) or (isset($idpCertdata['encryption']) and count($idpCertdata['encryption']) == 1))) or
191                         ((isset($idpCertdata['signing']) && count($idpCertdata['signing']) == 1) && isset($idpCertdata['encryption']) && count($idpCertdata['encryption']) == 1 && strcmp($idpCertdata['signing'][0], $idpCertdata['encryption'][0]) == 0)) {
192                        if (isset($metadataInfo['idp']['x509certMulti']['signing'][0])) {
193                            $metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['signing'][0];
194                        } else {
195                            $metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['encryption'][0];
196                        }
197                        unset($metadataInfo['idp']['x509certMulti']);
198                    }
199                }
200
201                $nameIdFormatNodes = Utils::query($dom, './md:NameIDFormat', $idpDescriptor);
202                if ($nameIdFormatNodes->length > 0) {
203                    $metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNodes->item(0)->nodeValue;
204                    if (!empty($desiredNameIdFormat)) {
205                        foreach ($nameIdFormatNodes as $nameIdFormatNode) {
206                            if (strcmp($nameIdFormatNode->nodeValue, $desiredNameIdFormat) == 0) {
207                                $metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNode->nodeValue;
208                                break;
209                            }
210                        }
211                    }
212                }
213            }
214        } catch (Exception $e) {
215            throw new Exception('Error parsing metadata. '.$e->getMessage());
216        }
217
218        return $metadataInfo;
219    }
220
221    /**
222     * Inject metadata info into php-saml settings array
223     *
224     * @param array $settings     php-saml settings array
225     * @param array $metadataInfo array metadata info
226     *
227     * @return array settings
228     */
229    public static function injectIntoSettings($settings, $metadataInfo)
230    {
231        if (isset($metadataInfo['idp']) && isset($settings['idp'])) {
232            if (isset($metadataInfo['idp']['x509certMulti']) && !empty($metadataInfo['idp']['x509certMulti']) && isset($settings['idp']['x509cert'])) {
233                unset($settings['idp']['x509cert']);
234            }
235
236            if (isset($metadataInfo['idp']['x509cert']) && !empty($metadataInfo['idp']['x509cert']) && isset($settings['idp']['x509certMulti'])) {
237                unset($settings['idp']['x509certMulti']);
238            }
239        }
240
241        return array_replace_recursive($settings, $metadataInfo);
242    }
243}
244