Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
SAMLBuilder.php 20.89 KiB
<?php

/**
 * Class for generating SAML 2.0 metadata from simpleSAMLphp metadata arrays.
 *
 * This class builds SAML 2.0 metadata for an entity by examining the metadata for the entity.
 *
 * @package simpleSAMLphp
 * @version $Id$
 */
class SimpleSAML_Metadata_SAMLBuilder {



	/**
	 * The DOMDocument we are working in.
	 */
	private $document;


	/**
	 * The EntityDescriptor we are building.
	 */
	private $entityDescriptor;


	private $maxCache = NULL;
	private $maxDuration = NULL;
	
	/**
	 * Initialize the builder.
	 *
	 * @param string $entityId  The entity id of the entity.
	 */
	public function __construct($entityId, $maxCache = NULL, $maxDuration = NULL) {
		assert('is_string($entityId)');

		$this->maxCache = $maxCache;
		$this->maxDuration = $maxDuration;

		$this->document = new DOMDocument();
		
		$this->entityDescriptor = $this->createElement('EntityDescriptor');
#		$this->entityDescriptor->setAttribute('xmlns:xml', 'http://www.w3.org/XML/1998/namespace');
		$this->entityDescriptor->setAttribute('entityID', $entityId);
		
		$this->document->appendChild($this->entityDescriptor);
	}

	private function setExpiration($metadata) {
	
		if (array_key_exists('expire', $metadata)) {
			if ($metadata['expire'] - time() < $this->maxDuration)
				$this->maxDuration = $metadata['expire'] - time();
		}
			
		if ($this->maxCache !== NULL) 
			$this->entityDescriptor->setAttribute('cacheDuration', 'PT' . $this->maxCache . 'S');
		if ($this->maxDuration !== NULL) 
			$this->entityDescriptor->setAttribute('validUntil', SimpleSAML_Utilities::generateTimestamp(time() + $this->maxDuration));
	}


	/**
	 * Retrieve the EntityDescriptor.
	 *
	 * Retrieve the EntityDescriptor element which is generated for this entity.
	 * @return DOMElement  The EntityDescriptor element for this entity.
	 */
	public function getEntityDescriptor() {
		return $this->entityDescriptor;
	}


	/**
	 * Retrieve the EntityDescriptor as text.
	 *
	 * This function serializes this EntityDescriptor, and returns it as text.
	 *
	 * @param bool $formatted  Whether the returned EntityDescriptor should be
	 *                         formatted first.
	 * @return string  The serialized EntityDescriptor.
	 */
	public function getEntityDescriptorText($formatted = TRUE) {
		assert('is_bool($formatted)');

		if ($formatted) {
			SimpleSAML_Utilities::formatDOMElement($this->entityDescriptor);
		}

		return $this->document->saveXML();
	}
	
	/**
	 * @param SimpleSAML_Configuration $metadata  Metadata.
	 * @param $e Reference to the element where the Extensions element should be included.
	 */
	private function addExtensions(SimpleSAML_Configuration $metadata, &$e = NULL) {
		$extensions = $this->createElement('Extensions'); 
		$includeExtensions = FALSE;
		
		if ($metadata->hasValue('tags')) {
			$includeExtensions = TRUE;
			$attr = $this->createElement('saml:Attribute', 'urn:oasis:names:tc:SAML:2.0:assertion');
			$attr->setAttribute('Name', 'tags');
			foreach ($metadata->getArray('tags') as $tag) {
				$attr->appendChild($this->createTextElement('saml:AttributeValue', $tag, 'urn:oasis:names:tc:SAML:2.0:assertion'));
			}
			$extensions->appendChild($attr);
		}

		if ($metadata->hasValue('hint.cidr')) {
			$includeExtensions = TRUE;
			$attr = $this->createElement('saml:Attribute', 'urn:oasis:names:tc:SAML:2.0:assertion');
			$attr->setAttribute('Name', 'hint.cidr');
			foreach ($metadata->getArrayizeString('hint.cidr') as $hint) {
				$attr->appendChild($this->createTextElement('saml:AttributeValue', $hint, 'urn:oasis:names:tc:SAML:2.0:assertion'));
			}
			$extensions->appendChild($attr);
		}

		
		if ($metadata->hasValue('scope')) {
			$includeExtensions = TRUE;
			foreach ($metadata->getArray('scope') as $scopetext) {
				$scope = $this->createElement('shibmd:Scope', 'urn:mace:shibboleth:metadata:1.0');
				$scope->setAttribute('regexp', 'false');
				$scope->appendChild($this->document->createTextNode($scopetext));
				$extensions->appendChild($scope);
			}
		}
		if ($includeExtensions) {
			if (isset($e)) {
				$e->appendChild($extensions);
			} else {
				$this->entityDescriptor->appendChild($extensions);
			}
		}
	}

