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 RobRichards\XMLSecLibs\XMLSecurityKey;
19use RobRichards\XMLSecLibs\XMLSecurityDSig;
20
21use DOMDocument;
22use Exception;
23
24/**
25 * Metadata lib of OneLogin PHP Toolkit
26 */
27class Metadata
28{
29    const TIME_VALID = 172800;  // 2 days
30    const TIME_CACHED = 604800; // 1 week
31
32    /**
33     * Generates the metadata of the SP based on the settings
34     *
35     * @param array         $sp            The SP data
36     * @param bool|string   $authnsign     authnRequestsSigned attribute
37     * @param bool|string   $wsign         wantAssertionsSigned attribute
38     * @param int|null      $validUntil    Metadata's valid time
39     * @param int|null      $cacheDuration Duration of the cache in seconds
40     * @param array         $contacts      Contacts info
41     * @param array         $organization  Organization ingo
42     * @param array         $attributes
43     *
44     * @return string SAML Metadata XML
45     */
46    public static function builder($sp, $authnsign = false, $wsign = false, $validUntil = null, $cacheDuration = null, $contacts = array(), $organization = array(), $attributes = array())
47    {
48
49        if (!isset($validUntil)) {
50            $validUntil =  time() + self::TIME_VALID;
51        }
52        $validUntilTime =  Utils::parseTime2SAML($validUntil);
53
54        if (!isset($cacheDuration)) {
55            $cacheDuration = self::TIME_CACHED;
56        }
57
58        $sls = '';
59
60        if (isset($sp['singleLogoutService'])) {
61            $slsUrl = htmlspecialchars($sp['singleLogoutService']['url'], ENT_QUOTES);
62            $sls = <<<SLS_TEMPLATE
63        <md:SingleLogoutService Binding="{$sp['singleLogoutService']['binding']}"
64                                Location="{$slsUrl}" />
65
66SLS_TEMPLATE;
67        }
68
69        if ($authnsign) {
70            $strAuthnsign = 'true';
71        } else {
72            $strAuthnsign = 'false';
73        }
74
75        if ($wsign) {
76            $strWsign = 'true';
77        } else {
78            $strWsign = 'false';
79        }
80
81        $strOrganization = '';
82
83        if (!empty($organization)) {
84            $organizationInfoNames = array();
85            $organizationInfoDisplaynames = array();
86            $organizationInfoUrls = array();
87            foreach ($organization as $lang => $info) {
88                $organizationInfoNames[] = <<<ORGANIZATION_NAME
89       <md:OrganizationName xml:lang="{$lang}">{$info['name']}</md:OrganizationName>
90ORGANIZATION_NAME;
91                $organizationInfoDisplaynames[] = <<<ORGANIZATION_DISPLAY
92       <md:OrganizationDisplayName xml:lang="{$lang}">{$info['displayname']}</md:OrganizationDisplayName>
93ORGANIZATION_DISPLAY;
94                $organizationInfoUrls[] = <<<ORGANIZATION_URL
95       <md:OrganizationURL xml:lang="{$lang}">{$info['url']}</md:OrganizationURL>
96ORGANIZATION_URL;
97            }
98            $orgData = implode("\n", $organizationInfoNames)."\n".implode("\n", $organizationInfoDisplaynames)."\n".implode("\n", $organizationInfoUrls);
99            $strOrganization = <<<ORGANIZATIONSTR
100
101    <md:Organization>
102{$orgData}
103    </md:Organization>
104ORGANIZATIONSTR;
105        }
106
107        $strContacts = '';
108        if (!empty($contacts)) {
109            $contactsInfo = array();
110            foreach ($contacts as $type => $info) {
111                $contactsInfo[] = <<<CONTACT
112    <md:ContactPerson contactType="{$type}">
113        <md:GivenName>{$info['givenName']}</md:GivenName>
114        <md:EmailAddress>{$info['emailAddress']}</md:EmailAddress>
115    </md:ContactPerson>
116CONTACT;
117            }
118            $strContacts = "\n".implode("\n", $contactsInfo);
119        }
120
121        $strAttributeConsumingService = '';
122        if (isset($sp['attributeConsumingService'])) {
123            $attrCsDesc = '';
124            if (isset($sp['attributeConsumingService']['serviceDescription'])) {
125                $attrCsDesc = sprintf(
126                    '            <md:ServiceDescription xml:lang="en">%s</md:ServiceDescription>' . PHP_EOL,
127                    $sp['attributeConsumingService']['serviceDescription']
128                );
129            }
130            if (!isset($sp['attributeConsumingService']['serviceName'])) {
131                $sp['attributeConsumingService']['serviceName'] = 'Service';
132            }
133            $requestedAttributeData = array();
134            foreach ($sp['attributeConsumingService']['requestedAttributes'] as $attribute) {
135                $requestedAttributeStr = sprintf('            <md:RequestedAttribute Name="%s"', $attribute['name']);
136                if (isset($attribute['nameFormat'])) {
137                    $requestedAttributeStr .= sprintf(' NameFormat="%s"', $attribute['nameFormat']);
138                }
139                if (isset($attribute['friendlyName'])) {
140                    $requestedAttributeStr .= sprintf(' FriendlyName="%s"', $attribute['friendlyName']);
141                }
142                if (isset($attribute['isRequired'])) {
143                    $requestedAttributeStr .= sprintf(' isRequired="%s"', $attribute['isRequired'] === true ? 'true' : 'false');
144                }
145                $reqAttrAuxStr = " />";
146
147                if (isset($attribute['attributeValue']) && !empty($attribute['attributeValue'])) {
148                    $reqAttrAuxStr = '>';
149                    if (is_string($attribute['attributeValue'])) {
150                        $attribute['attributeValue'] = array($attribute['attributeValue']);
151                    }
152                    foreach ($attribute['attributeValue'] as $attrValue) {
153                        $reqAttrAuxStr .=<<<ATTRIBUTEVALUE
154
155                <saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">{$attrValue}</saml:AttributeValue>
156ATTRIBUTEVALUE;
157                    }
158                    $reqAttrAuxStr .= "\n            </md:RequestedAttribute>";
159                }
160
161                $requestedAttributeData[] = $requestedAttributeStr . $reqAttrAuxStr;
162            }
163
164            $requestedAttributeStr = implode(PHP_EOL, $requestedAttributeData);
165            $strAttributeConsumingService = <<<METADATA_TEMPLATE
166<md:AttributeConsumingService index="1">
167            <md:ServiceName xml:lang="en">{$sp['attributeConsumingService']['serviceName']}</md:ServiceName>
168{$attrCsDesc}{$requestedAttributeStr}
169        </md:AttributeConsumingService>
170METADATA_TEMPLATE;
171        }
172
173        $spEntityId = htmlspecialchars($sp['entityId'], ENT_QUOTES);
174        $acsUrl = htmlspecialchars($sp['assertionConsumerService']['url'], ENT_QUOTES);
175        $metadata = <<<METADATA_TEMPLATE
176<?xml version="1.0"?>
177<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
178                     validUntil="{$validUntilTime}"
179                     cacheDuration="PT{$cacheDuration}S"
180                     entityID="{$spEntityId}">
181    <md:SPSSODescriptor AuthnRequestsSigned="{$strAuthnsign}" WantAssertionsSigned="{$strWsign}" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
182{$sls}        <md:NameIDFormat>{$sp['NameIDFormat']}</md:NameIDFormat>
183        <md:AssertionConsumerService Binding="{$sp['assertionConsumerService']['binding']}"
184                                     Location="{$acsUrl}"
185                                     index="1" />
186        {$strAttributeConsumingService}
187    </md:SPSSODescriptor>{$strOrganization}{$strContacts}
188</md:EntityDescriptor>
189METADATA_TEMPLATE;
190        return $metadata;
191    }
192
193    /**
194     * Signs the metadata with the key/cert provided
195     *
196     * @param string $metadata        SAML Metadata XML
197     * @param string $key             x509 key
198     * @param string $cert            x509 cert
199     * @param string $signAlgorithm   Signature algorithm method
200     * @param string $digestAlgorithm Digest algorithm method
201     *
202     * @return string Signed Metadata
203     *
204     * @throws Exception
205     */
206    public static function signMetadata($metadata, $key, $cert, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $digestAlgorithm = XMLSecurityDSig::SHA256)
207    {
208        return Utils::addSign($metadata, $key, $cert, $signAlgorithm, $digestAlgorithm);
209    }
210
211    /**
212     * Adds the x509 descriptors (sign/encryption) to the metadata
213     * The same cert will be used for sign/encrypt
214     *
215     * @param string $metadata       SAML Metadata XML
216     * @param string $cert           x509 cert
217     * @param bool   $wantsEncrypted Whether to include the KeyDescriptor for encryption
218     *
219     * @return string Metadata with KeyDescriptors
220     *
221     * @throws Exception
222     */
223    public static function addX509KeyDescriptors($metadata, $cert, $wantsEncrypted = true)
224    {
225        $xml = new DOMDocument();
226        $xml->preserveWhiteSpace = false;
227        $xml->formatOutput = true;
228        try {
229            $xml = Utils::loadXML($xml, $metadata);
230            if (!$xml) {
231                throw new Exception('Error parsing metadata');
232            }
233        } catch (Exception $e) {
234            throw new Exception('Error parsing metadata. '.$e->getMessage());
235        }
236
237        $formatedCert = Utils::formatCert($cert, false);
238        $x509Certificate = $xml->createElementNS(Constants::NS_DS, 'X509Certificate', $formatedCert);
239
240        $keyData = $xml->createElementNS(Constants::NS_DS, 'ds:X509Data');
241        $keyData->appendChild($x509Certificate);
242
243        $keyInfo = $xml->createElementNS(Constants::NS_DS, 'ds:KeyInfo');
244        $keyInfo->appendChild($keyData);
245
246        $keyDescriptor = $xml->createElementNS(Constants::NS_MD, "md:KeyDescriptor");
247
248        $SPSSODescriptor = $xml->getElementsByTagName('SPSSODescriptor')->item(0);
249        $SPSSODescriptor->insertBefore($keyDescriptor->cloneNode(), $SPSSODescriptor->firstChild);
250        if ($wantsEncrypted === true) {
251            $SPSSODescriptor->insertBefore($keyDescriptor->cloneNode(), $SPSSODescriptor->firstChild);
252        }
253
254        $signing = $xml->getElementsByTagName('KeyDescriptor')->item(0);
255        $signing->setAttribute('use', 'signing');
256        $signing->appendChild($keyInfo);
257
258        if ($wantsEncrypted === true) {
259            $encryption = $xml->getElementsByTagName('KeyDescriptor')->item(1);
260            $encryption->setAttribute('use', 'encryption');
261
262            $encryption->appendChild($keyInfo->cloneNode(true));
263        }
264
265        return $xml->saveXML();
266    }
267}
268