diff --git a/docs/simplesamlphp-metadata-extensions-attributes.txt b/docs/simplesamlphp-metadata-extensions-attributes.txt new file mode 100644 index 0000000000000000000000000000000000000000..fb9dad61a7587546420887dea9045b25b810feac --- /dev/null +++ b/docs/simplesamlphp-metadata-extensions-attributes.txt @@ -0,0 +1,113 @@ +SAML V2.0 Metadata Extensions for Login and Discovery User Interface +============================= + +<!-- + This file is written in Markdown syntax. + For more information about how to use the Markdown syntax, read here: + http://daringfireball.net/projects/markdown/syntax +--> + + * Version: `$Id:$` + * Author: Timothy Ace [tace@synacor.com](mailto:tace@synacor.com) + +<!-- {{TOC}} --> + +This is a reference for the SimpleSAMLphp implemenation of the [SAML +V2.0 Attribute Extensions](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-attribute-ext.pdf) +defined by OASIS. + +The `metadata/saml20-idp-hosted.php` entries are used to define the +metadata extension items. An example of this is: + + <?php + $metadata['entity-id-1'] = array( + /* ... */ + 'EntityAttributes' => array( + 'urn:simplesamlphp:v1:simplesamlphp' => array('is', 'really', 'cool'), + '{urn:simplesamlphp:v1}foo' => array('bar'), + ), + /* ... */ + ); + +The OASIS specification primarily defines how to include arbitrary +`Attribute` and `Assertion` elements within the metadata for an IdP. + +*Note*: SimpleSAMLphp does not support `Assertion` elements within the +metadata at this time. + +Defining Attributes +-------------- + +The `EntityAttributes` key is used to define the attributes in the +metadata. Each item in the `EntityAttributes` array defines a new +`<Attribute>` item in the metadata. The value for each key must be an +array. Each item in this array produces a separte `<AttributeValue>` +element within the `<Attribute>` element. + + 'EntityAttributes' => array( + 'urn:simplesamlphp:v1:simplesamlphp' => array('is', 'really', 'cool'), + ), + +This generates: + + <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:simplesamlphp:v1:simplesamlphp" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">is</saml:AttributeValue> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">really</saml:AttributeValue> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">cool</saml:AttributeValue> + </saml:Attribute> + +Each `<Attribute>` element requires a `NameFormat` attribute. This is +specified using curly braces at the beginning of the key name: + + 'EntityAttributes' => array( + '{urn:simplesamlphp:v1}foo' => array('bar'), + ), + +This generates: + + <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="foo" NameFormat="urn:simplesamlphp:v1"> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">bar</saml:AttributeValue> + </saml:Attribute> + +When the curly braces are omitted, the NameFormat is automatically set +to "urn:oasis:names:tc:SAML:2.0:attrname-format:uri". + +Generated XML Metadata Examples +---------------- + +If given the following configuration... + + $metadata['https://www.example.com/saml/saml2/idp/metadata.php'] = array( + 'host' => 'www.example.com', + 'certificate' => 'server.crt', + 'privatekey' => 'server.pem', + 'auth' => 'example-userpass', + + 'EntityAttributes' => array( + 'urn:simplesamlphp:v1:simplesamlphp' => array('is', 'really', 'cool'), + '{urn:simplesamlphp:v1}foo' => array('bar'), + ), + ); + +... will generate the following XML metadata: + + <?xml version="1.0"?> + <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://www.example.com/saml/saml2/idp/metadata.php"> + <md:Extensions> + <mdattr:EntityAttributes xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"> + <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="urn:simplesamlphp:v1:simplesamlphp" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">is</saml:AttributeValue> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">really</saml:AttributeValue> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">cool</saml:AttributeValue> + </saml:Attribute> + <saml:Attribute xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Name="foo" NameFormat="urn:simplesamlphp:v1"> + <saml:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">bar</saml:AttributeValue> + </saml:Attribute> + </mdattr:EntityAttributes> + </md:Extensions> + <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> + <md:KeyDescriptor use="signing"> + <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> + <ds:X509Data> + ... + diff --git a/docs/simplesamlphp-metadata-extensions-ui.txt b/docs/simplesamlphp-metadata-extensions-ui.txt new file mode 100644 index 0000000000000000000000000000000000000000..7585a32cd66e59a8e7bb0ba84d3bfd2f7920e2e4 --- /dev/null +++ b/docs/simplesamlphp-metadata-extensions-ui.txt @@ -0,0 +1,265 @@ +SAML V2.0 Metadata Extensions for Login and Discovery User Interface +============================= + +<!-- + This file is written in Markdown syntax. + For more information about how to use the Markdown syntax, read here: + http://daringfireball.net/projects/markdown/syntax +--> + + * Version: `$Id:$` + * Author: Timothy Ace [tace@synacor.com](mailto:tace@synacor.com) + +<!-- {{TOC}} --> + +This is a reference for the SimpleSAMLphp implemenation of the [SAML +V2.0 Metadata Extensions for Login and Discovery User Interface](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf) +defined by OASIS. + +The `metadata/saml20-idp-hosted.php` entries are used to define the +metadata extension items. An example of this is: + + <?php + $metadata['entity-id-1'] = array( + /* ... */ + 'UIInfo' => array( + 'DisplayName' => array( + 'en' => 'English name', + 'es' => 'Nombre en Español', + ), + 'Description' => array( + 'en' => 'English description', + 'es' => 'DescripciĂłn en Español', + ), + 'InformationURL' => array( + 'en' => 'http://example.com/info/en', + 'es' => 'http://example.com/info/es', + ), + 'PrivacyStatementURL' => array( + 'en' => 'http://example.com/privacy/en', + 'es' => 'http://example.com/privacy/es', + ), + 'Keywords' => array( + 'en' => array('communication', 'federated session'), + 'es' => array('comunicaciĂłn', 'sesiĂłn federated'), + ), + 'Logo' => array( + array( + 'url' => 'http://example.com/logo1.png', + 'height' => 200, + 'width' => 400, + 'lang' => 'en', + ), + array( + 'url' => 'http://example.com/logo2.png', + 'height' => 201, + 'width' => 401, + ), + ), + ), + 'DiscoHints' => array( + 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), + 'DomainHint' => array('example.com', 'www.example.com'), + 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), + ), + /* ... */ + ); + +The OASIS specification primarily defines how an IdP can communicate +metadata related to IdP discovery. There are two different types of +extensions defined. There are the `<mdui:UIInfo>`elements that define +how an IdP should be displayed and there are the `<mdui:DiscoHints>` +elements that define when an IdP should be choosen/displayed. + +UIInfo Items +-------------- + +These elements are used for IdP discovery to determine what to display +about an IdP. These properties are all children of the `UIInfo` key. + +*Note*: Most elements are localized strings that specify the language +using the array key as the language-code: + + 'DisplayName' => array( + 'en' => 'English name', + 'es' => 'Nombre en Español', + ), + +`DisplayName` +: The localized list of names for this IdP + + 'DisplayName' => array( + 'en' => 'English name', + 'es' => 'Nombre en Español', + ), + +`Description` +: The localized list of statements used to decribe this IdP + + 'Description' => array( + 'en' => 'English description', + 'es' => 'DescripciĂłn en Español', + ), + +`InformationURL` +: A localized list of URLs where more information about the IdP is + located. + + 'InformationURL' => array( + 'en' => 'http://example.com/info/en', + 'es' => 'http://example.com/info/es', + ), + +`PrivacyStatementURL` +: A localized list of URLs where the IdP's privacy statement is + located. + + 'PrivacyStatementURL' => array( + 'en' => 'http://example.com/privacy/en', + 'es' => 'http://example.com/privacy/es', + ), + +`Keywords` +: A localized list of keywords used to describe the IdP + + 'Keywords' => array( + 'en' => array('communication', 'federated session'), + 'es' => array('comunicaciĂłn', 'sesiĂłn federated'), + ), + +: *Note*: The `+` (plus) character is forbidden by specification from + being part of a Keyword. + +`Logo` +: The logos used to represent the IdP + + 'Logo' => array( + array( + 'url' => 'http://example.com/logo1.png', + 'height' => 200, + 'width' => 400, + 'lang' => 'en', + ), + array( + 'url' => 'http://example.com/logo2.png', + 'height' => 201, + 'width' => 401, + ), + ), + +: An optional `lang` key containing a language-code is supported for + localized Logos. + +DiscoHints Items +-------------- + +These elements are used for IdP discovery to determine when to choose or +present an IdP. These properties are all children of the `DiscoHints` +key. + +`IPHint` +: This is a list of both IPv4 and IPv6 addresses in CIDR notation + services by or associated with this entity. + + 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), + +`DomainHint` +: This specifies a list of domain names serviced by or associated with + this entity. + + 'DomainHint' => array('example.com', 'www.example.com'), + +`GeolocationHint` +: This specifies a list of geographic coordinates associated with, or + serviced by, the entity. Coordinates are given in URI form using the + geo URI scheme [RFC5870](http://www.ietf.org/rfc/rfc5870.txt). + + 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), + + +Generated XML Metadata Examples +---------------- + +If given the following configuration... + + $metadata['https://www.example.com/saml/saml2/idp/metadata.php'] = array( + 'host' => 'www.example.com', + 'certificate' => 'server.crt', + 'privatekey' => 'server.pem', + 'auth' => 'example-userpass', + + 'UIInfo' => array( + 'DisplayName' => array( + 'en' => 'English name', + 'es' => 'Nombre en Español', + ), + 'Description' => array( + 'en' => 'English description', + 'es' => 'DescripciĂłn en Español', + ), + 'InformationURL' => array( + 'en' => 'http://example.com/info/en', + 'es' => 'http://example.com/info/es', + ), + 'PrivacyStatementURL' => array( + 'en' => 'http://example.com/privacy/en', + 'es' => 'http://example.com/privacy/es', + ), + 'Keywords' => array( + 'en' => array('communication', 'federated session'), + 'es' => array('comunicaciĂłn', 'sesiĂłn federated'), + ), + 'Logo' => array( + array( + 'url' => 'http://example.com/logo1.png', + 'height' => 200, + 'width' => 400, + ), + array( + 'url' => 'http://example.com/logo2.png', + 'height' => 201, + 'width' => 401, + ), + ), + ), + 'DiscoHints' => array( + 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), + 'DomainHint' => array('example.com', 'www.example.com'), + 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), + ), + ); + +... will generate the following XML metadata: + + <?xml version="1.0"?> + <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://www.example.com/saml/saml2/idp/metadata.php"> + <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> + <md:Extensions> + <mdui:UIInfo xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"> + <mdui:DisplayName xml:lang="en">English name</mdui:DisplayName> + <mdui:DisplayName xml:lang="es">Nombre en Español</mdui:DisplayName> + <mdui:Description xml:lang="en">English description</mdui:Description> + <mdui:Description xml:lang="es">Descripción en Español</mdui:Description> + <mdui:InformationURL xml:lang="en">http://example.com/info/en</mdui:InformationURL> + <mdui:InformationURL xml:lang="es">http://example.com/info/es</mdui:InformationURL> + <mdui:PrivacyStatementURL xml:lang="en">http://example.com/privacy/en</mdui:PrivacyStatementURL> + <mdui:PrivacyStatementURL xml:lang="es">http://example.com/privacy/es</mdui:PrivacyStatementURL> + <mdui:Keywords xml:lang="en">communication federated+session</mdui:Keywords> + <mdui:Keywords xml:lang="es">comunicación sesión+federated</mdui:Keywords> + <mdui:Logo width="400" height="200" xml:lang="en">http://example.com/logo1.png</mdui:Logo> + <mdui:Logo width="401" height="201">http://example.com/logo2.png</mdui:Logo> + </mdui:UIInfo> + <mdui:DiscoHints xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"> + <mdui:IPHint>130.59.0.0/16</mdui:IPHint> + <mdui:IPHint>2001:620::0/96</mdui:IPHint> + <mdui:DomainHint>example.com</mdui:DomainHint> + <mdui:DomainHint>www.example.com</mdui:DomainHint> + <mdui:GeolocationHint>geo:47.37328,8.531126</mdui:GeolocationHint> + <mdui:GeolocationHint>geo:19.34343,12.342514</mdui:GeolocationHint> + </mdui:DiscoHints> + </md:Extensions> + <md:KeyDescriptor use="signing"> + <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> + <ds:X509Data> + ... + diff --git a/lib/SAML2/XML/md/Extensions.php b/lib/SAML2/XML/md/Extensions.php index 70d7d1cfdada248f4af09c1c473457ceb1d18c5e..d3d237acc2a25e10d8569aae5148a64d0f833a3c 100644 --- a/lib/SAML2/XML/md/Extensions.php +++ b/lib/SAML2/XML/md/Extensions.php @@ -24,6 +24,10 @@ class SAML2_XML_md_Extensions { $ret[] = new SAML2_XML_mdattr_EntityAttributes($node); } elseif ($node->namespaceURI === SAML2_XML_mdrpi_Common::NS_MDRPI && $node->localName === 'PublicationInfo') { $ret[] = new SAML2_XML_mdrpi_PublicationInfo($node); + } elseif ($node->namespaceURI === SAML2_XML_mdui_UIInfo::NS && $node->localName === 'UIInfo') { + $ret[] = new SAML2_XML_mdui_UIInfo($node); + } elseif ($node->namespaceURI === SAML2_XML_mdui_DiscoHints::NS && $node->localName === 'DiscoHints') { + $ret[] = new SAML2_XML_mdui_DiscoHints($node); } else { $ret[] = new SAML2_XML_Chunk($node); } diff --git a/lib/SAML2/XML/mdui/DiscoHints.php b/lib/SAML2/XML/mdui/DiscoHints.php new file mode 100644 index 0000000000000000000000000000000000000000..8ff510d96fb50a98f6994698248a9ee54bbea46e --- /dev/null +++ b/lib/SAML2/XML/mdui/DiscoHints.php @@ -0,0 +1,106 @@ +<?php + +/** + * Class for handling the metadata extensions for login and discovery user interface + * + * @link: http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_XML_mdui_DiscoHints { + + /** + * The namespace used for the DiscoHints extension. + */ + const NS = 'urn:oasis:names:tc:SAML:metadata:ui'; + + + /** + * Array with child elements. + * + * The elements can be any of the other SAML2_XML_mdui_* elements. + * + * @var array + */ + public $children = array(); + + + /** + * The IPHint, as an array of strings. + * + * @var array + */ + public $IPHint = array(); + + + /** + * The DomainHint, as an array of strings. + * + * @var array + */ + public $DomainHint = array(); + + + /** + * The GeolocationHint, as an array of strings. + * + * @var array + */ + public $GeolocationHint = array(); + + /** + * Create a DiscoHints element. + * + * @param DOMElement|NULL $xml The XML element we should load. + */ + public function __construct(DOMElement $xml = NULL) { + + if ($xml === NULL) { + return; + } + + $this->IPHint = SAML2_Utils::extractStrings($xml, self::NS, 'IPHint'); + $this->DomainHint = SAML2_Utils::extractStrings($xml, self::NS, 'DomainHint'); + $this->GeolocationHint = SAML2_Utils::extractStrings($xml, self::NS, 'GeolocationHint'); + + foreach (SAML2_Utils::xpQuery($xml, "./*[namespace-uri()!='".self::NS."']") as $node) { + $this->children[] = new SAML2_XML_Chunk($node); + } + } + + + /** + * Convert this DiscoHints to XML. + * + * @param DOMElement $parent The element we should append to. + */ + public function toXML(DOMElement $parent) { + assert('is_array($this->IPHint)'); + assert('is_array($this->DomainHint)'); + assert('is_array($this->GeolocationHint)'); + assert('is_array($this->children)'); + + if (!empty($this->IPHint) + || !empty($this->DomainHint) + || !empty($this->GeolocationHint) + || !empty($this->children)) { + $doc = $parent->ownerDocument; + + $e = $doc->createElementNS(self::NS, 'mdui:DiscoHints'); + $parent->appendChild($e); + + if (!empty($this->children)) { + foreach ($this->children as $child) { + $child->toXML($e); + } + } + + SAML2_Utils::addStrings($e, self::NS, 'mdui:IPHint', FALSE, $this->IPHint); + SAML2_Utils::addStrings($e, self::NS, 'mdui:DomainHint', FALSE, $this->DomainHint); + SAML2_Utils::addStrings($e, self::NS, 'mdui:GeolocationHint', FALSE, $this->GeolocationHint); + + return $e; + } + } + +} diff --git a/lib/SAML2/XML/mdui/Keywords.php b/lib/SAML2/XML/mdui/Keywords.php new file mode 100644 index 0000000000000000000000000000000000000000..0cc2893ea8629fa0dc0601570a25d06c7cfecbba --- /dev/null +++ b/lib/SAML2/XML/mdui/Keywords.php @@ -0,0 +1,79 @@ +<?php + +/** + * Class for handling the Keywords metadata extensions for login and discovery user interface + * + * @link: http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_XML_mdui_Keywords { + + /** + * The keywords of this item. + * + * @var string + */ + public $Keywords; + + + /** + * The language of this item. + * + * @var string + */ + public $lang; + + + /** + * Initialize a Keywords. + * + * @param DOMElement|NULL $xml The XML element we should load. + */ + public function __construct(DOMElement $xml = NULL) { + + if ($xml === NULL) { + return; + } + + if (!$xml->hasAttribute('xml:lang')) { + throw new Exception('Missing lang on Keywords.'); + } + if (!is_string($xml->textContent) || !strlen($xml->textContent)) { + throw new Exception('Missing value for Keywords.'); + } + $this->Keywords = array(); + foreach (explode(' ', $xml->textContent) as $keyword) { + $this->Keywords[] = str_replace('+', ' ', $keyword); + } + $this->lang = $xml->getAttribute('xml:lang'); + } + + + /** + * Convert this Keywords to XML. + * + * @param DOMElement $parent The element we should append this Keywords to. + */ + public function toXML(DOMElement $parent) { + assert('is_string($this->lang)'); + assert('is_array($this->Keywords)'); + + $doc = $parent->ownerDocument; + + $e = $doc->createElementNS(SAML2_XML_mdui_UIInfo::NS, 'mdui:Keywords'); + $e->setAttribute('xml:lang', $this->lang); + $e->nodeValue = ''; + foreach ($this->Keywords as $keyword) { + if (strpos($keyword, "+") !== false) { + throw new Exception('Keywords may not contain a "+" character.'); + } + $e->nodeValue .= str_replace(' ', '+', $keyword) . ' '; + } + $e->nodeValue = rtrim($e->nodeValue); + $parent->appendChild($e); + + return $e; + } + +} diff --git a/lib/SAML2/XML/mdui/Logo.php b/lib/SAML2/XML/mdui/Logo.php new file mode 100644 index 0000000000000000000000000000000000000000..1e327a1db0995f9b58c8bb615a61d24a3fb09eb7 --- /dev/null +++ b/lib/SAML2/XML/mdui/Logo.php @@ -0,0 +1,94 @@ +<?php + +/** + * Class for handling the Logo metadata extensions for login and discovery user interface + * + * @link: http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_XML_mdui_Logo { + + /** + * The url of this logo. + * + * @var string + */ + public $url; + + + /** + * The width of this logo. + * + * @var string + */ + public $width; + + + /** + * The height of this logo. + * + * @var string + */ + public $height; + + /** + * The language of this item. + * + * @var string + */ + public $lang; + + + /** + * Initialize a Logo. + * + * @param DOMElement|NULL $xml The XML element we should load. + */ + public function __construct(DOMElement $xml = NULL) { + + if ($xml === NULL) { + return; + } + + if (!$xml->hasAttribute('width')) { + throw new Exception('Missing width of Logo.'); + } + if (!$xml->hasAttribute('height')) { + throw new Exception('Missing height of Logo.'); + } + if (!is_string($xml->textContent) || !strlen($xml->textContent)) { + throw new Exception('Missing url value for Logo.'); + } + $this->url = $xml->textContent; + $this->width = (int)$xml->getAttribute('width'); + $this->height = (int)$xml->getAttribute('height'); + $this->lang = $xml->hasAttribute('xml:lang') ? $xml->getAttribute('xml:lang') : NULL; + } + + + /** + * Convert this Logo to XML. + * + * @param DOMElement $parent The element we should append this Logo to. + */ + public function toXML(DOMElement $parent) { + assert('is_int($this->width)'); + assert('is_int($this->height)'); + assert('is_string($this->url)'); + + $doc = $parent->ownerDocument; + + $e = $doc->createElementNS(SAML2_XML_mdui_UIInfo::NS, 'mdui:Logo'); + $e->nodeValue = $this->url; + $e->setAttribute('width', (int)$this->width); + $e->setAttribute('height', (int)$this->height); + if (isset($this->lang)) { + $e->setAttribute('xml:lang', $this->lang); + } + $parent->appendChild($e); + + return $e; + } + +} diff --git a/lib/SAML2/XML/mdui/UIInfo.php b/lib/SAML2/XML/mdui/UIInfo.php new file mode 100644 index 0000000000000000000000000000000000000000..2023abe73884aa2d68b20858c65531cff5e0b60f --- /dev/null +++ b/lib/SAML2/XML/mdui/UIInfo.php @@ -0,0 +1,154 @@ +<?php + +/** + * Class for handling the metadata extensions for login and discovery user interface + * + * @link: http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_XML_mdui_UIInfo { + + /** + * The namespace used for the UIInfo extension. + */ + const NS = 'urn:oasis:names:tc:SAML:metadata:ui'; + + + /** + * Array with child elements. + * + * The elements can be any of the other SAML2_XML_mdui_* elements. + * + * @var array + */ + public $children = array(); + + /** + * The DisplayName, as an array of language => translation. + * + * @var array + */ + public $DisplayName = array(); + + /** + * The Description, as an array of language => translation. + * + * @var array + */ + public $Description = array(); + + /** + * The InformationURL, as an array of language => url. + * + * @var array + */ + public $InformationURL = array(); + + /** + * The PrivacyStatementURL, as an array of language => url. + * + * @var array + */ + public $PrivacyStatementURL = array(); + + /** + * The Keywords, as an array of language => array of strings. + * + * @var array + */ + public $Keywords = array(); + + /** + * The Logo, as an array of associative arrays containing url, width, height, and optional lang. + * + * @var array + */ + public $Logo = array(); + + + /** + * Create a UIInfo element. + * + * @param DOMElement|NULL $xml The XML element we should load. + */ + public function __construct(DOMElement $xml = NULL) { + + if ($xml === NULL) { + return; + } + + $this->DisplayName = SAML2_Utils::extractLocalizedStrings($xml, self::NS, 'DisplayName'); + $this->Description = SAML2_Utils::extractLocalizedStrings($xml, self::NS, 'Description'); + $this->InformationURL = SAML2_Utils::extractLocalizedStrings($xml, self::NS, 'InformationURL'); + $this->PrivacyStatementURL = SAML2_Utils::extractLocalizedStrings($xml, self::NS, 'PrivacyStatementURL'); + + foreach (SAML2_Utils::xpQuery($xml, './*') as $node) { + if ($node->namespaceURI === self::NS) { + switch ($node->localName) { + case 'Keywords': + $this->Keywords[] = new SAML2_XML_mdui_Keywords($node); + break; + case 'Logo': + $this->Logo[] = new SAML2_XML_mdui_Logo($node); + break; + } + } else { + $this->children[] = new SAML2_XML_Chunk($node); + } + } + } + + + /** + * Convert this UIInfo to XML. + * + * @param DOMElement $parent The element we should append to. + */ + public function toXML(DOMElement $parent) { + assert('is_array($this->DisplayName)'); + assert('is_array($this->InformationURL)'); + assert('is_array($this->PrivacyStatementURL)'); + assert('is_array($this->Keywords)'); + assert('is_array($this->Logo)'); + assert('is_array($this->children)'); + + if (!empty($this->DisplayName) + || !empty($this->Description) + || !empty($this->InformationURL) + || !empty($this->PrivacyStatementURL) + || !empty($this->Keywords) + || !empty($this->Logo) + || !empty($this->children)) { + $doc = $parent->ownerDocument; + + $e = $doc->createElementNS(self::NS, 'mdui:UIInfo'); + $parent->appendChild($e); + + SAML2_Utils::addStrings($e, self::NS, 'mdui:DisplayName', TRUE, $this->DisplayName); + SAML2_Utils::addStrings($e, self::NS, 'mdui:Description', TRUE, $this->Description); + SAML2_Utils::addStrings($e, self::NS, 'mdui:InformationURL', TRUE, $this->InformationURL); + SAML2_Utils::addStrings($e, self::NS, 'mdui:PrivacyStatementURL', TRUE, $this->PrivacyStatementURL); + + if (!empty($this->Keywords)) { + foreach ($this->Keywords as $child) { + $child->toXML($e); + } + } + + if (!empty($this->Logo)) { + foreach ($this->Logo as $child) { + $child->toXML($e); + } + } + + if (!empty($this->children)) { + foreach ($this->children as $child) { + $child->toXML($e); + } + } + } + return $e; + } + +} diff --git a/lib/SimpleSAML/Metadata/SAMLBuilder.php b/lib/SimpleSAML/Metadata/SAMLBuilder.php index 3373d0f64987df7419d88c73ae0a84efd071dda8..5d21c78cc416f552c6693b9e3ea704f319f08164 100644 --- a/lib/SimpleSAML/Metadata/SAMLBuilder.php +++ b/lib/SimpleSAML/Metadata/SAMLBuilder.php @@ -118,6 +118,88 @@ class SimpleSAML_Metadata_SAMLBuilder { $e->Extensions[] = $s; } } + + if ($metadata->hasValue('EntityAttributes')) { + $ea = new SAML2_XML_mdattr_EntityAttributes(); + foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) { + $a = new SAML2_XML_saml_Attribute(); + $a->Name = $attributeName; + $a->NameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'; + + // Attribute names that is not URI is prefixed as this: '{nameformat}name' + if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) { + $a->Name = $matches[2]; + $nameFormat = $matches[1]; + if ($nameFormat !== SAML2_Const::NAMEFORMAT_UNSPECIFIED) { + $a->NameFormat = $nameFormat; + } + } + foreach ($attributeValues as $attributeValue) { + $a->AttributeValue[] = new SAML2_XML_saml_AttributeValue($attributeValue); + } + $ea->children[] = $a; + } + $this->entityDescriptor->Extensions[] = $ea; + } + + if ($metadata->hasValue('UIInfo')) { + $ui = new SAML2_XML_mdui_UIInfo(); + foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) { + switch ($uiName) { + case 'DisplayName': + $ui->DisplayName = $uiValues; + break; + case 'Description': + $ui->Description = $uiValues; + break; + case 'InformationURL': + $ui->InformationURL = $uiValues; + break; + case 'PrivacyStatementURL': + $ui->PrivacyStatementURL = $uiValues; + break; + case 'Keywords': + foreach ($uiValues as $lang => $keywords) { + $uiItem = new SAML2_XML_mdui_Keywords(); + $uiItem->lang = $lang; + $uiItem->Keywords = $keywords; + $ui->Keywords[] = $uiItem; + } + break; + case 'Logo': + foreach ($uiValues as $logo) { + $uiItem = new SAML2_XML_mdui_Logo(); + $uiItem->url = $logo['url']; + $uiItem->width = $logo['width']; + $uiItem->height = $logo['height']; + if (isset($logo['lang'])) { + $uiItem->lang = $logo['lang']; + } + $ui->Logo[] = $uiItem; + } + break; + } + } + $e->Extensions[] = $ui; + } + + if ($metadata->hasValue('DiscoHints')) { + $dh = new SAML2_XML_mdui_DiscoHints(); + foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) { + switch ($dhName) { + case 'IPHint': + $dh->IPHint = $dhValues; + break; + case 'DomainHint': + $dh->DomainHint = $dhValues; + break; + case 'GeolocationHint': + $dh->GeolocationHint = $dhValues; + break; + } + } + $e->Extensions[] = $dh; + } } diff --git a/lib/SimpleSAML/Metadata/SAMLParser.php b/lib/SimpleSAML/Metadata/SAMLParser.php index f5e089d5568d3ff767a4af6cd8bd129a37bca638..8822451abd3cd5cbf76bcdfbf0d1d16953718e3f 100644 --- a/lib/SimpleSAML/Metadata/SAMLParser.php +++ b/lib/SimpleSAML/Metadata/SAMLParser.php @@ -97,6 +97,8 @@ class SimpleSAML_Metadata_SAMLParser { private $entityAttributes; private $attributes; private $tags; + private $uiInfo; + private $discoHints; /** @@ -427,7 +429,14 @@ class SimpleSAML_Metadata_SAMLParser { if (!empty($this->entityAttributes)) { $metadata['EntityAttributes'] = $this->entityAttributes; } - + + if (!empty($roleDescriptor['UIInfo'])) { + $metadata['UIInfo'] = $roleDescriptor['UIInfo']; + } + + if (!empty($roleDescriptor['DiscoHints'])) { + $metadata['DiscoHints'] = $roleDescriptor['DiscoHints']; + } } @@ -739,6 +748,8 @@ class SimpleSAML_Metadata_SAMLParser { $ret['scope'] = $ext['scope']; $ret['tags'] = $ext['tags']; $ret['EntityAttributes'] = $ext['EntityAttributes']; + $ret['UIInfo'] = $ext['UIInfo']; + $ret['DiscoHints'] = $ext['DiscoHints']; return $ret; } @@ -861,6 +872,8 @@ class SimpleSAML_Metadata_SAMLParser { 'scope' => array(), 'tags' => array(), 'EntityAttributes' => array(), + 'UIInfo' => array(), + 'DiscoHints' => array(), ); foreach ($element->Extensions as $e) { @@ -869,7 +882,7 @@ class SimpleSAML_Metadata_SAMLParser { $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) { @@ -901,7 +914,52 @@ class SimpleSAML_Metadata_SAMLParser { } } } - + + // 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)) { diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php index e9ee76513211920742632a4cae8116e0f7d6a83b..e0b8e0db55a0743f08e2ab7f547ae6ddcabb954f 100644 --- a/www/saml2/idp/metadata.php +++ b/www/saml2/idp/metadata.php @@ -105,6 +105,18 @@ try { $metaArray['scope'] = $idpmeta->getArray('scope'); } + if ($idpmeta->hasValue('EntityAttributes')) { + $metaArray['EntityAttributes'] = $idpmeta->getArray('EntityAttributes'); + } + + if ($idpmeta->hasValue('UIInfo')) { + $metaArray['UIInfo'] = $idpmeta->getArray('UIInfo'); + } + + if ($idpmeta->hasValue('DiscoHints')) { + $metaArray['DiscoHints'] = $idpmeta->getArray('DiscoHints'); + } + $metaflat = '$metadata[' . var_export($idpentityid, TRUE) . '] = ' . var_export($metaArray, TRUE) . ';'; $metaBuilder = new SimpleSAML_Metadata_SAMLBuilder($idpentityid);