	/**
	 * Add Organization element.
	 *
	 * This function adds an organization element to the metadata.
	 *
	 * @param array $orgName  An array with the localized OrganizatioName.
	 * @param array $orgDisplayName  An array with the localized OrganizatioDisplayName.
	 * @param array $orgURL  An array with the localized OrganizatioURL.
	 */
	public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL) {

		$org = $this->createElement('Organization');

		foreach ($orgName AS $lang => $localname) {
			$e = $this->createTextElement('OrganizationName', $localname);
			$e->setAttribute('xml:lang', $lang);
			$org->appendChild($e);
		}

		foreach ($orgDisplayName AS $lang => $localname) {
			$e = $this->createTextElement('OrganizationDisplayName', $localname);
			$e->setAttribute('xml:lang', $lang);
			$org->appendChild($e);
		}

		foreach ($orgURL AS $lang => $locallink) {
			$e = $this->createTextElement('OrganizationURL', $locallink);
			$e->setAttribute('xml:lang', $lang);
			$org->appendChild($e);
		}

		$this->entityDescriptor->appendChild($org);
	}

	
	public function addOrganizationInfo($metadata) {
		if (array_key_exists('name', $metadata)) {

			if (is_array($metadata['name'])) {
				$name = $metadata['name'];
			} else {
				$name = array('en' => $metadata['name']);
			}


			if (!array_key_exists('url', $metadata)) {
				/*
				 * The specification requires an OrganizationURL element, but
				 * we haven't got an URL. Insert an empty element instead.
				 */
				$url = array('en' => '');
			} elseif (is_array($metadata['url'])) {
				$url = $metadata['url'];
			} else {
				$url = array('en' => $metadata['url']);
			}

			$this->addOrganization($name, $name, $url);
		}
	}
	

	/**
	 * Add endpoint list to metadata.
	 *
	 * @param DOMElement $ssoDesc  The *SSODescriptor element.
	 * @param string $endpointType  The endpoint type (e.g. 'SingleLogoutService').
	 * @param array $endpoints  The endpoints.
	 */
	private function addEndpoints(DOMElement $ssoDesc, $endpointType, array $endpoints) {
		assert('is_string($endpointType)');

		switch ($endpointType) {
		case 'ArtifactResolutionService':
		case 'AssertionConsumerService':
			$indexed = TRUE;
			break;
		case 'AssertionIDRequestService':
		case 'AttributeService':
		case 'AuthnQueryService':
		case 'AuthzService':
		case 'ManageNameIDService':
		case 'NameIDMappingService':
		case 'SingleLogoutService':
		case 'SingleSignOnService':
			$indexed = FALSE;
			break;
		default:
			throw new SimpleSAML_Error_Exception('TODO: Add endpoint type: ' . var_export($endpointType, TRUE));
		}

		foreach ($endpoints as &$ep) {
			$t = $this->createElement($endpointType);
			$t->setAttribute('Binding', $ep['Binding']);
			$t->setAttribute('Location', $ep['Location']);
			if (isset($ep['ResponseLocation'])) {
				$t->setAttribute('ResponseLocation', $ep['ResponseLocation']);
			}

			if ($indexed) {
				if (!isset($ep['index'])) {
					/* Find the maximum index. */
					$maxIndex = -1;
					foreach ($endpoints as $ep) {
						if (!isset($ep['index'])) {
							continue;
						}

						if ($ep['index'] > $maxIndex) {
							$maxIndex = $ep['index'];
						}
					}

					$ep['index'] = $maxIndex + 1;
				}

				$t->setAttribute('index', (string)$ep['index']);
			}

			$ssoDesc->appendChild($t);
		}

	}


	/**
	 * Add metadata set for entity.
	 *
	 * This function is used to add a metadata array to the entity.
	 *
	 * @param string $set  The metadata set this metadata comes from.
	 * @param array $metadata  The metadata.
	 */
	public function addMetadata($set, $metadata) {
		assert('is_string($set)');
		assert('is_array($metadata)');
		
		$this->setExpiration($metadata);
		switch ($set) {
		case 'saml20-sp-remote':
			$this->addMetadataSP20($metadata);
			break;
		case 'saml20-idp-remote':
			$this->addMetadataIdP20($metadata);
			break;
		case 'shib13-sp-remote':
			$this->addMetadataSP11($metadata);
			break;
		case 'shib13-idp-remote':
			$this->addMetadataIdP11($metadata);
			break;
		case 'attributeauthority-remote':
			$this->addAttributeAuthority($metadata);
			break;
		default:
			SimpleSAML_Logger::warning('Unable to generate metadata for unknown type \'' . $set . '\'.');
		}
		
	}

	/**
	 * Add SAML 2.0 SP metadata.
	 *
	 * @param array $metadata  The metadata.
	 */
	public function addMetadataSP20($metadata) {
		assert('is_array($metadata)');
		assert('isset($metadata["entityid"])');
		assert('isset($metadata["metadata-set"])');

		$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);

		$e = $this->createElement('SPSSODescriptor');
		$e->setAttribute('protocolSupportEnumeration', 'urn:oasis:names:tc:SAML:2.0:protocol');


		$this->addExtensions($metadata, $e);

		$this->addCertificate($e, $metadata);

		$this->addEndpoints($e, 'SingleLogoutService', $metadata->getEndpoints('SingleLogoutService'));

		if ($metadata->hasValue('NameIDFormat')) {
			$t = $this->createElement('NameIDFormat');
			$t->appendChild($this->document->createTextNode($metadata->getString('NameIDFormat')));
			$e->appendChild($t);
		}

		$endpoints = $metadata->getEndpoints('AssertionConsumerService');
		foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
			$endpoints[] = array(
				'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
				'Location' => $acs,
			);
		}
		$this->addEndpoints($e, 'AssertionConsumerService', $endpoints);

		$attributes = $metadata->getArray('attributes', array());
		if ( $metadata->hasValue('name') && count($attributes) > 0 ) {
			/*
			 * Add an AttributeConsumingService element with information as name and description and list
			 * of requested attributes
			 */
			$attributeconsumer = $this->createElement('AttributeConsumingService');
			$attributeconsumer->setAttribute('index', '0');

			$name = $metadata->getLocalizedString('name');
			foreach($name AS $lang => $localname) {
				$t = $this->createTextElement('ServiceName', $localname);
				$t->setAttribute('xml:lang', $lang);
				$attributeconsumer->appendChild($t);
			}

			$description = $metadata->getLocalizedString('description', array());
			foreach ($description as $lang => $localname) {
				$t = $this->createTextElement('ServiceDescription', $localname);
				$t->setAttribute('xml:lang', $lang);
				$attributeconsumer->appendChild($t);
			}


			foreach ($attributes as $attribute) {
				$t = $this->createElement('RequestedAttribute');
				$t->setAttribute('Name', $attribute);
				$attributeconsumer->appendChild($t);
			}

			$e->appendChild($attributeconsumer);
		}

		$this->entityDescriptor->appendChild($e);

		foreach ($metadata->getArray('contacts', array()) as $contact) {
			if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
				$this->addContact($contact['contactType'], $contact);
			}
		}

	}


	/**
	 * Add SAML 2.0 IdP metadata.
	 *
	 * @param array $metadata  The metadata.
	 */
	public function addMetadataIdP20($metadata) {
		assert('is_array($metadata)');
		assert('isset($metadata["entityid"])');
		assert('isset($metadata["metadata-set"])');

		$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);

		$e = $this->createElement('IDPSSODescriptor');
		$e->setAttribute('protocolSupportEnumeration', 'urn:oasis:names:tc:SAML:2.0:protocol');

		if ($metadata->getBoolean('redirect.sign', FALSE)) {
			$e->setAttribute('WantAuthnRequestSigned', 'true');
		}

		$this->addExtensions($metadata, $e);

		$this->addCertificate($e, $metadata);

		$this->addEndpoints($e, 'SingleLogoutService', $metadata->getEndpoints('SingleLogoutService'));

		if ($metadata->hasValue('NameIDFormat')) {
			$t = $this->createElement('NameIDFormat');
			$t->appendChild($this->document->createTextNode($metadata->getString('NameIDFormat')));
			$e->appendChild($t);
		}

		if ($metadata->hasValue('ArtifactResolutionService')){
			$this->addEndpoints($e, 'ArtifactResolutionService', $metadata->getEndpoints('ArtifactResolutionService'));
		}

		$this->addEndpoints($e, 'SingleSignOnService', $metadata->getEndpoints('SingleSignOnService'));
		$this->entityDescriptor->appendChild($e);

		foreach ($metadata->getArray('contacts', array()) as $contact) {
			if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
				$this->addContact($contact['contactType'], $contact);
			}
		}

	}


	/**
	 * Add SAML 1.1 SP metadata.
	 *
	 * @param array $metadata  The metadata.
	 */
	public function addMetadataSP11($metadata) {
		assert('is_array($metadata)');
		assert('isset($metadata["entityid"])');
		assert('isset($metadata["metadata-set"])');

		$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);

		$e = $this->createElement('SPSSODescriptor');
		$e->setAttribute('protocolSupportEnumeration', 'urn:oasis:names:tc:SAML:1.1:protocol');

		$this->addCertificate($e, $metadata);

		if ($metadata->hasValue('NameIDFormat')) {
			$t = $this->createElement('NameIDFormat');
			$t->appendChild($this->document->createTextNode($metadata->getString('NameIDFormat')));
			$e->appendChild($t);
		}

		$endpoints = $metadata->getEndpoints('AssertionConsumerService');
		foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
			$endpoints[] = array(
				'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
				'Location' => $acs,
			);
		}
		$this->addEndpoints($e, 'AssertionConsumerService', $endpoints);

		$this->entityDescriptor->appendChild($e);
	}


	/**
	 * Add SAML 1.1 IdP metadata.
	 *
	 * @param array $metadata  The metadata.
	 */
	public function addMetadataIdP11($metadata) {
		assert('is_array($metadata)');
		assert('isset($metadata["entityid"])');
		assert('isset($metadata["metadata-set"])');

		$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);

		$e = $this->createElement('IDPSSODescriptor');
		$e->setAttribute('protocolSupportEnumeration', 'urn:oasis:names:tc:SAML:1.1:protocol');

		$this->addCertificate($e, $metadata);

		if ($metadata->hasValue('NameIDFormat')) {
			$t = $this->createElement('NameIDFormat');
			$t->appendChild($this->document->createTextNode($metadata->getString('NameIDFormat')));
			$e->appendChild($t);
		}
		$this->addEndpoints($e, 'SingleSignOnService', $metadata->getEndpoints('SingleSignOnService'));

		$this->entityDescriptor->appendChild($e);
	}


	/**
	 * Add a AttributeAuthorityDescriptor.
	 *
	 * @param array $metadata  The AttributeAuthorityDescriptor, in the format returned by SAMLParser.
	 */
	public function addAttributeAuthority(array $metadata) {
		assert('is_array($metadata)');
		assert('isset($metadata["entityid"])');
		assert('isset($metadata["metadata-set"])');

		$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);

		$e = $this->createElement('AttributeAuthorityDescriptor');
		$e->setAttribute('protocolSupportEnumeration', implode(' ', $metadata->getArray('protocols', array())));

		$this->addExtensions($metadata, $e);
		$this->addCertificate($e, $metadata);

		$this->addEndpoints($e, 'AttributeService', $metadata->getEndpoints('AttributeService'));
		$this->addEndpoints($e, 'AssertionIDRequestService', $metadata->getEndpoints('AssertionIDRequestService'));

		foreach ($metadata->getArray('NameIDFormat', array()) as $format) {
			$t = $this->createElement('NameIDFormat');
			$t->appendChild($this->document->createTextNode($format));
			$e->appendChild($t);
		}

		$this->entityDescriptor->appendChild($e);
	}


	/**
	 * Add contact information.
	 *
	 * Accepts a contact type, and an array of the following elements (all are optional):
	 * - emailAddress     Email address (as string), or array of email addresses.
	 * - telephoneNumber  Telephone number of contact (as string), or array of telephone numbers.
	 * - name             Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>.
	 * - surName          Surname of contact.
	 * - givenName        Givenname of contact.
	 * - company          Company name of contact.
	 *
	 * 'name' will only be used if neither givenName nor surName is present.
	 *
	 * The following contact types are allowed:
	 * "technical", "support", "administrative", "billing", "other"
	 *
	 * @param string $type  The type of contact.
	 * @param array $details  The details about the contact.
	 */
	public function addContact($type, $details) {
		assert('is_string($type)');
		assert('is_array($details)');
		assert('in_array($type, array("technical", "support", "administrative", "billing", "other"), TRUE)');

		/* Parse name into givenName and surName. */
		if (isset($details['name']) && empty($details['surName']) && empty($details['givenName'])) {
			$names = explode(',', $details['name'], 2);
			if (count($names) === 2) {
				$details['surName'] = trim($names[0]);
				$details['givenName'] = trim($names[1]);
			} else {
				$names = explode(' ', $details['name'], 2);
				if (count($names) === 2) {
					$details['givenName'] = trim($names[0]);
					$details['surName'] = trim($names[1]);
				} else {
					$details['surName'] = trim($names[0]);
				}
			}
		}

		$e = $this->createElement('ContactPerson');
		$e->setAttribute('contactType', $type);

		if (isset($details['company'])) {
			$e->appendChild($this->createTextElement('Company', $details['company']));
		}
		if (isset($details['givenName'])) {
			$e->appendChild($this->createTextElement('GivenName', $details['givenName']));
		}
		if (isset($details['surName'])) {
			$e->appendChild($this->createTextElement('SurName', $details['surName']));
		}

		if (isset($details['emailAddress'])) {
			$eas = $details['emailAddress'];
			if (!is_array($eas)) {
				$eas = array($eas);
			}
			foreach ($eas as $ea) {
				$e->appendChild($this->createTextElement('EmailAddress', $ea));
			}
		}

		if (isset($details['telephoneNumber'])) {
			$tlfNrs = $details['telephoneNumber'];
			if (!is_array($tlfNrs)) {
				$tlfNrs = array($tlfNrs);
			}
			foreach ($tlfNrs as $tlfNr) {
				$e->appendChild($this->createTextElement('TelephoneNumber', $tlfNr));
			}
		}

		$this->entityDescriptor->appendChild($e);
	}


	/**
	 * Create DOMElement in metadata namespace.
	 *
	 * Helper function for creating DOMElements with the metadata namespace.
	 *
	 * @param string $name  The name of the DOMElement.
	 * @return DOMElement  The new DOMElement.
	 */
	private function createElement($name, $ns = 'urn:oasis:names:tc:SAML:2.0:metadata') {
		assert('is_string($name)');
		assert('is_string($ns)');
		return $this->document->createElementNS($ns, $name);
	}


	/**
	 * Create a DOMElement in metadata namespace with a single text node.
	 *
	 * @param string $name  The name of the DOMElement.
	 * @param string $text  The text contained in the element.
	 * @return DOMElement  The new DOMElement with a text node.
	 */
	private function createTextElement($name, $text, $ns = NULL) {
		assert('is_string($name)');
		assert('is_string($text)');

		if ($ns !== NULL) {
			$node = $this->createElement($name, $ns);
		} else {
			$node = $this->createElement($name);
		}
		$node->appendChild($this->document->createTextNode($text));

		return $node;
	}


	/**
	 * Add a KeyDescriptor with an X509 certificate.
	 *
	 * @param DOMElement $ssoDesc  The IDPSSODescroptor or SPSSODecriptor the certificate
	 *                             should be added to.
	 * @param string|NULL $use  The value of the use-attribute.
	 * @param string $x509data  The certificate data.
	 */
	private function addX509KeyDescriptor(DOMElement $ssoDesc, $use, $x509data) {
		assert('in_array($use, array(NULL, "encryption", "signing"), TRUE)');
		assert('is_string($x509data)');

		$keyDescriptor = $this->createElement('KeyDescriptor');
		if ($use !== NULL) {
			$keyDescriptor->setAttribute('use', $use);
		}
		$ssoDesc->appendChild($keyDescriptor);

		$keyInfo = $this->document->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'ds:KeyInfo');
		$keyDescriptor->appendChild($keyInfo);

		$x509Data = $this->document->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'ds:X509Data');
		$keyInfo->appendChild($x509Data);

		$x509Certificate = $this->document->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'ds:X509Certificate');
		$x509Data->appendChild($x509Certificate);

		$x509Certificate->appendChild($this->document->createTextNode($x509data));
	}


	/**
	 * Add certificate.
	 *
	 * Helper function for adding a certificate to the metadata.
	 *
	 * @param DOMElement $ssoDesc  The IDPSSODescroptor or SPSSODecriptor the certificate
	 *                             should be added to.
	 * @param SimpleSAML_Configuration $metadata  The metadata for the entity.
	 */
	private function addCertificate(DOMElement $ssoDesc, SimpleSAML_Configuration $metadata) {

		$certInfo = SimpleSAML_Utilities::loadPublicKey($metadata->toArray());
		if ($certInfo === NULL || !array_key_exists('certData', $certInfo)) {
			/* No certificate to add. */
			return;
		}

		$certData = $certInfo['certData'];

		$this->addX509KeyDescriptor($ssoDesc, 'signing', $certData);
		$this->addX509KeyDescriptor($ssoDesc, 'encryption', $certData);
	}

}

?>