diff --git a/config-templates/config.php b/config-templates/config.php index 9df78a4a22a550b8843758ac6197710b310753b8..9136e0545c970f1f313c273a2db96ccc79032ddb 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -240,6 +240,12 @@ $config = array ( */ 'session.cookie.secure' => FALSE, + /* + * When set to FALSE fallback to transient session on session initialization + * failure, throw exception otherwise. + */ + 'session.disable_fallback' => FALSE, + /* * Enable secure POST from HTTPS to HTTP. * diff --git a/docs/simplesamlphp-changelog.txt b/docs/simplesamlphp-changelog.txt index ea17b82ea557585bbac5be584de42c9f622e468c..62554114eb435fb56f5c2f4428ad848226e57a49 100644 --- a/docs/simplesamlphp-changelog.txt +++ b/docs/simplesamlphp-changelog.txt @@ -8,7 +8,7 @@ See the upgrade notes for specific information about upgrading. ## Version 1.9 -Released 2012-04-XX. +Released 2012-05-XX. * Restructure error templates to share a common base template. * Warnings about URL length limits from Suhosin PHP extension. @@ -53,6 +53,14 @@ Released 2012-04-XX. * Fix invalid HTML for login pages where username is set. * Remove unecessary check for PHP version >= 5.2 when setting cookies. * Better error message when a module is missing a default-enable or default-disable file. + * Support for validating RSA-SHA256 signatures. + +### `aselect` + + * New module that replaces the previous module. + * Better error handling. + * Support for request signing. + * Loses support for A-Select Cross. ### `authcrypt` @@ -135,6 +143,10 @@ Released 2012-04-XX. * Update to latest version of the OpenID library. * Support for sending authentication requests via GET requests (with the prefer_http_redirect option). +### `radius` + + * Support for setting the "NAS-Identifier" attribute. + ### `saml` * Preserve ID-attributes on elements during signing. (Makes it possible to change the binding for some messages.) @@ -153,11 +165,14 @@ Released 2012-04-XX. * IdP: Add `saml:AllowCreate` to the state array. This makes it possible to access this parameter from authentication processing filters. * IdP: Sign the artifact response message. * IdP: Allow the "host" metadata option to include more than one path element. + * IdP: Support for generating metadata with MDUI extension elements. * SP: Use the discojuice-module as a discovery service if it is enabled. * SP: Add `saml:idp`-parameter to trigger login to a specific IdP to as_login.php. * SP: Do not display error on duplicate response when we have a valid session. * SP: Fix for logout after IdP initiated authentication. * SP: Fix handling of authentication response without a saml:Issuer element. + * SP: Support for specifying required attributes in metadata. + * SP: Support for limiting the AssertionConsumerService endpoints listed in metadata. * `saml:PersistentNameID`: Fail when the user has more than one value in the user ID attribute. * `saml:SQLPersistentNameID`: Persistent NameID stored in a SQL database. * `saml:AuthnContextClassRef`: New filter to set the AuthnContextClassRef in responses. 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/docs/simplesamlphp-upgrade-notes-1.9.txt b/docs/simplesamlphp-upgrade-notes-1.9.txt index 865abdd2923463ec638cc62e0d2b07c701c8194b..3f655b45e9da1e726de4c7b8c6568a338d1a2634 100644 --- a/docs/simplesamlphp-upgrade-notes-1.9.txt +++ b/docs/simplesamlphp-upgrade-notes-1.9.txt @@ -7,3 +7,4 @@ Upgrade notes for simpleSAMLphp 1.9 * Access permissions of generated files are now restricted to the current user. * The code to set cookies now requires PHP version >= 5.2. (PHP version 5.2.0 or newer has been the only supported version for a while, but it has in some cases been possible to run simpleSAMLphp with older versions.) * It used to be possible to set an array of endpoints for the SingleSignOnService in `saml20-idp-hosted.php`. That is no longer supported. + * The `aselect` module has been replaced with a new module. The new module gives us better error handling and support for request signing, but we lose support for A-Select Cross. 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/lib/SimpleSAML/Session.php b/lib/SimpleSAML/Session.php index 49339247ff39250c8c21c419ff3db77c443561a1..74390edfdd03a57702b4828602336005d498b10a 100644 --- a/lib/SimpleSAML/Session.php +++ b/lib/SimpleSAML/Session.php @@ -45,6 +45,14 @@ class SimpleSAML_Session { private $sessionId; + /** + * Transient session flag. + * + * @var boolean|FALSE + */ + private $transient = FALSE; + + /** * The track id is a new random unique identifier that is generate for each session. * This is used in the debug logs and error messages to easily track more information @@ -150,6 +158,7 @@ class SimpleSAML_Session { if ($transient) { $this->trackid = 'XXXXXXXXXX'; + $this->transient = TRUE; return; } @@ -249,14 +258,21 @@ class SimpleSAML_Session { try { self::$instance = self::getSession(); } catch (Exception $e) { + /* For some reason, we were unable to initialize this session. Use a transient session instead. */ + self::useTransientSession(); + + $globalConfig = SimpleSAML_Configuration::getInstance(); + if ($globalConfig->getBoolean('session.disable_fallback', FALSE) === TRUE) { + throw $e; + } + if ($e instanceof SimpleSAML_Error_Exception) { SimpleSAML_Logger::error('Error loading session:'); $e->logError(); } else { SimpleSAML_Logger::error('Error loading session: ' . $e->getMessage()); } - /* For some reason, we were unable to initialize this session. Use a transient session instead. */ - self::useTransientSession(); + return self::$instance; } @@ -299,6 +315,16 @@ class SimpleSAML_Session { } + /** + * Retrieve if session is transient. + * + * @return boolean The session transient flag. + */ + public function isTransient() { + return $this->transient; + } + + /** * Get a unique ID that will be permanent for this session. * Used for debugging and tracing log files related to a session. diff --git a/lib/SimpleSAML/Utilities.php b/lib/SimpleSAML/Utilities.php index f2c7144460fa363987982af71d971a53c6ee8e74..e6c80c4b3e01e8aa00a301daf31245aa55c8f663 100644 --- a/lib/SimpleSAML/Utilities.php +++ b/lib/SimpleSAML/Utilities.php @@ -128,7 +128,11 @@ class SimpleSAML_Utilities { */ private static function getServerPort() { - $portnumber = $_SERVER["SERVER_PORT"]; + if (isset($_SERVER["SERVER_PORT"])) { + $portnumber = $_SERVER["SERVER_PORT"]; + } else { + $portnumber = 80; + } $port = ':' . $portnumber; if (self::getServerHTTPS()) { @@ -2179,19 +2183,24 @@ class SimpleSAML_Utilities { // Data and headers. if ($getHeaders) { - $headers = array(); - - foreach($http_response_header as $h) { - if(preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { - $headers = array(); // reset - $headers[0] = $h; - continue; - } - $bits = explode(':', $h, 2); - if(count($bits) === 2) { - $headers[strtolower($bits[0])] = trim($bits[1]); + if (isset($http_response_header)) { + $headers = array(); + foreach($http_response_header as $h) { + if(preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { + $headers = array(); // reset + $headers[0] = $h; + continue; + } + $bits = explode(':', $h, 2); + if(count($bits) === 2) { + $headers[strtolower($bits[0])] = trim($bits[1]); + } } + } else { + /* No HTTP headers - probably a different protocol, e.g. file. */ + $headers = NULL; } + return array($data, $headers); } diff --git a/lib/SimpleSAML/XHTML/EMail.php b/lib/SimpleSAML/XHTML/EMail.php index 4c5385e5ef2d1ee1d907add08fe93ed421279b5c..f167413895ef22f6a1b9da5421d09f6768aa8d77 100644 --- a/lib/SimpleSAML/XHTML/EMail.php +++ b/lib/SimpleSAML/XHTML/EMail.php @@ -90,7 +90,7 @@ Content-Transfer-Encoding: 8bit --simplesamlphp-' . $random_hash . '-- '; - $headers = join("\r\n", $this->headers); + $headers = implode("\n", $this->headers); $mail_sent = @mail($this->to, $this->subject, $message, $headers); SimpleSAML_Logger::debug('Email: Sending e-mail to [' . $this->to . '] : ' . ($mail_sent ? 'OK' : 'Failed')); diff --git a/modules/aselect/docs/aselect.txt b/modules/aselect/docs/aselect.txt index 9e3f91cb1c70b56ade5840c7737923695470a911..756798e12fc3a23eb183a483b7e9f1eba29d816d 100644 --- a/modules/aselect/docs/aselect.txt +++ b/modules/aselect/docs/aselect.txt @@ -1,64 +1,44 @@ -Using the A-Select authentication source with simpleSAMLphp -=========================================================== +A-Select module for simpleSAMLphp +--------------------------------- -This authentication source for A-Select is based on the a-select -handler by Hans Zandbelt. The original source combines the possibility -to use an A-Select server as an authentication source and the possibility -to use simpleSAMLphp as an A-Select server. This module only acts as a -authentication source. Signing is not (yet) supported. +This module allows one to use an A-Select server as authentication +source for simpleSAMLphp. -The structure applied follows the structure of the CAS authentication source. +The module supports the A-Select protocol, including signing of +requests. Not supported is A-Select Cross. +Usage: -Setting up the A-Select authentication module ----------------------------------------------- +Enable the module if not already enabled: +$ touch modules/aselect/enabled -The first thing you need to do is to enable the aselect module: +In config/authsources.php, configure your A-Selectserver as an +authentication source. The following is an example for a source +named 'aselect': - touch modules/aselect/enable - -The A-Select authentication module has two modes of operation. - -1. The module can act to the A-Select server as an application. - -Configuration in A-Select: - - <application id="app1" level="30"> - <attribute_policy>policyA</attribute_policy> - <forced_authenticate>false</forced_authenticate> - </application> - -Configuration in authsources.php - - 'example-aselect' => array( + 'aselect' => array( 'aselect:aselect', - 'serverurl' => 'http://a-select.dev.han.nl:8080/aselectserver/server', - 'serverid' => 'hanaselect', - 'type' => 'app', # type = app/cross - 'app_id' => 'app1', # only if type = app + 'app_id' => 'simplesamlphp', + 'server_id' => 'sso.example.com', + 'server_url' => 'https://test.sso.example.com/server', + 'private_key' => 'file:///etc/ssl/private/aselect.key' ), -2. The module can act to the A-Select server as cross A-Select. +The parameters: +- app_id: the application I for simpleSAMLphp as configured in + your A-Select server; +- server_id: the A-Select server ID as configured in your + A-Select server; +- server_url: the URL for your A-Selectserver, usually ends in + '/server/. +- private_key: the key you want to use for signing requests. + If you're really sure you do not want request signing, you + can set this option to a null value. +Options 'serverurl' and 'serverid' (without underscore) are +supported for backwards compatibility. -Configuration in A-Select: - - <cross_aselect> - <local_servers require_signing="false"> - <organization id="simpleSAMLphp" server="sso.testorg.com" attribute_policy="policyA"> - </organization> - </local_servers> - </cross_aselect> - - -Configuration in authsources.php - - 'example-aselect' => array( - 'aselect:aselect', - 'serverurl' => 'http://a-select.dev.han.nl:8080/aselectserver/server', - 'serverid' => 'hanaselect', - 'type' => 'cross', # type = app/cross - 'local_organization' => 'simpleSAMLphp', # only if type = cross - 'required_level' => 10, # only if type = cross, defaults to 10 - ), +Author: Wessel Dankers <wsl@uvt.nl> +Copyright: © 2011,2012 Tilburg University (http://www.tilburguniversity.edu) +License: GPL version 3 or any later version. diff --git a/modules/aselect/lib/Auth/Source/aselect.php b/modules/aselect/lib/Auth/Source/aselect.php index 2cba46470614d2277d8ccc65bb4536a501cade3d..c7cb88d2f789b7a370a6c8924c760dc4ee8bf531 100644 --- a/modules/aselect/lib/Auth/Source/aselect.php +++ b/modules/aselect/lib/Auth/Source/aselect.php @@ -1,30 +1,15 @@ <?php /** - * A-Select authentication source. + * Authentication module which acts as an A-Select client * - * Based on www/aselect/handler.php by Hans Zandbelt, SURFnet BV. <hans.zandbelt@surfnet.nl> - * - * @author Patrick Honing, Hogeschool van Arnhem en Nijmegen. <Patrick.Honing@han.nl> - * @package simpleSAMLphp - * @version $Id$ + * @author Wessel Dankers, Tilburg University */ class sspmod_aselect_Auth_Source_aselect extends SimpleSAML_Auth_Source { - - /** - * The string used to identify our states. - */ - const STAGE_INIT = 'aselect:init'; - - /** - * The key of the AuthId field in the state. - */ - const AUTHID = 'aselect:AuthId'; - - /** - * @var array with aselect configuration - */ - private $asconfig; + private $app_id = 'simplesamlphp'; + private $server_id; + private $server_url; + private $private_key; /** * Constructor for this authentication source. @@ -33,142 +18,186 @@ class sspmod_aselect_Auth_Source_aselect extends SimpleSAML_Auth_Source { * @param array $config Configuration. */ public function __construct($info, $config) { - assert('is_array($info)'); - assert('is_array($config)'); - /* Call the parent constructor first, as required by the interface. */ parent::__construct($info, $config); - if (!array_key_exists('serverurl', $config)) throw new Exception('aselect serverurl not specified'); - $this->asconfig['serverurl'] = $config['serverurl']; + $cfg = SimpleSAML_Configuration::loadFromArray($config, + 'Authentication source ' . var_export($this->authId, true)); - if (!array_key_exists('serverid', $config)) throw new Exception('aselect serverid not specified'); - $this->asconfig['serverid'] = $config['serverid']; + $cfg->getValueValidate('type', array('app'), 'app'); + $this->app_id = $cfg->getString('app_id'); + $this->private_key = $cfg->getString('private_key', null); - if (!array_key_exists('type', $config)) throw new Exception('aselect type not specified'); - $this->asconfig['type'] = $config['type']; + // accept these arguments with '_' for consistency + // accept these arguments without '_' for backwards compatibility + $this->server_id = $cfg->getString('serverid', null); + if($this->server_id === null) + $this->server_id = $cfg->getString('server_id'); - if ($this->asconfig['type'] == 'app') { - if (!array_key_exists('app_id', $config)) throw new Exception('aselect app_id not specified'); - $this->asconfig['app_id'] = $config['app_id']; - } elseif($this->asconfig['type'] == 'cross') { - if (!array_key_exists('local_organization', $config)) throw new Exception('aselect local_organization not specified'); - $this->asconfig['local_organization'] = $config['local_organization']; + $this->server_url = $cfg->getString('serverurl', null); + if($this->server_url === null) + $this->server_url = $cfg->getString('server_url'); + } - $this->asconfig['required_level'] = (array_key_exists('required_level', $config)) ? $config['required_level'] : 10; - } else { - throw new Exception('aselect type need to be either app or cross'); - } + /** + * Initiate authentication. + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(&$state) { + $state['aselect::authid'] = $this->authId; + $id = SimpleSAML_Auth_State::saveState($state, 'aselect:login', true); + + try { + $app_url = SimpleSAML_Module::getModuleURL('aselect/credentials.php', array('ssp_state' => $id)); + $as_url = $this->request_authentication($app_url); + SimpleSAML_Utilities::redirect($as_url); + } catch(Exception $e) { + // attach the exception to the state + SimpleSAML_Auth_State::throwException($state, $e); + } } + /** + * Sign a string using the configured private key + * + * @param string $str The string to calculate a signature for + */ + private function base64_signature($str) { + $key = openssl_pkey_get_private($this->private_key); + if($key === false) + throw new SimpleSAML_Error_Exception("Unable to load private key: ".openssl_error_string()); + if(!openssl_sign($str, $sig, $key)) + throw new SimpleSAML_Error_Exception("Unable to create signature: ".openssl_error_string()); + openssl_pkey_free($key); + return base64_encode($sig); + } - // helper function for sending a non-browser request to a remote server - function as_call($url) { - $ch = curl_init(); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_URL, $url); - $result = curl_exec($ch); - $error = curl_error($ch); - curl_close($ch); - if ($result == FALSE) { - throw new Exception('Request on remote server failed: ' . $error); - } - $parms = array(); - foreach (explode('&', $result) as $parm) { - $tuple = explode('=', $parm); - $parms[urldecode($tuple[0])] = urldecode($tuple[1]); - } - if ($parms['result_code'] != '0000') { - throw new Exception('Request on remote server returned error: ' . $result); + /** + * Parse a base64 encoded attribute blob. Can't use parse_str() because it + * may contain multi-valued attributes. + * + * @param string $base64 The base64 string to decode. + */ + private static function decode_attributes($base64) { + $blob = base64_decode($base64, true); + if($blob === false) + throw new SimpleSAML_Error_Exception("Attributes parameter base64 malformed"); + $pairs = explode('&', $blob); + $ret = array(); + foreach($pairs as $pair) { + $keyval = explode('=', $pair, 2); + if(count($keyval) < 2) + throw new SimpleSAML_Error_Exception("Missing value in attributes parameter"); + $key = urldecode($keyval[0]); + $val = urldecode($keyval[1]); + $ret[$key][] = $val; } - return $parms; + return $ret; } + /** + * Default options for curl invocations. + */ + private static $curl_options = array( + CURLOPT_BINARYTRANSFER => true, + CURLOPT_FAILONERROR => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 1, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => "simpleSAMLphp", + ); /** - * Log-in using A-Select + * Create a (possibly signed) URL to contact the A-Select server. * - * @param array &$state Information about the current authentication. + * @param string $request The name of the request (authenticate / verify_credentials). + * @param array $parameters The parameters to pass for this request. */ - public function authenticate(&$state) { - assert('is_array($state)'); - - /* We are going to need the authId in order to retrieve this authentication source later. */ - $state[self::AUTHID] = $this->authId; - - $stateID = SimpleSAML_Auth_State::saveState($state, self::STAGE_INIT); - - $serviceUrl = SimpleSAML_Module::getModuleURL('aselect/linkback.php', array('stateID' => $stateID)); - - if ($this->asconfig['type'] == 'app') { - $params = array( - 'request' => 'authenticate', - 'a-select-server' => $this->asconfig['serverid'], - 'app_id' => $this->asconfig['app_id'], - 'app_url' => $serviceUrl, - ); - } else { // type = cross - $params = array( - 'request' => 'authenticate', - 'a-select-server' => $this->asconfig['serverid'], - 'local_organization' => $this->asconfig['local_organization'], - 'required_level' => $this->asconfig['required_level'], - 'local_as_url' => $serviceUrl, - - ); + private function create_aselect_url($request, $parameters) { + $parameters['request'] = $request; + $parameters['a-select-server'] = $this->server_id; + if(!is_null($this->private_key)) { + $signable = ''; + foreach(array('a-select-server', 'app_id', 'app_url', 'aselect_credentials', 'rid') as $p) + if(array_key_exists($p, $parameters)) + $signable .= $parameters[$p]; + $parameters['signature'] = $this->base64_signature($signable); } - $url = SimpleSAML_Utilities::addURLparameter($this->asconfig['serverurl'],$params); + return SimpleSAML_Utilities::addURLparameter($this->server_url, $parameters); + } + + /** + * Contact the A-Select server and return the result as an associative array. + * + * @param string $request The name of the request (authenticate / verify_credentials). + * @param array $parameters The parameters to pass for this request. + */ + private function call_aselect($request, $parameters) { + $url = $this->create_aselect_url($request, $parameters); + + $curl = curl_init($url); + if($curl === false) + throw new SimpleSAML_Error_Exception("Unable to create CURL handle"); + + if(!curl_setopt_array($curl, self::$curl_options)) + throw new SimpleSAML_Error_Exception("Unable to set CURL options: ".curl_error($curl)); + + $str = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if($str === false) + throw new SimpleSAML_Error_Exception("Unable to retrieve URL: $error"); - $parm = $this->as_call($url); + parse_str($str, $res); - SimpleSAML_Utilities::redirect( - $parm['as_url'], - array( - 'rid' => $parm['rid'], - 'a-select-server' => $this->asconfig['serverid'], - ) - ); + // message is only available with some A-Select server implementations + if($res['result_code'] != '0000') + if(array_key_exists('message', $res)) + throw new SimpleSAML_Error_Exception("Unable to contact SSO service: result_code=".$res['result_code']." message=".$res['message']); + else + throw new SimpleSAML_Error_Exception("Unable to contact SSO service: result_code=".$res['result_code']); + unset($res['result_code']); + + return $res; } - public function finalStep(&$state) { - $credentials = $state['aselect:credentials']; - $rid = $state['aselect:rid']; - assert('isset($credentials)'); - assert('isset($rid)'); - - $params = array( - 'request' => 'verify_credentials', - 'rid' => $rid, - 'a-select-server' => $this->asconfig['serverid'], - 'aselect_credentials' => $credentials, - ); - if ($this->asconfig['type'] == 'cross') { - $params['local_organization'] = $this->asconfig['local_organization']; - } + /** + * Initiate authentication. Returns a URL to redirect the user to. + * + * @param string $app_url The SSP URL to return to after authenticating (similar to an ACS). + */ + public function request_authentication($app_url) { + $res = $this->call_aselect('authenticate', + array('app_id' => $this->app_id, 'app_url' => $app_url)); - $url = SimpleSAML_Utilities::addURLparameter($this->asconfig['serverurl'], $params); - - $parms = $this->as_call($url); - $attributes = array('uid' => array($parms['uid'])); - - if (array_key_exists('attributes', $parms)) { - $decoded = base64_decode($parms['attributes']); - foreach (explode('&', $decoded) as $parm) { - $tuple = explode('=', $parm); - $name = urldecode($tuple[0]); - if (preg_match('/\[\]$/',$name)) { - $name = substr($name, 0 ,-2); - } - if (!array_key_exists($name, $attributes)) { - $attributes[$name] = array(); - } - $attributes[$name][] = urldecode($tuple[1]); - } - } - $state['Attributes'] = $attributes; + $as_url = $res['as_url']; + unset($res['as_url']); + + return SimpleSAML_Utilities::addURLparameter($as_url, $res); + } + + /** + * Verify the credentials upon return from the A-Select server. Returns an associative array + * with the information given by the A-Select server. Any attributes are pre-parsed. + * + * @param string $server_id The A-Select server ID as passed by the client + * @param string $credentials The credentials as passed by the client + * @param string $rid The request ID as passed by the client + */ + public function verify_credentials($server_id, $credentials, $rid) { + if($server_id != $this->server_id) + throw new SimpleSAML_Error_Exception("Acquired server ID ($server_id) does not match configured server ID ($this->server_id)"); + + $res = $this->call_aselect('verify_credentials', + array('aselect_credentials' => $credentials, 'rid' => $rid)); + + if(array_key_exists('attributes', $res)) + $res['attributes'] = self::decode_attributes($res['attributes']); - SimpleSAML_Auth_Source::completeAuth($state); + return $res; } } diff --git a/modules/aselect/www/credentials.php b/modules/aselect/www/credentials.php new file mode 100644 index 0000000000000000000000000000000000000000..3d3b8cba1204a903c768285f831c544bc253a113 --- /dev/null +++ b/modules/aselect/www/credentials.php @@ -0,0 +1,47 @@ +<?php + +/** + * Check the credentials that the user got from the A-Select server. + * This function is called after the user returns from the A-Select server. + * + * @author Wessel Dankers, Tilburg University + */ +function check_credentials() { + $state = SimpleSAML_Auth_State::loadState($_REQUEST['ssp_state'], 'aselect:login'); + + if(!array_key_exists('a-select-server', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing a-select-server parameter")); + $server_id = $_REQUEST['a-select-server']; + + if(!array_key_exists('aselect_credentials', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing aselect_credentials parameter")); + $credentials = $_REQUEST['aselect_credentials']; + + if(!array_key_exists('rid', $_REQUEST)) + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Missing rid parameter")); + $rid = $_REQUEST['rid']; + + try { + if(!array_key_exists('aselect::authid', $state)) + throw new SimpleSAML_Error_Exception("ASelect authentication source missing in state"); + $authid = $state['aselect::authid']; + $aselect = SimpleSAML_Auth_Source::getById($authid); + if(is_null($aselect)) + throw new SimpleSAML_Error_Exception("Could not find authentication source with id $authid"); + $creds = $aselect->verify_credentials($server_id, $credentials, $rid); + + if(array_key_exists('attributes', $creds)) { + $state['Attributes'] = $creds['attributes']; + } else { + $res = $creds['res']; + $state['Attributes'] = array('uid' => array($res['uid']), 'organization' => array($res['organization'])); + } + } catch(Exception $e) { + SimpleSAML_Auth_State::throwException($state, $e); + } + + SimpleSAML_Auth_Source::completeAuth($state); + SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_Exception("Internal error in A-Select component")); +} + +check_credentials(); diff --git a/modules/aselect/www/linkback.php b/modules/aselect/www/linkback.php index 1f3efba228777d941f009f22ce31942e48689ea2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/modules/aselect/www/linkback.php +++ b/modules/aselect/www/linkback.php @@ -1,36 +0,0 @@ -<?php - -/** - * Handle linkback() response from A-Select. - */ - - -if (!isset($_GET['stateID'])) { - throw new SimpleSAML_Error_BadRequest('Missing stateID parameter.'); -} -$stateId = (string)$_GET['stateID']; - -if (!isset($_GET['aselect_credentials'])) { - throw new SimpleSAML_Error_BadRequest('Missing aselect_credentials parameter.'); -} -if (!isset($_GET['rid'])) { - throw new SimpleSAML_Error_BadRequest('Missing ridparameter.'); -} - - -$state = SimpleSAML_Auth_State::loadState($stateId, sspmod_aselect_Auth_Source_aselect::STAGE_INIT); -$state['aselect:credentials'] = $_GET['aselect_credentials']; -$state['aselect:rid'] = $_GET['rid']; - - -/* Find authentication source. */ -assert('array_key_exists(sspmod_aselect_Auth_Source_aselect::AUTHID, $state)'); -$sourceId = $state[sspmod_aselect_Auth_Source_aselect::AUTHID]; - -$source = SimpleSAML_Auth_Source::getById($sourceId); -if ($source === NULL) { - throw new Exception('Could not find authentication source with id ' . $sourceId); -} - -$source->finalStep($state); - diff --git a/modules/discojuice/config-templates/discojuice.php b/modules/discojuice/config-templates/discojuice.php new file mode 100644 index 0000000000000000000000000000000000000000..2f1e0e31e48621b0c2fdb6dbcc2a8d8c0eb9a570 --- /dev/null +++ b/modules/discojuice/config-templates/discojuice.php @@ -0,0 +1,42 @@ +<?php + +/** + * This is the configuration file for the DiscoJuice. + */ + +$config = array( + + // A human readable name describing the Service Provider + 'name' => 'Service', + + /* A set of prepared metadata feeds from discojuice.org + * You may visit + * https://static.discojuice.org/feeds/ + * + * to review the available feed identifiers. + * You may choose to not use any of the provider feed, by setting this to an + * empty array: array() + */ + 'feeds' => array('edugain'), + + /* + * You may provide additional feeds + */ + 'additionalFeeds' => array( + ), + + /* + * If you set this value to true, the module will contact discojuice.org to read and write cookies. + * If you enable this, you will also need to get your host accepted in the access control list of + * discojuice.org + * + * The response url of your service, similar to: + * + * https://sp.example.org/simplesaml/module.php/discojuice/response.html + * + * will need to be registered at discojuice.org. If your response url is already registered in the metadata + * of one of the federation feeds at discojuice.org, you should already have access. + */ + 'enableCentralStorage' => false, + +); \ No newline at end of file diff --git a/modules/discojuice/default-disable b/modules/discojuice/default-disable new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/modules/discojuice/templates/central.tpl.php b/modules/discojuice/templates/central.tpl.php new file mode 100644 index 0000000000000000000000000000000000000000..8067e168041f104f291c21cba5600f5d9e0e2401 --- /dev/null +++ b/modules/discojuice/templates/central.tpl.php @@ -0,0 +1,80 @@ +<?php + +header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'); + +?><!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <title>Select Your Login Provider</title> + + <!-- JQuery hosted by Google --> + <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript"></script> + + <!-- DiscoJuice hosted by UNINETT at discojuice.org --> + <script type="text/javascript" src="https://engine.discojuice.org/discojuice-stable.min.js"></script> + <link rel="stylesheet" type="text/css" href="https://static.discojuice.org/css/discojuice.css" /> + + <style type="text/css"> + body { + text-align: center; + } + div.discojuice { + text-align: left; + position: relative; + width: 600px; + margin-right: auto; + margin-left: auto; + } + </style> + + <script type="text/javascript"> + +<?php + + echo ' + $("document").ready(function() { + var djc = DiscoJuice.Hosted.getConfig(' . + json_encode($this->data['hostedConfig'][0]) . "," . + json_encode($this->data['hostedConfig'][1]) . "," . + json_encode($this->data['hostedConfig'][2]) . "," . + json_encode($this->data['hostedConfig'][3]) . "," . + json_encode($this->data['hostedConfig'][4]) . + ');'; + + // echo " djc.country = false;\n"; + // echo " djc.showLocationInfo = false;\n"; + + if (!$this->data['enableCentralStorage']) { + echo " delete djc.disco;\n"; + } + if (!empty($this->data['additionalFeeds'])) { + foreach($this->data['additionalFeeds'] AS $feed) { + echo " djc.metadata.push(" . json_encode($feed) . ");\n"; + } + } + + echo " djc.always = true;\n"; + + echo ' + $("a.signin").DiscoJuice(djc); + }); + '; + +?> + + + + </script> + + + +</head> +<body style="background: #ccc"> + + <p style="display: none; text-align: right"><a class="signin" href="/">signin</a></p> + +</body> +</html> + + diff --git a/modules/discojuice/www/central.php b/modules/discojuice/www/central.php new file mode 100644 index 0000000000000000000000000000000000000000..6f1a7ead6fd137123deefb72e0e98b77073141af --- /dev/null +++ b/modules/discojuice/www/central.php @@ -0,0 +1,49 @@ +<?php + +if (empty($_REQUEST['entityID'])) throw new Exception('Missing parameter [entityID]'); +if (empty($_REQUEST['return'])) throw new Exception('Missing parameter [return]'); + + +$djconfig = SimpleSAML_Configuration::getOptionalConfig('discojuice.php'); +$config = SimpleSAML_Configuration::getInstance(); + +// EntityID +$entityid = $_REQUEST['entityID']; + +// Return to... +$returnidparam = !empty($_REQUEST['returnIDParam']) ? $_REQUEST['returnIDParam'] : 'entityID'; +$href = SimpleSAML_Utilities::addURLparameter( + $_REQUEST['return'], + array($returnidparam => '') +); + + +$hostedConfig = array( + // Name of service + $djconfig->getString('name', 'Service'), + + $entityid, + + // Url to response + SimpleSAML_Module::getModuleURL('discojuice/response.html'), + + // Set of feeds to subscribe to. + $djconfig->getArray('feeds', array('edugain')), + + $href +); + +/* + "a.signin", "Teest Demooo", + "https://example.org/saml2/entityid", + "' . SimpleSAML_Module::getModuleURL('discojuice/discojuice/discojuiceDiscoveryResponse.html') . '", ["kalmar"], "http://example.org/login?idp=" +*/ + +$t = new SimpleSAML_XHTML_Template($config, 'discojuice:central.tpl.php'); +$t->data['hostedConfig'] = $hostedConfig; +$t->data['enableCentralStorage'] = $djconfig->getBoolean('enableCentralStorage', true); +$t->data['additionalFeeds'] = $djconfig->getArray('additionalFeeds', null); +$t->show(); + + + diff --git a/modules/discojuice/www/response.html b/modules/discojuice/www/response.html new file mode 100644 index 0000000000000000000000000000000000000000..3067d41be9609fab65a30c05e0afdd1b3431288c --- /dev/null +++ b/modules/discojuice/www/response.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE"> + <META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE"> + <title>IdP Discovery Response Receiver</title> + + <script type="text/javascript"> + +function parseURL(url) { + var a = document.createElement('a'); + a.href = url; + return a.hostname; +} + +function receive() { + var urlParams = {}, + cid = null; + (function () { + var e, + a = /\+/g, // Regex for replacing addition symbol with a space + r = /([^&;=]+)=?([^&;]*)/g, + d = function (s) { return decodeURIComponent(s.replace(a, " ")); }, + q = window.location.search.substring(1); + + while (e = r.exec(q)) + urlParams[d(e[1])] = d(e[2]); + })(); + + if (urlParams.cid) cid = urlParams.cid; + + var sender = parseURL(document.referrer); + + // Received a specific entity ID from the storage. + if (urlParams.entityID) { + window.parent.DiscoJuice.Utils.log('ResponseLocation: Response from discovery service [' + sender + ']: ' + urlParams.entityID + ' subID: ' + urlParams.subID); + window.parent.DiscoJuice.Control.discoResponse(sender, urlParams.entityID, urlParams.subID, cid); + + // Received a textual error from the storage, to show in the debug log. + } else if (urlParams['error']) { + window.parent.DiscoJuice.Control.discoResponseError(cid, + "Error from IdP Discovery Service [" + sender + "]: " + urlParams.error); + + // Did not receive a response parameter. This probably means that the Disco storage did not have a stored preference + // for the user. Consequently: no error. + } else { + window.parent.DiscoJuice.Utils.log('No valid response parameters. cid[' + cid + ']'); + window.parent.DiscoJuice.Control.discoResponseError(cid); + } + +} + + </script> +</head> + +<body onload="receive();"> + +</body> +</html> \ No newline at end of file diff --git a/modules/metarefresh/bin/metarefresh.php b/modules/metarefresh/bin/metarefresh.php index c8a918122f8f7979ca78dc00c57a45ca555bc644..08fe926f819ccbf92c8e0758bb75cb34e07a15cc 100755 --- a/modules/metarefresh/bin/metarefresh.php +++ b/modules/metarefresh/bin/metarefresh.php @@ -13,6 +13,8 @@ $baseDir = dirname(dirname(dirname(dirname(__FILE__)))); /* Add library autoloader. */ require_once($baseDir . '/lib/_autoload.php'); +SimpleSAML_Session::useTransientSession(); /* No need to try to create a session here. */ + if(!SimpleSAML_Module::isModuleEnabled('metarefresh')) { echo("You need to enable the metarefresh module before this script can be used.\n"); echo("You can enable it by running the following command:\n"); diff --git a/modules/metarefresh/lib/MetaLoader.php b/modules/metarefresh/lib/MetaLoader.php index 2fb532ff2497840a37d061e69eaaf426e88c3ffd..ea5bfc647a99378b53d2bf2895312bc9104068b0 100644 --- a/modules/metarefresh/lib/MetaLoader.php +++ b/modules/metarefresh/lib/MetaLoader.php @@ -45,79 +45,85 @@ class sspmod_metarefresh_MetaLoader { */ public function loadSource($source) { - // Build new HTTP context - $context = $this->createContext($source); + if (preg_match('@^https?://@i', $source['src'])) { + // Build new HTTP context + $context = $this->createContext($source); + + // GET! + try { + list($data, $responseHeaders) = SimpleSAML_Utilities::fetch($source['src'], $context, TRUE); + } catch(Exception $e) { + SimpleSAML_Logger::warning('metarefresh: ' . $e->getMessage()); + } - // GET! - try { - list($data, $responseHeaders) = SimpleSAML_Utilities::fetch($source['src'], $context, TRUE); - } catch(Exception $e) { - SimpleSAML_Logger::warning('metarefresh: ' . $e->getMessage()); + // We have response headers, so the request succeeded + if(!isset($responseHeaders)) { + // No response headers, this means the request failed in some way, so re-use old data + SimpleSAML_Logger::debug('No response from ' . $source['src'] . ' - attempting to re-use cached metadata'); + $this->addCachedMetadata($source); + return; + } elseif(preg_match('@^HTTP/1\.[01]\s304\s@', $responseHeaders[0])) { + // 304 response + SimpleSAML_Logger::debug('Received HTTP 304 (Not Modified) - attempting to re-use cached metadata'); + $this->addCachedMetadata($source); + return; + } elseif(!preg_match('@^HTTP/1\.[01]\s200\s@', $responseHeaders[0])) { + // Other error. + SimpleSAML_Logger::debug('Error from ' . $source['src'] . ' - attempting to re-use cached metadata'); + $this->addCachedMetadata($source); + return; + } + } else { + /* Local file. */ + $data = file_get_contents($source['src']); + $responseHeaders = NULL; } - // We have response headers, so the request succeeded - if(isset($responseHeaders)) { - - // 200 OK - if(preg_match('@^HTTP/1\.[01]\s200\s@', $responseHeaders[0])) { - - if (isset($source['conditionalGET']) && $source['conditionalGET']) { - // Stale or no metadata, so a fresh copy - SimpleSAML_Logger::debug('Downloaded fresh copy'); - } - - $entities = $this->loadXML($data, $source); - - foreach($entities as $entity) { - - if(isset($source['blacklist'])) { - if(!empty($source['blacklist']) && in_array($entity->getEntityID(), $source['blacklist'])) { - SimpleSAML_Logger::info('Skipping "' . $entity->getEntityID() . '" - blacklisted.' . "\n"); - continue; - } - } - - if(isset($source['whitelist'])) { - if(!empty($source['whitelist']) && !in_array($entity->getEntityID(), $source['whitelist'])) { - SimpleSAML_Logger::info('Skipping "' . $entity->getEntityID() . '" - not in the whitelist.' . "\n"); - continue; - } - } + /* Everything OK. Proceed. */ + if (isset($source['conditionalGET']) && $source['conditionalGET']) { + // Stale or no metadata, so a fresh copy + SimpleSAML_Logger::debug('Downloaded fresh copy'); + } - if(array_key_exists('validateFingerprint', $source) && $source['validateFingerprint'] !== NULL) { - if(!$entity->validateFingerprint($source['validateFingerprint'])) { - SimpleSAML_Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature.' . "\n"); - continue; - } - } + $entities = $this->loadXML($data, $source); - $template = NULL; - if (array_key_exists('template', $source)) $template = $source['template']; + foreach($entities as $entity) { - $this->addMetadata($source['src'], $entity->getMetadata1xSP(), 'shib13-sp-remote', $template); - $this->addMetadata($source['src'], $entity->getMetadata1xIdP(), 'shib13-idp-remote', $template); - $this->addMetadata($source['src'], $entity->getMetadata20SP(), 'saml20-sp-remote', $template); - $this->addMetadata($source['src'], $entity->getMetadata20IdP(), 'saml20-idp-remote', $template); - $attributeAuthorities = $entity->getAttributeAuthorities(); - if (!empty($attributeAuthorities)) { - $this->addMetadata($source['src'], $attributeAuthorities[0], 'attributeauthority-remote', $template); - } + if(isset($source['blacklist'])) { + if(!empty($source['blacklist']) && in_array($entity->getEntityID(), $source['blacklist'])) { + SimpleSAML_Logger::info('Skipping "' . $entity->getEntityID() . '" - blacklisted.' . "\n"); + continue; } + } - $this->saveState($source, $responseHeaders); + if(isset($source['whitelist'])) { + if(!empty($source['whitelist']) && !in_array($entity->getEntityID(), $source['whitelist'])) { + SimpleSAML_Logger::info('Skipping "' . $entity->getEntityID() . '" - not in the whitelist.' . "\n"); + continue; + } } - // 304 response - if(preg_match('@^HTTP/1\.[01]\s304\s@', $responseHeaders[0])) { - SimpleSAML_Logger::debug('Received HTTP 304 (Not Modified) - attempting to re-use cached metadata'); - $this->addCachedMetadata($source); + if(array_key_exists('validateFingerprint', $source) && $source['validateFingerprint'] !== NULL) { + if(!$entity->validateFingerprint($source['validateFingerprint'])) { + SimpleSAML_Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature.' . "\n"); + continue; + } } - } else { - // No response headers, this means the request failed in some way, so re-use old data - SimpleSAML_Logger::debug('No response from ' . $source['src'] . ' - attempting to re-use cached metadata'); - $this->addCachedMetadata($source); + $template = NULL; + if (array_key_exists('template', $source)) $template = $source['template']; + + $this->addMetadata($source['src'], $entity->getMetadata1xSP(), 'shib13-sp-remote', $template); + $this->addMetadata($source['src'], $entity->getMetadata1xIdP(), 'shib13-idp-remote', $template); + $this->addMetadata($source['src'], $entity->getMetadata20SP(), 'saml20-sp-remote', $template); + $this->addMetadata($source['src'], $entity->getMetadata20IdP(), 'saml20-idp-remote', $template); + $attributeAuthorities = $entity->getAttributeAuthorities(); + if (!empty($attributeAuthorities)) { + $this->addMetadata($source['src'], $attributeAuthorities[0], 'attributeauthority-remote', $template); + } } + + $this->saveState($source, $responseHeaders); } /** diff --git a/modules/radius/lib/Auth/Source/Radius.php b/modules/radius/lib/Auth/Source/Radius.php index 283027ec0c08059d0f1158f24eac3f59dcf71982..57c023c6c424465c1b867681ea9aa63929d22b09 100644 --- a/modules/radius/lib/Auth/Source/Radius.php +++ b/modules/radius/lib/Auth/Source/Radius.php @@ -49,7 +49,10 @@ class sspmod_radius_Auth_Source_Radius extends sspmod_core_Auth_UserPassBase { * The vendor-specific attribute for the RADIUS attributes we are interrested in. */ private $vendorType; - + /** + * The NAS-Identifier that should be set in Access-Request packets. + */ + private $nasIdentifier; /** * Constructor for this authentication source. @@ -74,6 +77,7 @@ class sspmod_radius_Auth_Source_Radius extends sspmod_core_Auth_UserPassBase { $this->timeout = $config->getInteger('timeout', 5); $this->retries = $config->getInteger('retries', 3); $this->usernameAttribute = $config->getString('username_attribute', NULL); + $this->nasIdentifier = $config->getString('nas_identifier', NULL); $this->vendor = $config->getInteger('attribute_vendor', NULL); if ($this->vendor !== NULL) { @@ -105,6 +109,9 @@ class sspmod_radius_Auth_Source_Radius extends sspmod_core_Auth_UserPassBase { radius_put_attr($radius, RADIUS_USER_NAME, $username); radius_put_attr($radius, RADIUS_USER_PASSWORD, $password); + if ($this->nasIdentifier != NULL) + radius_put_attr($radius, RADIUS_NAS_IDENTIFIER, $this->nasIdentifier); + $res = radius_send_request($radius); if ($res != RADIUS_ACCESS_ACCEPT) { switch ($res) { @@ -179,6 +186,3 @@ class sspmod_radius_Auth_Source_Radius extends sspmod_core_Auth_UserPassBase { } } - - -?> \ No newline at end of file diff --git a/modules/saml/docs/sp.txt b/modules/saml/docs/sp.txt index 5e5c66a0cbc8e17d9b22ddfa2064416654bbc819..673d69ca9e021dce7d9418e768b4974e66efc093 100644 --- a/modules/saml/docs/sp.txt +++ b/modules/saml/docs/sp.txt @@ -105,6 +105,38 @@ Here we will list some examples for this authentication source. ), +### Specifying attributes and required attributes + + An SP that wants eduPersonPrincipalName and mail, where eduPersonPrincipalName should be listed as required: + + 'example-attributes => array( + 'saml:SP', + 'name' => array( //Name required for AttributeConsumingService-element. + 'en' => 'Example service', + 'no' => 'Eksempeltjeneste', + ), + 'attributes' => array( + 'eduPersonPrincipalName', + 'mail', + ) + 'attributes.required' => array ( + 'eduPersonPrincipalName', + ), + 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', + ), + + +### Limiting supported AssertionConsumerService endpoint bindings + + 'example-acs-limit' => array( + 'saml:SP', + 'acs.Bindings' => array( + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + ), + ), + + ### Requesting a specific authentication method. $auth = new SimpleSAML_Auth_Simple('default-sp'); @@ -127,6 +159,16 @@ Here we will list some examples for this authentication source. Options ------- +`acs.Bindings` +: List of bindings the SP should support. If it is unset, all will be added. +: Possible values: + + * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` + * `urn:oasis:names:tc:SAML:1.0:profiles:browser-post` + * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact` + * `urn:oasis:names:tc:SAML:1.0:profiles:artifact-01` + * `urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser` + `assertion.encryption` : Whether assertions received by this SP must be encrypted. The default value is `FALSE`. If this option is set to `TRUE`, unencrypted assertions will be rejected. @@ -147,6 +189,10 @@ Options `attributes.NameFormat` : The `NameFormat` for the requested attributes. +`attributes.required` +: If you have attributes added you can here specify which should be marked as required. +: The attributes should still be present in `attributes`. + `AuthnContextClassRef` : The SP can request authentication with a specific authentication context class. One example of usage could be if the IdP supports both username/password authentication as well as software-PKI. diff --git a/modules/saml/www/sp/metadata.php b/modules/saml/www/sp/metadata.php index b19a392193cd3e95757dbc56a6ded8e8e424240b..25e9d7fd245bb971d0a5d59b138c5840ed537dd2 100644 --- a/modules/saml/www/sp/metadata.php +++ b/modules/saml/www/sp/metadata.php @@ -1,6 +1,5 @@ <?php - if (!array_key_exists('PATH_INFO', $_SERVER)) { throw new SimpleSAML_Error_BadRequest('Missing authentication source id in metadata URL'); } @@ -48,36 +47,48 @@ if ($store instanceof SimpleSAML_Store_SQL) { $sp->SingleLogoutService[] = $slo; } -$acs = new SAML2_XML_md_IndexedEndpointType(); -$acs->index = 0; -$acs->Binding = SAML2_Const::BINDING_HTTP_POST; -$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); -$sp->AssertionConsumerService[] = $acs; - -$acs = new SAML2_XML_md_IndexedEndpointType(); -$acs->index = 1; -$acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post'; -$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId); -$sp->AssertionConsumerService[] = $acs; - -$acs = new SAML2_XML_md_IndexedEndpointType(); -$acs->index = 2; -$acs->Binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'; -$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); -$sp->AssertionConsumerService[] = $acs; - -$acs = new SAML2_XML_md_IndexedEndpointType(); -$acs->index = 3; -$acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01'; -$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId . '/artifact'); -$sp->AssertionConsumerService[] = $acs; - -$acs = new SAML2_XML_md_IndexedEndpointType(); -$acs->index = 4; -$acs->Binding = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'; -$acs->ProtocolBinding = SAML2_Const::BINDING_HTTP_POST; -$acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); -$sp->AssertionConsumerService[] = $acs; +$assertionsconsumerservicesdefault = array( + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', + 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01', + 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', +); + +$assertionsconsumerservices = $spconfig->getArray('acs.Bindings', $assertionsconsumerservicesdefault); + +$index = 0; +foreach ($assertionsconsumerservices as $services) { + + $acs = new SAML2_XML_md_IndexedEndpointType(); + $acs->index = $index; + switch ($services) { + case 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST': + $acs->Binding = SAML2_Const::BINDING_HTTP_POST; + $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post': + $acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post'; + $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId); + break; + case 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact': + $acs->Binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'; + $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); + break; + case 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01': + $acs->Binding = 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01'; + $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $sourceId . '/artifact'); + break; + case 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser': + $acs->Binding = 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser'; + $acs->ProtocolBinding = SAML2_Const::BINDING_HTTP_POST; + $acs->Location = SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $sourceId); + break; + } + $sp->AssertionConsumerService[] = $acs; + $index++; +} + $keys = array(); $certInfo = SimpleSAML_Utilities::loadPublicKey($spconfig, FALSE, 'new_'); @@ -99,7 +110,6 @@ if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) { 'encryption' => TRUE, 'X509Certificate' => $certInfo['certData'], ); - } else { $hasNewCert = FALSE; } @@ -124,14 +134,17 @@ if ($certInfo !== NULL && array_key_exists('certData', $certInfo)) { 'encryption' => ($hasNewCert ? FALSE : TRUE), 'X509Certificate' => $certInfo['certData'], ); - } else { $certData = NULL; } $name = $spconfig->getLocalizedString('name', NULL); $attributes = $spconfig->getArray('attributes', array()); + if ($name !== NULL && !empty($attributes)) { + + $attributesrequired = $spconfig->getArray('attributes.required', array()); + /* We have everything necessary to add an AttributeConsumingService. */ $acs = new SAML2_XML_md_AttributeConsumingService(); $sp->AttributeConsumingService[] = $acs; @@ -149,6 +162,10 @@ if ($name !== NULL && !empty($attributes)) { $a = new SAML2_XML_md_RequestedAttribute(); $a->Name = $attribute; $a->NameFormat = $nameFormat; + // Is the attribute required + if (in_array($attribute, $attributesrequired)) + $a->isRequired = true; + $acs->RequestedAttribute[] = $a; } @@ -163,6 +180,7 @@ if ($name !== NULL && !empty($attributes)) { } } + $orgName = $spconfig->getLocalizedString('OrganizationName', NULL); if ($orgName !== NULL) { $o = new SAML2_XML_md_Organization(); @@ -233,5 +251,4 @@ if (array_key_exists('output', $_REQUEST) && $_REQUEST['output'] == 'xhtml') { header('Content-Type: application/samlmetadata+xml'); echo($xml); } - ?> diff --git a/www/errorreport.php b/www/errorreport.php index bef296c130150acab056a721ee1cb77647ef456a..5a245c0648659535faab295bbbcefa76c7e9f48f 100644 --- a/www/errorreport.php +++ b/www/errorreport.php @@ -17,19 +17,27 @@ $reportId = (string)$_REQUEST['reportId']; $email = (string)$_REQUEST['email']; $text = htmlspecialchars((string)$_REQUEST['text']); -$session = SimpleSAML_Session::getInstance(); -$data = $session->getData('core:errorreport', $reportId); +try { + $session = SimpleSAML_Session::getInstance(); + $data = $session->getData('core:errorreport', $reportId); +} catch (Exception $e) { + SimpleSAML_Logger::error('Error loading error report data: ' . var_export($e->getMessage(), TRUE)); +} if ($data === NULL) { $data = array( 'exceptionMsg' => 'not set', 'exceptionTrace' => 'not set', 'reportId' => $reportId, - 'trackId' => $session->getTrackId(), + 'trackId' => 'not set', 'url' => 'not set', 'version' => $config->getVersion(), 'referer' => 'not set', ); + + if (isset($session)) { + $data['trackId'] = $session->getTrackId(); + } } foreach ($data as $k => $v) { 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);