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