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