From 7afde767295dccb36845b288b701f91ec8737eee Mon Sep 17 00:00:00 2001 From: Olav Morken <olav.morken@uninett.no> Date: Fri, 4 May 2012 08:03:44 +0000 Subject: [PATCH] Add support for the MDUI extension. This patch adds support parsing and generating metadata with the mdui:UIInfo and mdui:DiscoHints elements. Support for generating metadata with the extensions is added to the SAML 2.0 IdP. It should also work through the metadata aggregator. Thanks to Timothy Ace at Synacor, Inc. for implementing this! git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@3088 44740490-163a-0410-bde0-09ae8108e29a --- ...samlphp-metadata-extensions-attributes.txt | 113 ++++++++ docs/simplesamlphp-metadata-extensions-ui.txt | 265 ++++++++++++++++++ lib/SAML2/XML/md/Extensions.php | 4 + lib/SAML2/XML/mdui/DiscoHints.php | 106 +++++++ lib/SAML2/XML/mdui/Keywords.php | 79 ++++++ lib/SAML2/XML/mdui/Logo.php | 94 +++++++ lib/SAML2/XML/mdui/UIInfo.php | 154 ++++++++++ lib/SimpleSAML/Metadata/SAMLBuilder.php | 82 ++++++ lib/SimpleSAML/Metadata/SAMLParser.php | 64 ++++- www/saml2/idp/metadata.php | 12 + 10 files changed, 970 insertions(+), 3 deletions(-) create mode 100644 docs/simplesamlphp-metadata-extensions-attributes.txt create mode 100644 docs/simplesamlphp-metadata-extensions-ui.txt create mode 100644 lib/SAML2/XML/mdui/DiscoHints.php create mode 100644 lib/SAML2/XML/mdui/Keywords.php create mode 100644 lib/SAML2/XML/mdui/Logo.php create mode 100644 lib/SAML2/XML/mdui/UIInfo.php diff --git a/docs/simplesamlphp-metadata-extensions-attributes.txt b/docs/simplesamlphp-metadata-extensions-attributes.txt new file mode 100644 index 000000000..fb9dad61a --- /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 000000000..7585a32cd --- /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 70d7d1cfd..d3d237acc 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 000000000..8ff510d96 --- /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 000000000..0cc2893ea --- /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 000000000..1e327a1db --- /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 000000000..2023abe73 --- /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 3373d0f64..5d21c78cc 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 f5e089d55..8822451ab 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 e9ee76513..e0b8e0db5 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); -- GitLab