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