1479 lines
54 KiB
PHP
Executable File
1479 lines
54 KiB
PHP
Executable File
<?php
|
|
|
|
/**
|
|
* This is class for parsing of SAML 1.x and SAML 2.0 metadata.
|
|
*
|
|
* Metadata is loaded by calling the static methods parseFile, parseString or parseElement.
|
|
* These functions returns an instance of SimpleSAML_Metadata_SAMLParser. To get metadata
|
|
* from this object, use the methods getMetadata1xSP or getMetadata20SP.
|
|
*
|
|
* To parse a file which can contain a collection of EntityDescriptor or EntitiesDescriptor elements, use the
|
|
* parseDescriptorsFile, parseDescriptorsString or parseDescriptorsElement methods. These functions will return
|
|
* an array of SAMLParser elements where each element represents an EntityDescriptor-element.
|
|
*/
|
|
|
|
class SimpleSAML_Metadata_SAMLParser
|
|
{
|
|
/**
|
|
* This is the list of SAML 1.x protocols.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private static $SAML1xProtocols = array(
|
|
'urn:oasis:names:tc:SAML:1.0:protocol',
|
|
'urn:oasis:names:tc:SAML:1.1:protocol',
|
|
);
|
|
|
|
/**
|
|
* This is the list with the SAML 2.0 protocol.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private static $SAML20Protocols = array(
|
|
'urn:oasis:names:tc:SAML:2.0:protocol',
|
|
);
|
|
|
|
/**
|
|
* This is the entity id we find in the metadata.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $entityId;
|
|
|
|
/**
|
|
* This is an array with the processed SPSSODescriptor elements we have found in this
|
|
* metadata file.
|
|
* Each element in the array is an associative array with the elements from parseSSODescriptor and:
|
|
* - 'AssertionConsumerService': Array with the SP's assertion consumer services.
|
|
* Each assertion consumer service is stored as an associative array with the
|
|
* elements that parseGenericEndpoint returns.
|
|
*
|
|
* @var array[]
|
|
*/
|
|
private $spDescriptors;
|
|
|
|
/**
|
|
* This is an array with the processed IDPSSODescriptor elements we have found.
|
|
* Each element in the array is an associative array with the elements from parseSSODescriptor and:
|
|
* - 'SingleSignOnService': Array with the IdP's single sign on service endpoints. Each endpoint is stored
|
|
* as an associative array with the elements that parseGenericEndpoint returns.
|
|
*
|
|
* @var array[]
|
|
*/
|
|
private $idpDescriptors;
|
|
|
|
/**
|
|
* List of attribute authorities we have found.
|
|
*
|
|
* @var array
|
|
*/
|
|
private $attributeAuthorityDescriptors = array();
|
|
|
|
/**
|
|
* This is an associative array with the organization name for this entity. The key of
|
|
* the associative array is the language code, while the value is a string with the
|
|
* organization name.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private $organizationName = array();
|
|
|
|
/**
|
|
* This is an associative array with the organization display name for this entity. The key of
|
|
* the associative array is the language code, while the value is a string with the
|
|
* organization display name.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private $organizationDisplayName = array();
|
|
|
|
/**
|
|
* This is an associative array with the organization URI for this entity. The key of
|
|
* the associative array is the language code, while the value is the URI.
|
|
*
|
|
* @var string[]
|
|
*/
|
|
private $organizationURL = array();
|
|
|
|
/**
|
|
* This is an array of the Contact Persons of this entity.
|
|
*
|
|
* @var array[]
|
|
*/
|
|
private $contacts = array();
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $scopes;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $entityAttributes;
|
|
|
|
/**
|
|
* An associative array of attributes from the RegistrationInfo element.
|
|
* @var array
|
|
*/
|
|
private $registrationInfo;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private $tags;
|
|
|
|
/**
|
|
* This is an array of elements that may be used to validate this element.
|
|
*
|
|
* @var \SAML2\SignedElementHelper[]
|
|
*/
|
|
private $validators = array();
|
|
|
|
/**
|
|
* The original EntityDescriptor element for this entity, as a base64 encoded string.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $entityDescriptor;
|
|
|
|
/**
|
|
* This is the constructor for the SAMLParser class.
|
|
*
|
|
* @param \SAML2\XML\md\EntityDescriptor $entityElement The EntityDescriptor.
|
|
* @param int|null $maxExpireTime The unix timestamp for when this entity should expire, or
|
|
* NULL if unknown.
|
|
* @param array $validators An array of parent elements that may validate this element.
|
|
* @param array $parentExtensions An optional array of extensions from the parent element.
|
|
*/
|
|
private function __construct(
|
|
\SAML2\XML\md\EntityDescriptor $entityElement,
|
|
$maxExpireTime,
|
|
array $validators = array(),
|
|
array $parentExtensions = array()
|
|
) {
|
|
assert($maxExpireTime === null || is_int($maxExpireTime));
|
|
|
|
$this->spDescriptors = array();
|
|
$this->idpDescriptors = array();
|
|
|
|
$e = $entityElement->toXML();
|
|
$e = $e->ownerDocument->saveXML($e);
|
|
$this->entityDescriptor = base64_encode($e);
|
|
$this->entityId = $entityElement->entityID;
|
|
|
|
$expireTime = self::getExpireTime($entityElement, $maxExpireTime);
|
|
|
|
$this->validators = $validators;
|
|
$this->validators[] = $entityElement;
|
|
|
|
// process Extensions element, if it exists
|
|
$ext = self::processExtensions($entityElement, $parentExtensions);
|
|
$this->scopes = $ext['scope'];
|
|
$this->tags = $ext['tags'];
|
|
$this->entityAttributes = $ext['EntityAttributes'];
|
|
$this->registrationInfo = $ext['RegistrationInfo'];
|
|
|
|
// look over the RoleDescriptors
|
|
foreach ($entityElement->RoleDescriptor as $child) {
|
|
if ($child instanceof \SAML2\XML\md\SPSSODescriptor) {
|
|
$this->processSPSSODescriptor($child, $expireTime);
|
|
} elseif ($child instanceof \SAML2\XML\md\IDPSSODescriptor) {
|
|
$this->processIDPSSODescriptor($child, $expireTime);
|
|
} elseif ($child instanceof \SAML2\XML\md\AttributeAuthorityDescriptor) {
|
|
$this->processAttributeAuthorityDescriptor($child, $expireTime);
|
|
}
|
|
}
|
|
|
|
if ($entityElement->Organization) {
|
|
$this->processOrganization($entityElement->Organization);
|
|
}
|
|
|
|
if (!empty($entityElement->ContactPerson)) {
|
|
foreach ($entityElement->ContactPerson as $contact) {
|
|
$this->processContactPerson($contact);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a file which contains XML encoded metadata.
|
|
*
|
|
* @param string $file The path to the file which contains the metadata.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded.
|
|
* @throws Exception If the file does not parse as XML.
|
|
*/
|
|
public static function parseFile($file)
|
|
{
|
|
$data = \SimpleSAML\Utils\HTTP::fetch($file);
|
|
|
|
try {
|
|
$doc = \SAML2\DOMDocumentFactory::fromString($data);
|
|
} catch (\Exception $e) {
|
|
throw new Exception('Failed to read XML from file: '.$file);
|
|
}
|
|
|
|
return self::parseDocument($doc);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a string which contains XML encoded metadata.
|
|
*
|
|
* @param string $metadata A string which contains XML encoded metadata.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded.
|
|
* @throws Exception If the string does not parse as XML.
|
|
*/
|
|
public static function parseString($metadata)
|
|
{
|
|
try {
|
|
$doc = \SAML2\DOMDocumentFactory::fromString($metadata);
|
|
} catch (\Exception $e) {
|
|
throw new Exception('Failed to parse XML string.');
|
|
}
|
|
|
|
return self::parseDocument($doc);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a DOMDocument which is assumed to contain a single EntityDescriptor element.
|
|
*
|
|
* @param DOMDocument $document The DOMDocument which contains the EntityDescriptor element.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded.
|
|
*/
|
|
public static function parseDocument($document)
|
|
{
|
|
assert($document instanceof DOMDocument);
|
|
|
|
$entityElement = self::findEntityDescriptor($document);
|
|
|
|
return self::parseElement($entityElement);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a \SAML2\XML\md\EntityDescriptor object which represents a EntityDescriptor element.
|
|
*
|
|
* @param \SAML2\XML\md\EntityDescriptor $entityElement A \SAML2\XML\md\EntityDescriptor object which represents a
|
|
* EntityDescriptor element.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser An instance of this class with the metadata loaded.
|
|
*/
|
|
public static function parseElement($entityElement)
|
|
{
|
|
assert($entityElement instanceof \SAML2\XML\md\EntityDescriptor);
|
|
return new SimpleSAML_Metadata_SAMLParser($entityElement, null, array());
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a file where the root node is either an EntityDescriptor element or an
|
|
* EntitiesDescriptor element. In both cases it will return an associative array of SAMLParser instances. If
|
|
* the file contains a single EntityDescriptorElement, then the array will contain a single SAMLParser
|
|
* instance.
|
|
*
|
|
* @param string $file The path to the file which contains the EntityDescriptor or EntitiesDescriptor element.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser[] An array of SAMLParser instances.
|
|
* @throws Exception If the file does not parse as XML.
|
|
*/
|
|
public static function parseDescriptorsFile($file)
|
|
{
|
|
if ($file === null) {
|
|
throw new Exception('Cannot open file NULL. File name not specified.');
|
|
}
|
|
|
|
$data = \SimpleSAML\Utils\HTTP::fetch($file);
|
|
|
|
try {
|
|
$doc = \SAML2\DOMDocumentFactory::fromString($data);
|
|
} catch (\Exception $e) {
|
|
throw new Exception('Failed to read XML from file: '.$file);
|
|
}
|
|
|
|
if ($doc->documentElement === null) {
|
|
throw new Exception('Opened file is not an XML document: '.$file);
|
|
}
|
|
|
|
return self::parseDescriptorsElement($doc->documentElement);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a string with XML data. The root node of the XML data is expected to be either an
|
|
* EntityDescriptor element or an EntitiesDescriptor element. It will return an associative array of
|
|
* SAMLParser instances.
|
|
*
|
|
* @param string $string The string with XML data.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will
|
|
* be the entity id.
|
|
* @throws Exception If the string does not parse as XML.
|
|
*/
|
|
public static function parseDescriptorsString($string)
|
|
{
|
|
try {
|
|
$doc = \SAML2\DOMDocumentFactory::fromString($string);
|
|
} catch (\Exception $e) {
|
|
throw new Exception('Failed to parse XML string.');
|
|
}
|
|
|
|
return self::parseDescriptorsElement($doc->documentElement);
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a DOMElement which represents either an EntityDescriptor element or an
|
|
* EntitiesDescriptor element. It will return an associative array of SAMLParser instances in both cases.
|
|
*
|
|
* @param DOMElement|NULL $element The DOMElement which contains the EntityDescriptor element or the
|
|
* EntitiesDescriptor element.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser[] An associative array of SAMLParser instances. The key of the array will
|
|
* be the entity id.
|
|
* @throws Exception if the document is empty or the root is an unexpected node.
|
|
*/
|
|
public static function parseDescriptorsElement(DOMElement $element = null)
|
|
{
|
|
if ($element === null) {
|
|
throw new Exception('Document was empty.');
|
|
}
|
|
|
|
if (SimpleSAML\Utils\XML::isDOMNodeOfType($element, 'EntityDescriptor', '@md') === true) {
|
|
return self::processDescriptorsElement(new \SAML2\XML\md\EntityDescriptor($element));
|
|
} elseif (SimpleSAML\Utils\XML::isDOMNodeOfType($element, 'EntitiesDescriptor', '@md') === true) {
|
|
return self::processDescriptorsElement(new \SAML2\XML\md\EntitiesDescriptor($element));
|
|
} else {
|
|
throw new Exception('Unexpected root node: ['.$element->namespaceURI.']:'.$element->localName);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param \SAML2\XML\md\EntityDescriptor|\SAML2\XML\md\EntitiesDescriptor $element The element we should process.
|
|
* @param int|NULL $maxExpireTime The maximum expiration time
|
|
* of the entities.
|
|
* @param array $validators The parent-elements that may be
|
|
* signed.
|
|
* @param array $parentExtensions An optional array of
|
|
* extensions from the parent element.
|
|
*
|
|
* @return SimpleSAML_Metadata_SAMLParser[] Array of SAMLParser instances.
|
|
*/
|
|
private static function processDescriptorsElement(
|
|
$element,
|
|
$maxExpireTime = null,
|
|
array $validators = array(),
|
|
array $parentExtensions = array()
|
|
) {
|
|
assert($maxExpireTime === null || is_int($maxExpireTime));
|
|
|
|
if ($element instanceof \SAML2\XML\md\EntityDescriptor) {
|
|
$ret = new SimpleSAML_Metadata_SAMLParser($element, $maxExpireTime, $validators, $parentExtensions);
|
|
$ret = array($ret->getEntityId() => $ret);
|
|
/** @var SimpleSAML_Metadata_SAMLParser[] $ret */
|
|
return $ret;
|
|
}
|
|
|
|
assert($element instanceof \SAML2\XML\md\EntitiesDescriptor);
|
|
|
|
$extensions = self::processExtensions($element, $parentExtensions);
|
|
$expTime = self::getExpireTime($element, $maxExpireTime);
|
|
|
|
$validators[] = $element;
|
|
|
|
$ret = array();
|
|
foreach ($element->children as $child) {
|
|
$ret += self::processDescriptorsElement($child, $expTime, $validators, $extensions);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine how long a given element can be cached.
|
|
*
|
|
* This function looks for the 'validUntil' attribute to determine
|
|
* how long a given XML-element is valid. It returns this as a unix timestamp.
|
|
*
|
|
* @param mixed $element The element we should determine the expiry time of.
|
|
* @param int|NULL $maxExpireTime The maximum expiration time.
|
|
*
|
|
* @return int The unix timestamp for when the element should expire. Will be NULL if no
|
|
* limit is set for the element.
|
|
*/
|
|
private static function getExpireTime($element, $maxExpireTime)
|
|
{
|
|
// validUntil may be null
|
|
$expire = $element->validUntil;
|
|
|
|
if ($maxExpireTime !== null && ($expire === null || $maxExpireTime < $expire)) {
|
|
$expire = $maxExpireTime;
|
|
}
|
|
|
|
return $expire;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function returns the entity id of this parsed entity.
|
|
*
|
|
* @return string The entity id of this parsed entity.
|
|
*/
|
|
public function getEntityId()
|
|
{
|
|
return $this->entityId;
|
|
}
|
|
|
|
|
|
private function getMetadataCommon()
|
|
{
|
|
$ret = array();
|
|
$ret['entityid'] = $this->entityId;
|
|
$ret['entityDescriptor'] = $this->entityDescriptor;
|
|
|
|
// add organizational metadata
|
|
if (!empty($this->organizationName)) {
|
|
$ret['description'] = $this->organizationName;
|
|
$ret['OrganizationName'] = $this->organizationName;
|
|
}
|
|
if (!empty($this->organizationDisplayName)) {
|
|
$ret['name'] = $this->organizationDisplayName;
|
|
$ret['OrganizationDisplayName'] = $this->organizationDisplayName;
|
|
}
|
|
if (!empty($this->organizationURL)) {
|
|
$ret['url'] = $this->organizationURL;
|
|
$ret['OrganizationURL'] = $this->organizationURL;
|
|
}
|
|
|
|
//add contact metadata
|
|
$ret['contacts'] = $this->contacts;
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add data parsed from extensions to metadata.
|
|
*
|
|
* @param array &$metadata The metadata that should be updated.
|
|
* @param array $roleDescriptor The parsed role descriptor.
|
|
*/
|
|
private function addExtensions(array &$metadata, array $roleDescriptor)
|
|
{
|
|
assert(array_key_exists('scope', $roleDescriptor));
|
|
assert(array_key_exists('tags', $roleDescriptor));
|
|
|
|
$scopes = array_merge($this->scopes, array_diff($roleDescriptor['scope'], $this->scopes));
|
|
if (!empty($scopes)) {
|
|
$metadata['scope'] = $scopes;
|
|
}
|
|
|
|
$tags = array_merge($this->tags, array_diff($roleDescriptor['tags'], $this->tags));
|
|
if (!empty($tags)) {
|
|
$metadata['tags'] = $tags;
|
|
}
|
|
|
|
|
|
if (!empty($this->registrationInfo)) {
|
|
$metadata['RegistrationInfo'] = $this->registrationInfo;
|
|
}
|
|
|
|
if (!empty($this->entityAttributes)) {
|
|
$metadata['EntityAttributes'] = $this->entityAttributes;
|
|
|
|
// check for entity categories
|
|
if (SimpleSAML\Utils\Config\Metadata::isHiddenFromDiscovery($metadata)) {
|
|
$metadata['hide.from.discovery'] = true;
|
|
}
|
|
}
|
|
|
|
if (!empty($roleDescriptor['UIInfo'])) {
|
|
$metadata['UIInfo'] = $roleDescriptor['UIInfo'];
|
|
}
|
|
|
|
if (!empty($roleDescriptor['DiscoHints'])) {
|
|
$metadata['DiscoHints'] = $roleDescriptor['DiscoHints'];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function returns the metadata for SAML 1.x SPs in the format SimpleSAMLphp expects.
|
|
* This is an associative array with the following fields:
|
|
* - 'entityid': The entity id of the entity described in the metadata.
|
|
* - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports
|
|
* the browser-post binding.
|
|
* - 'certData': X509Certificate for entity (if present).
|
|
*
|
|
* Metadata must be loaded with one of the parse functions before this function can be called.
|
|
*
|
|
* @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x SP.
|
|
*/
|
|
public function getMetadata1xSP()
|
|
{
|
|
$ret = $this->getMetadataCommon();
|
|
$ret['metadata-set'] = 'shib13-sp-remote';
|
|
|
|
|
|
// find SP information which supports one of the SAML 1.x protocols
|
|
$spd = $this->getSPDescriptors(self::$SAML1xProtocols);
|
|
if (count($spd) === 0) {
|
|
return null;
|
|
}
|
|
|
|
// we currently only look at the first SPDescriptor which supports SAML 1.x
|
|
$spd = $spd[0];
|
|
|
|
// add expire time to metadata
|
|
if (array_key_exists('expire', $spd)) {
|
|
$ret['expire'] = $spd['expire'];
|
|
}
|
|
|
|
// find the assertion consumer service endpoints
|
|
$ret['AssertionConsumerService'] = $spd['AssertionConsumerService'];
|
|
|
|
// add the list of attributes the SP should receive
|
|
if (array_key_exists('attributes', $spd)) {
|
|
$ret['attributes'] = $spd['attributes'];
|
|
}
|
|
if (array_key_exists('attributes.required', $spd)) {
|
|
$ret['attributes.required'] = $spd['attributes.required'];
|
|
}
|
|
if (array_key_exists('attributes.NameFormat', $spd)) {
|
|
$ret['attributes.NameFormat'] = $spd['attributes.NameFormat'];
|
|
}
|
|
|
|
// add name & description
|
|
if (array_key_exists('name', $spd)) {
|
|
$ret['name'] = $spd['name'];
|
|
}
|
|
if (array_key_exists('description', $spd)) {
|
|
$ret['description'] = $spd['description'];
|
|
}
|
|
|
|
// add public keys
|
|
if (!empty($spd['keys'])) {
|
|
$ret['keys'] = $spd['keys'];
|
|
}
|
|
|
|
// add extensions
|
|
$this->addExtensions($ret, $spd);
|
|
|
|
// prioritize mdui:DisplayName as the name if available
|
|
if (!empty($ret['UIInfo']['DisplayName'])) {
|
|
$ret['name'] = $ret['UIInfo']['DisplayName'];
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function returns the metadata for SAML 1.x IdPs in the format SimpleSAMLphp expects.
|
|
* This is an associative array with the following fields:
|
|
* - 'entityid': The entity id of the entity described in the metadata.
|
|
* - 'name': Auto generated name for this entity. Currently set to the entity id.
|
|
* - 'SingleSignOnService': String with the URL of the SSO service which supports the redirect binding.
|
|
* - 'SingleLogoutService': String with the URL where we should send logout requests/responses.
|
|
* - 'certData': X509Certificate for entity (if present).
|
|
* - 'certFingerprint': Fingerprint of the X509Certificate from the metadata. (deprecated)
|
|
*
|
|
* Metadata must be loaded with one of the parse functions before this function can be called.
|
|
*
|
|
* @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 1.x
|
|
* IdP.
|
|
*/
|
|
public function getMetadata1xIdP()
|
|
{
|
|
$ret = $this->getMetadataCommon();
|
|
$ret['metadata-set'] = 'shib13-idp-remote';
|
|
|
|
// find IdP information which supports the SAML 1.x protocol
|
|
$idp = $this->getIdPDescriptors(self::$SAML1xProtocols);
|
|
if (count($idp) === 0) {
|
|
return null;
|
|
}
|
|
|
|
// we currently only look at the first IDP descriptor which supports SAML 1.x
|
|
$idp = $idp[0];
|
|
|
|
// fdd expire time to metadata
|
|
if (array_key_exists('expire', $idp)) {
|
|
$ret['expire'] = $idp['expire'];
|
|
}
|
|
|
|
// find the SSO service endpoints
|
|
$ret['SingleSignOnService'] = $idp['SingleSignOnService'];
|
|
|
|
// find the ArtifactResolutionService endpoint
|
|
$ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService'];
|
|
|
|
// add public keys
|
|
if (!empty($idp['keys'])) {
|
|
$ret['keys'] = $idp['keys'];
|
|
}
|
|
|
|
// add extensions
|
|
$this->addExtensions($ret, $idp);
|
|
|
|
// prioritize mdui:DisplayName as the name if available
|
|
if (!empty($ret['UIInfo']['DisplayName'])) {
|
|
$ret['name'] = $ret['UIInfo']['DisplayName'];
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function returns the metadata for SAML 2.0 SPs in the format SimpleSAMLphp expects.
|
|
* This is an associative array with the following fields:
|
|
* - 'entityid': The entity id of the entity described in the metadata.
|
|
* - 'AssertionConsumerService': String with the URL of the assertion consumer service which supports
|
|
* the browser-post binding.
|
|
* - 'SingleLogoutService': String with the URL where we should send logout requests/responses.
|
|
* - 'NameIDFormat': The name ID format this SP expects. This may be unset.
|
|
* - 'certData': X509Certificate for entity (if present).
|
|
*
|
|
* Metadata must be loaded with one of the parse functions before this function can be called.
|
|
*
|
|
* @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.x SP.
|
|
*/
|
|
public function getMetadata20SP()
|
|
{
|
|
$ret = $this->getMetadataCommon();
|
|
$ret['metadata-set'] = 'saml20-sp-remote';
|
|
|
|
// find SP information which supports the SAML 2.0 protocol
|
|
$spd = $this->getSPDescriptors(self::$SAML20Protocols);
|
|
if (count($spd) === 0) {
|
|
return null;
|
|
}
|
|
|
|
// we currently only look at the first SPDescriptor which supports SAML 2.0
|
|
$spd = $spd[0];
|
|
|
|
// add expire time to metadata
|
|
if (array_key_exists('expire', $spd)) {
|
|
$ret['expire'] = $spd['expire'];
|
|
}
|
|
|
|
// find the assertion consumer service endpoints
|
|
$ret['AssertionConsumerService'] = $spd['AssertionConsumerService'];
|
|
|
|
|
|
// find the single logout service endpoint
|
|
$ret['SingleLogoutService'] = $spd['SingleLogoutService'];
|
|
|
|
|
|
// find the NameIDFormat. This may not exist
|
|
if (count($spd['nameIDFormats']) > 0) {
|
|
// SimpleSAMLphp currently only supports a single NameIDFormat pr. SP. We use the first one
|
|
$ret['NameIDFormat'] = $spd['nameIDFormats'][0];
|
|
}
|
|
|
|
// add the list of attributes the SP should receive
|
|
if (array_key_exists('attributes', $spd)) {
|
|
$ret['attributes'] = $spd['attributes'];
|
|
}
|
|
if (array_key_exists('attributes.required', $spd)) {
|
|
$ret['attributes.required'] = $spd['attributes.required'];
|
|
}
|
|
if (array_key_exists('attributes.NameFormat', $spd)) {
|
|
$ret['attributes.NameFormat'] = $spd['attributes.NameFormat'];
|
|
}
|
|
if (array_key_exists('attributes.index', $spd)) {
|
|
$ret['attributes.index'] = $spd['attributes.index'];
|
|
}
|
|
if (array_key_exists('attributes.isDefault', $spd)) {
|
|
$ret['attributes.isDefault'] = $spd['attributes.isDefault'];
|
|
}
|
|
|
|
// add name & description
|
|
if (array_key_exists('name', $spd)) {
|
|
$ret['name'] = $spd['name'];
|
|
}
|
|
if (array_key_exists('description', $spd)) {
|
|
$ret['description'] = $spd['description'];
|
|
}
|
|
|
|
// add public keys
|
|
if (!empty($spd['keys'])) {
|
|
$ret['keys'] = $spd['keys'];
|
|
}
|
|
|
|
// add validate.authnrequest
|
|
if (array_key_exists('AuthnRequestsSigned', $spd)) {
|
|
$ret['validate.authnrequest'] = $spd['AuthnRequestsSigned'];
|
|
}
|
|
|
|
// add saml20.sign.assertion
|
|
if (array_key_exists('WantAssertionsSigned', $spd)) {
|
|
$ret['saml20.sign.assertion'] = $spd['WantAssertionsSigned'];
|
|
}
|
|
|
|
// add extensions
|
|
$this->addExtensions($ret, $spd);
|
|
|
|
// prioritize mdui:DisplayName as the name if available
|
|
if (!empty($ret['UIInfo']['DisplayName'])) {
|
|
$ret['name'] = $ret['UIInfo']['DisplayName'];
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function returns the metadata for SAML 2.0 IdPs in the format SimpleSAMLphp expects.
|
|
* This is an associative array with the following fields:
|
|
* - 'entityid': The entity id of the entity described in the metadata.
|
|
* - 'name': Auto generated name for this entity. Currently set to the entity id.
|
|
* - 'SingleSignOnService': String with the URL of the SSO service which supports the redirect binding.
|
|
* - 'SingleLogoutService': String with the URL where we should send logout requests(/responses).
|
|
* - 'SingleLogoutServiceResponse': String where we should send logout responses (if this is different from
|
|
* the 'SingleLogoutService' endpoint.
|
|
* - 'NameIDFormats': The name ID formats this IdP supports.
|
|
* - 'certData': X509Certificate for entity (if present).
|
|
* - 'certFingerprint': Fingerprint of the X509Certificate from the metadata. (deprecated)
|
|
*
|
|
* Metadata must be loaded with one of the parse functions before this function can be called.
|
|
*
|
|
* @return array An associative array with metadata or NULL if we are unable to generate metadata for a SAML 2.0
|
|
* IdP.
|
|
*/
|
|
public function getMetadata20IdP()
|
|
{
|
|
$ret = $this->getMetadataCommon();
|
|
$ret['metadata-set'] = 'saml20-idp-remote';
|
|
|
|
// find IdP information which supports the SAML 2.0 protocol
|
|
$idp = $this->getIdPDescriptors(self::$SAML20Protocols);
|
|
if (count($idp) === 0) {
|
|
return null;
|
|
}
|
|
|
|
// we currently only look at the first IDP descriptor which supports SAML 2.0
|
|
$idp = $idp[0];
|
|
|
|
// add expire time to metadata
|
|
if (array_key_exists('expire', $idp)) {
|
|
$ret['expire'] = $idp['expire'];
|
|
}
|
|
|
|
// enable redirect.sign if WantAuthnRequestsSigned is enabled
|
|
if ($idp['WantAuthnRequestsSigned']) {
|
|
$ret['sign.authnrequest'] = true;
|
|
}
|
|
|
|
// find the SSO service endpoint
|
|
$ret['SingleSignOnService'] = $idp['SingleSignOnService'];
|
|
|
|
// find the single logout service endpoint
|
|
$ret['SingleLogoutService'] = $idp['SingleLogoutService'];
|
|
|
|
// find the ArtifactResolutionService endpoint
|
|
$ret['ArtifactResolutionService'] = $idp['ArtifactResolutionService'];
|
|
|
|
// add supported nameIDFormats
|
|
$ret['NameIDFormats'] = $idp['nameIDFormats'];
|
|
|
|
// add public keys
|
|
if (!empty($idp['keys'])) {
|
|
$ret['keys'] = $idp['keys'];
|
|
}
|
|
|
|
// add extensions
|
|
$this->addExtensions($ret, $idp);
|
|
|
|
// prioritize mdui:DisplayName as the name if available
|
|
if (!empty($ret['UIInfo']['DisplayName'])) {
|
|
$ret['name'] = $ret['UIInfo']['DisplayName'];
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve AttributeAuthorities from the metadata.
|
|
*
|
|
* @return array Array of AttributeAuthorityDescriptor entries.
|
|
*/
|
|
public function getAttributeAuthorities()
|
|
{
|
|
return $this->attributeAuthorityDescriptors;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse a RoleDescriptorType element.
|
|
*
|
|
* The returned associative array has the following elements:
|
|
* - 'protocols': Array with the protocols supported.
|
|
* - 'expire': Timestamp for when this descriptor expires.
|
|
* - 'keys': Array of associative arrays with the elements from parseKeyDescriptor.
|
|
*
|
|
* @param \SAML2\XML\md\RoleDescriptor $element The element we should extract metadata from.
|
|
* @param int|NULL $expireTime The unix timestamp for when this element should expire, or
|
|
* NULL if unknown.
|
|
*
|
|
* @return array An associative array with metadata we have extracted from this element.
|
|
*/
|
|
private static function parseRoleDescriptorType(\SAML2\XML\md\RoleDescriptor $element, $expireTime)
|
|
{
|
|
assert($expireTime === null || is_int($expireTime));
|
|
|
|
$ret = array();
|
|
|
|
$expireTime = self::getExpireTime($element, $expireTime);
|
|
|
|
if ($expireTime !== null) {
|
|
// we got an expired timestamp, either from this element or one of the parent elements
|
|
$ret['expire'] = $expireTime;
|
|
}
|
|
|
|
$ret['protocols'] = $element->protocolSupportEnumeration;
|
|
|
|
// process KeyDescriptor elements
|
|
$ret['keys'] = array();
|
|
foreach ($element->KeyDescriptor as $kd) {
|
|
$key = self::parseKeyDescriptor($kd);
|
|
if ($key !== null) {
|
|
$ret['keys'][] = $key;
|
|
}
|
|
}
|
|
|
|
$ext = self::processExtensions($element);
|
|
$ret['scope'] = $ext['scope'];
|
|
$ret['tags'] = $ext['tags'];
|
|
$ret['EntityAttributes'] = $ext['EntityAttributes'];
|
|
$ret['UIInfo'] = $ext['UIInfo'];
|
|
$ret['DiscoHints'] = $ext['DiscoHints'];
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function extracts metadata from a SSODescriptor element.
|
|
*
|
|
* The returned associative array has the following elements:
|
|
* - 'protocols': Array with the protocols this SSODescriptor supports.
|
|
* - 'SingleLogoutService': Array with the single logout service endpoints. Each endpoint is stored
|
|
* as an associative array with the elements that parseGenericEndpoint returns.
|
|
* - 'nameIDFormats': The NameIDFormats supported by this SSODescriptor. This may be an empty array.
|
|
* - 'keys': Array of associative arrays with the elements from parseKeyDescriptor:
|
|
*
|
|
* @param \SAML2\XML\md\SSODescriptorType $element The element we should extract metadata from.
|
|
* @param int|NULL $expireTime The unix timestamp for when this element should expire, or
|
|
* NULL if unknown.
|
|
*
|
|
* @return array An associative array with metadata we have extracted from this element.
|
|
*/
|
|
private static function parseSSODescriptor(\SAML2\XML\md\SSODescriptorType $element, $expireTime)
|
|
{
|
|
assert($expireTime === null || is_int($expireTime));
|
|
|
|
$sd = self::parseRoleDescriptorType($element, $expireTime);
|
|
|
|
// find all SingleLogoutService elements
|
|
$sd['SingleLogoutService'] = self::extractEndpoints($element->SingleLogoutService);
|
|
|
|
// find all ArtifactResolutionService elements
|
|
$sd['ArtifactResolutionService'] = self::extractEndpoints($element->ArtifactResolutionService);
|
|
|
|
|
|
// process NameIDFormat elements
|
|
$sd['nameIDFormats'] = $element->NameIDFormat;
|
|
|
|
return $sd;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function extracts metadata from a SPSSODescriptor element.
|
|
*
|
|
* @param \SAML2\XML\md\SPSSODescriptor $element The element which should be parsed.
|
|
* @param int|NULL $expireTime The unix timestamp for when this element should expire, or
|
|
* NULL if unknown.
|
|
*/
|
|
private function processSPSSODescriptor(\SAML2\XML\md\SPSSODescriptor $element, $expireTime)
|
|
{
|
|
assert($expireTime === null || is_int($expireTime));
|
|
|
|
$sp = self::parseSSODescriptor($element, $expireTime);
|
|
|
|
// find all AssertionConsumerService elements
|
|
$sp['AssertionConsumerService'] = self::extractEndpoints($element->AssertionConsumerService);
|
|
|
|
// find all the attributes and SP name...
|
|
$attcs = $element->AttributeConsumingService;
|
|
if (count($attcs) > 0) {
|
|
self::parseAttributeConsumerService($attcs[0], $sp);
|
|
}
|
|
|
|
// check AuthnRequestsSigned
|
|
if ($element->AuthnRequestsSigned !== null) {
|
|
$sp['AuthnRequestsSigned'] = $element->AuthnRequestsSigned;
|
|
}
|
|
|
|
// check WantAssertionsSigned
|
|
if ($element->WantAssertionsSigned !== null) {
|
|
$sp['WantAssertionsSigned'] = $element->WantAssertionsSigned;
|
|
}
|
|
|
|
$this->spDescriptors[] = $sp;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function extracts metadata from a IDPSSODescriptor element.
|
|
*
|
|
* @param \SAML2\XML\md\IDPSSODescriptor $element The element which should be parsed.
|
|
* @param int|NULL $expireTime The unix timestamp for when this element should expire, or
|
|
* NULL if unknown.
|
|
*/
|
|
private function processIDPSSODescriptor(\SAML2\XML\md\IDPSSODescriptor $element, $expireTime)
|
|
{
|
|
assert($expireTime === null || is_int($expireTime));
|
|
|
|
$idp = self::parseSSODescriptor($element, $expireTime);
|
|
|
|
// find all SingleSignOnService elements
|
|
$idp['SingleSignOnService'] = self::extractEndpoints($element->SingleSignOnService);
|
|
|
|
if ($element->WantAuthnRequestsSigned) {
|
|
$idp['WantAuthnRequestsSigned'] = true;
|
|
} else {
|
|
$idp['WantAuthnRequestsSigned'] = false;
|
|
}
|
|
|
|
$this->idpDescriptors[] = $idp;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function extracts metadata from a AttributeAuthorityDescriptor element.
|
|
*
|
|
* @param \SAML2\XML\md\AttributeAuthorityDescriptor $element The element which should be parsed.
|
|
* @param int|NULL $expireTime The unix timestamp for when this element should
|
|
* expire, or NULL if unknown.
|
|
*/
|
|
private function processAttributeAuthorityDescriptor(
|
|
\SAML2\XML\md\AttributeAuthorityDescriptor $element,
|
|
$expireTime
|
|
) {
|
|
assert($expireTime === null || is_int($expireTime));
|
|
|
|
$aad = self::parseRoleDescriptorType($element, $expireTime);
|
|
$aad['entityid'] = $this->entityId;
|
|
$aad['metadata-set'] = 'attributeauthority-remote';
|
|
|
|
$aad['AttributeService'] = self::extractEndpoints($element->AttributeService);
|
|
$aad['AssertionIDRequestService'] = self::extractEndpoints($element->AssertionIDRequestService);
|
|
$aad['NameIDFormat'] = $element->NameIDFormat;
|
|
|
|
$this->attributeAuthorityDescriptors[] = $aad;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse an Extensions element. Extensions may appear in multiple elements and certain extension may get inherited
|
|
* from a parent element.
|
|
*
|
|
* @param mixed $element The element which contains the Extensions element.
|
|
* @param array $parentExtensions An optional array of extensions from the parent element.
|
|
*
|
|
* @return array An associative array with the extensions parsed.
|
|
*/
|
|
private static function processExtensions($element, $parentExtensions = array())
|
|
{
|
|
$ret = array(
|
|
'scope' => array(),
|
|
'tags' => array(),
|
|
'EntityAttributes' => array(),
|
|
'RegistrationInfo' => array(),
|
|
'UIInfo' => array(),
|
|
'DiscoHints' => array(),
|
|
);
|
|
|
|
// Some extensions may get inherited from a parent element
|
|
if (($element instanceof \SAML2\XML\md\EntityDescriptor || $element instanceof \SAML2\XML\md\EntitiesDescriptor)
|
|
&& !empty($parentExtensions['RegistrationInfo'])) {
|
|
$ret['RegistrationInfo'] = $parentExtensions['RegistrationInfo'];
|
|
}
|
|
|
|
foreach ($element->Extensions as $e) {
|
|
if ($e instanceof \SAML2\XML\shibmd\Scope) {
|
|
$ret['scope'][] = $e->scope;
|
|
continue;
|
|
}
|
|
|
|
// Entity Attributes are only allowed at entity level extensions and not at RoleDescriptor level
|
|
if ($element instanceof \SAML2\XML\md\EntityDescriptor ||
|
|
$element instanceof \SAML2\XML\md\EntitiesDescriptor) {
|
|
if ($e instanceof \SAML2\XML\mdrpi\RegistrationInfo) {
|
|
// Registration Authority cannot be overridden (warn only if override attempts to change the value)
|
|
if (isset($ret['RegistrationInfo']['registrationAuthority'])
|
|
&& $ret['RegistrationInfo']['registrationAuthority'] !== $e->registrationAuthority) {
|
|
SimpleSAML\Logger::warning('Invalid attempt to override registrationAuthority \''
|
|
. $ret['RegistrationInfo']['registrationAuthority'] . "' with '{$e->registrationAuthority}'");
|
|
} else {
|
|
$ret['RegistrationInfo']['registrationAuthority'] = $e->registrationAuthority;
|
|
}
|
|
}
|
|
if ($e instanceof \SAML2\XML\mdattr\EntityAttributes && !empty($e->children)) {
|
|
foreach ($e->children as $attr) {
|
|
// only saml:Attribute are currently supported here. The specifications also allows
|
|
// saml:Assertions, which more complex processing
|
|
if ($attr instanceof \SAML2\XML\saml\Attribute) {
|
|
if (empty($attr->Name) || empty($attr->AttributeValue)) {
|
|
continue;
|
|
}
|
|
|
|
// attribute names that is not URI is prefixed as this: '{nameformat}name'
|
|
$name = $attr->Name;
|
|
if (empty($attr->NameFormat)) {
|
|
$name = '{'.\SAML2\Constants::NAMEFORMAT_UNSPECIFIED.'}'.$attr->Name;
|
|
} elseif ($attr->NameFormat !== 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri') {
|
|
$name = '{'.$attr->NameFormat.'}'.$attr->Name;
|
|
}
|
|
|
|
$values = array();
|
|
foreach ($attr->AttributeValue as $attrvalue) {
|
|
$values[] = $attrvalue->getString();
|
|
}
|
|
|
|
$ret['EntityAttributes'][$name] = $values;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// UIInfo elements are only allowed at RoleDescriptor level extensions
|
|
if ($element instanceof \SAML2\XML\md\RoleDescriptor) {
|
|
if ($e instanceof \SAML2\XML\mdui\UIInfo) {
|
|
$ret['UIInfo']['DisplayName'] = $e->DisplayName;
|
|
$ret['UIInfo']['Description'] = $e->Description;
|
|
$ret['UIInfo']['InformationURL'] = $e->InformationURL;
|
|
$ret['UIInfo']['PrivacyStatementURL'] = $e->PrivacyStatementURL;
|
|
|
|
foreach ($e->Keywords as $uiItem) {
|
|
if (!($uiItem instanceof \SAML2\XML\mdui\Keywords)
|
|
|| empty($uiItem->Keywords)
|
|
|| empty($uiItem->lang)
|
|
) {
|
|
continue;
|
|
}
|
|
$ret['UIInfo']['Keywords'][$uiItem->lang] = $uiItem->Keywords;
|
|
}
|
|
foreach ($e->Logo as $uiItem) {
|
|
if (!($uiItem instanceof \SAML2\XML\mdui\Logo)
|
|
|| empty($uiItem->url)
|
|
|| empty($uiItem->height)
|
|
|| empty($uiItem->width)
|
|
) {
|
|
continue;
|
|
}
|
|
$logo = array(
|
|
'url' => $uiItem->url,
|
|
'height' => $uiItem->height,
|
|
'width' => $uiItem->width,
|
|
);
|
|
if (!empty($uiItem->lang)) {
|
|
$logo['lang'] = $uiItem->lang;
|
|
}
|
|
$ret['UIInfo']['Logo'][] = $logo;
|
|
}
|
|
}
|
|
}
|
|
|
|
// DiscoHints elements are only allowed at IDPSSODescriptor level extensions
|
|
if ($element instanceof \SAML2\XML\md\IDPSSODescriptor) {
|
|
if ($e instanceof \SAML2\XML\mdui\DiscoHints) {
|
|
$ret['DiscoHints']['IPHint'] = $e->IPHint;
|
|
$ret['DiscoHints']['DomainHint'] = $e->DomainHint;
|
|
$ret['DiscoHints']['GeolocationHint'] = $e->GeolocationHint;
|
|
}
|
|
}
|
|
|
|
if (!($e instanceof \SAML2\XML\Chunk)) {
|
|
continue;
|
|
}
|
|
|
|
if ($e->localName === 'Attribute' && $e->namespaceURI === \SAML2\Constants::NS_SAML) {
|
|
$attribute = $e->xml;
|
|
|
|
$name = $attribute->getAttribute('Name');
|
|
$values = array_map(
|
|
array('SimpleSAML\Utils\XML', 'getDOMText'),
|
|
SimpleSAML\Utils\XML::getDOMChildren($attribute, 'AttributeValue', '@saml2')
|
|
);
|
|
|
|
if ($name === 'tags') {
|
|
foreach ($values as $tagname) {
|
|
if (!empty($tagname)) {
|
|
$ret['tags'][] = $tagname;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse and process a Organization element.
|
|
*
|
|
* @param \SAML2\XML\md\Organization $element The Organization element.
|
|
*/
|
|
private function processOrganization(\SAML2\XML\md\Organization $element)
|
|
{
|
|
$this->organizationName = $element->OrganizationName;
|
|
$this->organizationDisplayName = $element->OrganizationDisplayName;
|
|
$this->organizationURL = $element->OrganizationURL;
|
|
}
|
|
|
|
|
|
/**
|
|
* Parse and process a ContactPerson element.
|
|
*
|
|
* @param \SAML2\XML\md\ContactPerson $element The ContactPerson element.
|
|
*/
|
|
|
|
private function processContactPerson(\SAML2\XML\md\ContactPerson $element)
|
|
{
|
|
$contactPerson = array();
|
|
if (!empty($element->contactType)) {
|
|
$contactPerson['contactType'] = $element->contactType;
|
|
}
|
|
if (!empty($element->Company)) {
|
|
$contactPerson['company'] = $element->Company;
|
|
}
|
|
if (!empty($element->GivenName)) {
|
|
$contactPerson['givenName'] = $element->GivenName;
|
|
}
|
|
if (!empty($element->SurName)) {
|
|
$contactPerson['surName'] = $element->SurName;
|
|
}
|
|
if (!empty($element->EmailAddress)) {
|
|
$contactPerson['emailAddress'] = $element->EmailAddress;
|
|
}
|
|
if (!empty($element->TelephoneNumber)) {
|
|
$contactPerson['telephoneNumber'] = $element->TelephoneNumber;
|
|
}
|
|
if (!empty($contactPerson)) {
|
|
$this->contacts[] = $contactPerson;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses AttributeConsumerService elements.
|
|
*
|
|
* @param \SAML2\XML\md\AttributeConsumingService $element The AttributeConsumingService to parse.
|
|
* @param array $sp The array with the SP's metadata.
|
|
*/
|
|
private static function parseAttributeConsumerService(\SAML2\XML\md\AttributeConsumingService $element, &$sp)
|
|
{
|
|
assert(is_array($sp));
|
|
|
|
$sp['name'] = $element->ServiceName;
|
|
$sp['description'] = $element->ServiceDescription;
|
|
|
|
$format = null;
|
|
$sp['attributes'] = array();
|
|
$sp['attributes.required'] = array();
|
|
foreach ($element->RequestedAttribute as $child) {
|
|
$attrname = $child->Name;
|
|
$sp['attributes'][] = $attrname;
|
|
|
|
if ($child->isRequired !== null && $child->isRequired === true) {
|
|
$sp['attributes.required'][] = $attrname;
|
|
}
|
|
|
|
if ($child->NameFormat !== null) {
|
|
$attrformat = $child->NameFormat;
|
|
} else {
|
|
$attrformat = \SAML2\Constants::NAMEFORMAT_UNSPECIFIED;
|
|
}
|
|
|
|
if ($format === null) {
|
|
$format = $attrformat;
|
|
} elseif ($format !== $attrformat) {
|
|
$format = \SAML2\Constants::NAMEFORMAT_UNSPECIFIED;
|
|
}
|
|
}
|
|
|
|
if (empty($sp['attributes'])) {
|
|
// a really invalid configuration: all AttributeConsumingServices should have one or more attributes
|
|
unset($sp['attributes']);
|
|
}
|
|
if (empty($sp['attributes.required'])) {
|
|
unset($sp['attributes.required']);
|
|
}
|
|
|
|
if ($format !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED && $format !== null) {
|
|
$sp['attributes.NameFormat'] = $format;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* This function is a generic endpoint element parser.
|
|
*
|
|
* The returned associative array has the following elements:
|
|
* - 'Binding': The binding this endpoint uses.
|
|
* - 'Location': The URL to this endpoint.
|
|
* - 'ResponseLocation': The URL where responses should be sent. This may not exist.
|
|
* - 'index': The index of this endpoint. This attribute is only for indexed endpoints.
|
|
* - 'isDefault': Whether this endpoint is the default endpoint for this type. This attribute may not exist.
|
|
*
|
|
* @param \SAML2\XML\md\EndpointType $element The element which should be parsed.
|
|
*
|
|
* @return array An associative array with the data we have extracted from the element.
|
|
*/
|
|
private static function parseGenericEndpoint(\SAML2\XML\md\EndpointType $element)
|
|
{
|
|
$ep = array();
|
|
|
|
$ep['Binding'] = $element->Binding;
|
|
$ep['Location'] = $element->Location;
|
|
|
|
if ($element->ResponseLocation !== null) {
|
|
$ep['ResponseLocation'] = $element->ResponseLocation;
|
|
}
|
|
|
|
if ($element instanceof \SAML2\XML\md\IndexedEndpointType) {
|
|
$ep['index'] = $element->index;
|
|
|
|
if ($element->isDefault !== null) {
|
|
$ep['isDefault'] = $element->isDefault;
|
|
}
|
|
}
|
|
|
|
return $ep;
|
|
}
|
|
|
|
|
|
/**
|
|
* Extract generic endpoints.
|
|
*
|
|
* @param array $endpoints The endpoints we should parse.
|
|
*
|
|
* @return array Array of parsed endpoints.
|
|
*/
|
|
private static function extractEndpoints(array $endpoints)
|
|
{
|
|
$ret = array();
|
|
foreach ($endpoints as $ep) {
|
|
$ret[] = self::parseGenericEndpoint($ep);
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function parses a KeyDescriptor element. It currently only supports keys with a single
|
|
* X509 certificate.
|
|
*
|
|
* The associative array for a key can contain:
|
|
* - 'encryption': Indicates whether this key can be used for encryption.
|
|
* - 'signing': Indicates whether this key can be used for signing.
|
|
* - 'type: The type of the key. 'X509Certificate' is the only key type we support.
|
|
* - 'X509Certificate': The contents of the first X509Certificate element (if the type is 'X509Certificate ').
|
|
*
|
|
* @param \SAML2\XML\md\KeyDescriptor $kd The KeyDescriptor element.
|
|
*
|
|
* @return array|null An associative array describing the key, or null if this is an unsupported key.
|
|
*/
|
|
private static function parseKeyDescriptor(\SAML2\XML\md\KeyDescriptor $kd)
|
|
{
|
|
$r = array();
|
|
|
|
if ($kd->use === 'encryption') {
|
|
$r['encryption'] = true;
|
|
$r['signing'] = false;
|
|
} elseif ($kd->use === 'signing') {
|
|
$r['encryption'] = false;
|
|
$r['signing'] = true;
|
|
} else {
|
|
$r['encryption'] = true;
|
|
$r['signing'] = true;
|
|
}
|
|
|
|
$keyInfo = $kd->KeyInfo;
|
|
|
|
foreach ($keyInfo->info as $i) {
|
|
if ($i instanceof \SAML2\XML\ds\X509Data) {
|
|
foreach ($i->data as $d) {
|
|
if ($d instanceof \SAML2\XML\ds\X509Certificate) {
|
|
$r['type'] = 'X509Certificate';
|
|
$r['X509Certificate'] = $d->certificate;
|
|
return $r;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function finds SP descriptors which supports one of the given protocols.
|
|
*
|
|
* @param $protocols Array with the protocols we accept.
|
|
*
|
|
* @return Array with SP descriptors which supports one of the given protocols.
|
|
*/
|
|
private function getSPDescriptors($protocols)
|
|
{
|
|
assert(is_array($protocols));
|
|
|
|
$ret = array();
|
|
|
|
foreach ($this->spDescriptors as $spd) {
|
|
$sharedProtocols = array_intersect($protocols, $spd['protocols']);
|
|
if (count($sharedProtocols) > 0) {
|
|
$ret[] = $spd;
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function finds IdP descriptors which supports one of the given protocols.
|
|
*
|
|
* @param $protocols Array with the protocols we accept.
|
|
*
|
|
* @return Array with IdP descriptors which supports one of the given protocols.
|
|
*/
|
|
private function getIdPDescriptors($protocols)
|
|
{
|
|
assert(is_array($protocols));
|
|
|
|
$ret = array();
|
|
|
|
foreach ($this->idpDescriptors as $idpd) {
|
|
$sharedProtocols = array_intersect($protocols, $idpd['protocols']);
|
|
if (count($sharedProtocols) > 0) {
|
|
$ret[] = $idpd;
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function locates the EntityDescriptor node in a DOMDocument. This node should
|
|
* be the first (and only) node in the document.
|
|
*
|
|
* This function will throw an exception if it is unable to locate the node.
|
|
*
|
|
* @param DOMDocument $doc The DOMDocument where we should find the EntityDescriptor node.
|
|
*
|
|
* @return \SAML2\XML\md\EntityDescriptor The DOMEntity which represents the EntityDescriptor.
|
|
* @throws Exception If the document is empty or the first element is not an EntityDescriptor element.
|
|
*/
|
|
private static function findEntityDescriptor($doc)
|
|
{
|
|
assert($doc instanceof DOMDocument);
|
|
|
|
// find the EntityDescriptor DOMElement. This should be the first (and only) child of the DOMDocument
|
|
$ed = $doc->documentElement;
|
|
|
|
if ($ed === null) {
|
|
throw new Exception('Failed to load SAML metadata from empty XML document.');
|
|
}
|
|
|
|
if (SimpleSAML\Utils\XML::isDOMNodeOfType($ed, 'EntityDescriptor', '@md') === false) {
|
|
throw new Exception('Expected first element in the metadata document to be an EntityDescriptor element.');
|
|
}
|
|
|
|
return new \SAML2\XML\md\EntityDescriptor($ed);
|
|
}
|
|
|
|
|
|
/**
|
|
* If this EntityDescriptor was signed this function use the public key to check the signature.
|
|
*
|
|
* @param array $certificates One ore more certificates with the public key. This makes it possible
|
|
* to do a key rollover.
|
|
*
|
|
* @return boolean True if it is possible to check the signature with the certificate, false otherwise.
|
|
* @throws Exception If the certificate file cannot be found.
|
|
*/
|
|
public function validateSignature($certificates)
|
|
{
|
|
foreach ($certificates as $cert) {
|
|
assert(is_string($cert));
|
|
$certFile = \SimpleSAML\Utils\Config::getCertPath($cert);
|
|
if (!file_exists($certFile)) {
|
|
throw new Exception(
|
|
'Could not find certificate file ['.$certFile.'], which is needed to validate signature'
|
|
);
|
|
}
|
|
$certData = file_get_contents($certFile);
|
|
|
|
foreach ($this->validators as $validator) {
|
|
$key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'public'));
|
|
$key->loadKey($certData);
|
|
try {
|
|
if ($validator->validate($key)) {
|
|
return true;
|
|
}
|
|
} catch (Exception $e) {
|
|
// this certificate did not sign this element, skip
|
|
}
|
|
}
|
|
}
|
|
SimpleSAML\Logger::debug('Could not validate signature');
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* This function checks if this EntityDescriptor was signed with a certificate with the
|
|
* given fingerprint.
|
|
*
|
|
* @param string $fingerprint Fingerprint of the certificate which should have been used to sign this
|
|
* EntityDescriptor.
|
|
*
|
|
* @return boolean True if it was signed with the certificate with the given fingerprint, false otherwise.
|
|
*/
|
|
public function validateFingerprint($fingerprint)
|
|
{
|
|
assert(is_string($fingerprint));
|
|
|
|
$fingerprint = strtolower(str_replace(":", "", $fingerprint));
|
|
|
|
$candidates = array();
|
|
foreach ($this->validators as $validator) {
|
|
foreach ($validator->getValidatingCertificates() as $cert) {
|
|
$fp = strtolower(sha1(base64_decode($cert)));
|
|
$candidates[] = $fp;
|
|
if ($fp === $fingerprint) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
SimpleSAML\Logger::debug('Fingerprint was ['.$fingerprint.'] not one of ['.join(', ', $candidates).']');
|
|
return false;
|
|
}
|
|
}
|