diff --git a/docs/simplesamlphp-ecp-idp.txt b/docs/simplesamlphp-ecp-idp.txt new file mode 100644 index 0000000000000000000000000000000000000000..28ac7f90a125dc2b14c1915cfa46755d82009453 --- /dev/null +++ b/docs/simplesamlphp-ecp-idp.txt @@ -0,0 +1,74 @@ +Adding Enhanced Client or Proxy (ECP) Profile support to the IdP +=============================================================== + +This document describes the necessary steps to enable support for the [SAML V2.0 Enhanced Client or Proxy Profile Version 2.0](http://docs.oasis-open.org/security/saml/Post2.0/saml-ecp/v2.0/cs01/saml-ecp-v2.0-cs01.pdf) on a simpleSAMLphp Identity Provider (IdP). + +The SAML V2.0 Enhanced Client or Proxy (ECP) profile is a SSO profile for use with HTTP, and clients with the capability to directly contact a principal's identity provider(s) without requiring discovery and redirection by the service provider, as in the case of a browser. It is particularly useful for desktop or server-side HTTP clients. + +Limitations +----------- +* Authentication must be done via [HTTP Basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme). +* Authentication must not require user interaction (e.g. auth filters that require user input). +* Channel Bindings are unsupported. +* "Holder of Key" Subject Confirmation is unsupported. + +This feature has been tested to work with Microsoft Office 365, but other service providers may require features of the ECP profile that are currently unsupported! + +Enabling ECP Profile on the IdP +----------------------------------- + +To enable the IdP to send ECP assertions you must add the `saml20.ecp` option to the `saml20-idp-hosted` metadata file: + + $metadata['__DYNAMIC:1__'] = array( + [....] + 'auth' => 'example-userpass', + 'saml20.ecp' => true, + ); + +Note: authentication filters that require interaction with the user will not work with ECP. + +Add new metadata to SPs +----------------------- + +After enabling the ECP Profile your IdP metadata will change. An additional ECP `SingleSignOnService` endpoint is added. +You therefore need to update the metadata for your IdP at your SPs. +The `saml20-idp-remote` metadata for simpleSAMLphp SPs should contain something like the following code: + + 'SingleSignOnService' => + array ( + 0 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', + ), + 1 => + array ( + 'index' => 0, + 'Location' => 'https://didp.example.org/simplesaml/saml2/idp/SSOService.php', + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP', + ), + ), + +SP metadata on the IdP +---------------------- + +A SP using the ECP Profile must have an `AssertionConsumerService` endpoint supporting that profile. +This means that you have to use the complex endpoint format in `saml20-sp-remote` metadata. +In general, this should look like the following code: + + 'AssertionConsumerService' => + array ( + 0 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://sp.example.org/Shibboleth.sso/SAML2/POST', + 'index' => 1, + ), + 1 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS', + 'Location' => 'https://sp.example.org/ECP', + 'index' => 2, + ), + ), + diff --git a/docs/simplesamlphp-reference-idp-hosted.md b/docs/simplesamlphp-reference-idp-hosted.md index 0e5cb6298e92a0ba1f393c4d9961955345b81335..712e2b45c4195569992cd5a240e51de5c876e05b 100644 --- a/docs/simplesamlphp-reference-idp-hosted.md +++ b/docs/simplesamlphp-reference-idp-hosted.md @@ -255,6 +255,10 @@ The following SAML 2.0 options are available: : Note that this requires a configured memcache server. +`saml20.ecp` +: Set to `true` to enable the IdP to recieve authnrequests and send responses according the Enhanced Client or Proxy (ECP) Profile. Note: authentication filters that require interaction with the user will not work with ECP. + Defaults to `false`. + `saml20.hok.assertion` : Set to `TRUE` to enable the IdP to send responses according the [Holder-of-Key Web Browser SSO Profile](./simplesamlphp-hok-idp). Defaults to `FALSE`. diff --git a/modules/core/lib/Auth/UserPassBase.php b/modules/core/lib/Auth/UserPassBase.php index a8c7e5445003f27cc175aa3edf9c4e9417930b35..bc05f61967cf976ac80f323a148759dfd215a5be 100644 --- a/modules/core/lib/Auth/UserPassBase.php +++ b/modules/core/lib/Auth/UserPassBase.php @@ -31,9 +31,9 @@ abstract class sspmod_core_Auth_UserPassBase extends SimpleSAML_Auth_Source { * If this is NULL, we won't force any username. */ private $forcedUsername; - + /** - * Links to pages from login page. + * Links to pages from login page. * From configuration */ protected $loginLinks; @@ -183,7 +183,24 @@ abstract class sspmod_core_Auth_UserPassBase extends SimpleSAML_Auth_Source { * is allowed to change the username. */ $state['forcedUsername'] = $this->forcedUsername; - } + } + + // ECP requests supply authentication credentials with the AUthnRequest + // so we validate them now rather than redirecting + if (isset($state['core:auth:username']) && isset($state['core:auth:password'])) { + $username = $state['core:auth:username']; + $password = $state['core:auth:password']; + + if (isset($state['forcedUsername'])) { + $username = $state['forcedUsername']; + } + + $attributes = $this->login($username, $password); + assert('is_array($attributes)'); + $state['Attributes'] = $attributes; + + return; + } /* Save the $state-array, so that we can restore it after a redirect. */ $id = SimpleSAML_Auth_State::saveState($state, self::STAGEID); diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php index a02c8fc6c04439b19173469daf451a7e1d1d176f..384e7806cb9aa68b1f0bac0430149f5c8c52f821 100644 --- a/modules/saml/lib/IdP/SAML2.php +++ b/modules/saml/lib/IdP/SAML2.php @@ -1,6 +1,5 @@ <?php - use RobRichards\XMLSecLibs\XMLSecurityKey; /** @@ -258,6 +257,9 @@ class sspmod_saml_IdP_SAML2 if ($idpMetadata->getBoolean('saml20.hok.assertion', false)) { $supportedBindings[] = \SAML2\Constants::BINDING_HOK_SSO; } + if ($idpMetadata->getBoolean('saml20.ecp', false)) { + $supportedBindings[] = \SAML2\Constants::BINDING_PAOS; + } if (isset($_REQUEST['spentityid'])) { /* IdP initiated authentication. */ @@ -428,9 +430,25 @@ class sspmod_saml_IdP_SAML2 'saml:RequestedAuthnContext' => $authnContext, ); + // ECP AuthnRequests need to supply credentials + if ($binding instanceof SOAP) { + self::processSOAPAuthnRequest($state); + } + $idp->handleAuthenticationRequest($state); } + public static function processSOAPAuthnRequest(array &$state) + { + if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) { + SimpleSAML_Logger::error("ECP AuthnRequest did not contain Basic Authentication header"); + // TODO Throw some sort of ECP-specific exception / convert this to SOAP fault + throw new SimpleSAML_Error_Error("WRONGUSERPASS"); + } + + $state['core:auth:username'] = $_SERVER['PHP_AUTH_USER']; + $state['core:auth:password'] = $_SERVER['PHP_AUTH_PW']; + } /** * Send a logout request to a given association. diff --git a/tests/modules/core/lib/Auth/UserPassBaseTest.php b/tests/modules/core/lib/Auth/UserPassBaseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ff248bdc5459814ed92f3881fe4319ef4dc7c873 --- /dev/null +++ b/tests/modules/core/lib/Auth/UserPassBaseTest.php @@ -0,0 +1,52 @@ +<?php + +class sspmod_core_Auth_UserPassBaseTest extends \PHPUnit_Framework_TestCase +{ + public function testAuthenticateECPCallsLoginAndSetsAttributes() + { + $state = array(); + $attributes = array('attrib' => 'val'); + + $username = $state['core:auth:username'] = 'username'; + $password = $state['core:auth:password'] = 'password'; + + $stub = $this->getMockBuilder('sspmod_core_Auth_UserPassBase') + ->disableOriginalConstructor() + ->setMethods(array('login')) + ->getMockForAbstractClass(); + + $stub->expects($this->once()) + ->method('login') + ->with($username, $password) + ->will($this->returnValue($attributes)); + + $stub->authenticate($state); + + $this->assertSame($attributes, $state['Attributes']); + } + + public function testAuthenticateECPCallsLoginWithForcedUsername() + { + $state = array(); + $attributes = array(); + + $forcedUsername = 'forcedUsername'; + + $state['core:auth:username'] = 'username'; + $password = $state['core:auth:password'] = 'password'; + + $stub = $this->getMockBuilder('sspmod_core_Auth_UserPassBase') + ->disableOriginalConstructor() + ->setMethods(array('login')) + ->getMockForAbstractClass(); + + $stub->expects($this->once()) + ->method('login') + ->with($forcedUsername, $password) + ->will($this->returnValue($attributes)); + + $stub->setForcedUsername($forcedUsername); + + $stub->authenticate($state); + } +} diff --git a/tests/modules/saml/lib/IdP/SAML2Test.php b/tests/modules/saml/lib/IdP/SAML2Test.php new file mode 100644 index 0000000000000000000000000000000000000000..4ca2095920eeef09f42f75d7507d94e264462dd7 --- /dev/null +++ b/tests/modules/saml/lib/IdP/SAML2Test.php @@ -0,0 +1,38 @@ +<?php + +class sspmod_saml_IdP_SAML2Test extends \PHPUnit_Framework_TestCase +{ + public function testProcessSOAPAuthnRequest() + { + $username = $_SERVER['PHP_AUTH_USER'] = 'username'; + $password = $_SERVER['PHP_AUTH_PW'] = 'password'; + $state = array(); + + sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state); + + $this->assertEquals($username, $state['core:auth:username']); + $this->assertEquals($password, $state['core:auth:password']); + } + + public function testProcessSOAPAuthnRequestMissingUsername() + { + $this->setExpectedException('SimpleSAML_Error_Error', 'WRONGUSERPASS'); + + $_SERVER['PHP_AUTH_PW'] = 'password'; + unset($_SERVER['PHP_AUTH_USER']); + $state = array(); + + sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state); + } + + public function testProcessSOAPAuthnRequestMissingPassword() + { + $this->setExpectedException('SimpleSAML_Error_Error', 'WRONGUSERPASS'); + + $_SERVER['PHP_AUTH_USER'] = 'username'; + unset($_SERVER['PHP_AUTH_PW']); + $state = array(); + + sspmod_saml_IdP_SAML2::processSOAPAuthnRequest($state); + } +} diff --git a/www/saml2/idp/metadata.php b/www/saml2/idp/metadata.php index fabe2a2efe4f60816ceb10f4524b114193f931e1..d99aab0fb051c3af56f015323f1a882b1c195cb8 100644 --- a/www/saml2/idp/metadata.php +++ b/www/saml2/idp/metadata.php @@ -127,6 +127,14 @@ try { )); } + if ($idpmeta->getBoolean('saml20.ecp', false)) { + $metaArray['SingleSignOnService'][] = array( + 'index' => 0, + 'Binding' => SAML2_Const::BINDING_SOAP, + 'Location' => SimpleSAML_Utilities::getHostnameURL() . 'saml2/idp/SSOService.php', + ); + } + $metaArray['NameIDFormat'] = $idpmeta->getString( 'NameIDFormat', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'