From ee4dcfb732de1391976e858a38737a713b4b0e70 Mon Sep 17 00:00:00 2001 From: John Maguire <jmaguire@duosecurity.com> Date: Wed, 31 May 2017 17:34:57 -0400 Subject: [PATCH] Add IdP support for ECP profile --- docs/simplesamlphp-ecp-idp.txt | 9 ++++ modules/core/lib/Auth/UserPassBase.php | 25 +++++++-- modules/saml/lib/IdP/SAML2.php | 20 ++++++- .../core/lib/Auth/UserPassBaseTest.php | 52 +++++++++++++++++++ tests/modules/saml/lib/IdP/SAML2Test.php | 38 ++++++++++++++ www/saml2/idp/metadata.php | 8 +++ 6 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 tests/modules/core/lib/Auth/UserPassBaseTest.php create mode 100644 tests/modules/saml/lib/IdP/SAML2Test.php diff --git a/docs/simplesamlphp-ecp-idp.txt b/docs/simplesamlphp-ecp-idp.txt index 5a03be055..28ac7f90a 100644 --- a/docs/simplesamlphp-ecp-idp.txt +++ b/docs/simplesamlphp-ecp-idp.txt @@ -5,6 +5,15 @@ This document describes the necessary steps to enable support for the [SAML V2.0 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 ----------------------------------- diff --git a/modules/core/lib/Auth/UserPassBase.php b/modules/core/lib/Auth/UserPassBase.php index e688a70dd..8ae524eec 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; @@ -84,7 +84,7 @@ abstract class sspmod_core_Auth_UserPassBase extends SimpleSAML_Auth_Source { public function __construct($info, &$config) { assert('is_array($info)'); assert('is_array($config)'); - + if (isset($config['core:loginpage_links'])) { $this->loginLinks = $config['core:loginpage_links']; } @@ -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 158cdd21b..ebb8354f9 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 000000000..ff248bdc5 --- /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 000000000..4ca209592 --- /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 897f22d31..5a7e98942 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' -- GitLab