From 9c4b02ac6588e56521370ac2b1688e3682562b88 Mon Sep 17 00:00:00 2001 From: Patrick Radtke <patrick@cirrusidentity.com> Date: Fri, 19 Oct 2018 16:08:31 -0700 Subject: [PATCH] Initial tests for IDP initiated login --- .../Metadata/MetaDataStorageHandler.php | 14 +- modules/saml/lib/IdP/SAML2.php | 6 +- tests/Utils/StateClearer.php | 2 +- tests/modules/saml/lib/IdP/SAML2Test.php | 143 +++++++++++++++++- 4 files changed, 161 insertions(+), 4 deletions(-) diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php index e171ab90b..f39c2c047 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php @@ -2,6 +2,8 @@ namespace SimpleSAML\Metadata; +use SimpleSAML\Utils\ClearableState; + /** * This file defines a class for metadata handling. * @@ -9,7 +11,7 @@ namespace SimpleSAML\Metadata; * @package SimpleSAMLphp */ -class MetaDataStorageHandler +class MetaDataStorageHandler implements ClearableState { /** * This static variable contains a reference to the current @@ -358,4 +360,14 @@ class MetaDataStorageHandler return null; } + + /** + * Clear any metadata cached. + * Allows for metadata configuration to be changed and reloaded during a given request. Most useful + * when running phpunit tests and needing to alter config.php and metadata sources between test cases + */ + public static function clearInternalState() + { + self::$metadataHandler = null; + } } diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php index 4118b56b7..c846ed83a 100644 --- a/modules/saml/lib/IdP/SAML2.php +++ b/modules/saml/lib/IdP/SAML2.php @@ -402,11 +402,15 @@ class SAML2 $sessionLostParams = [ 'spentityid' => $spEntityId, - 'cookieTime' => time(), ]; if ($relayState !== null) { $sessionLostParams['RelayState'] = $relayState; } + /* + Putting cookieTime as the last parameter makes unit testing easier since we don't need to handle a + changing time component in the middle of the url + */ + $sessionLostParams['cookieTime'] = time(); $sessionLostURL = \SimpleSAML\Utils\HTTP::addURLParameters( \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(), diff --git a/tests/Utils/StateClearer.php b/tests/Utils/StateClearer.php index ee2b686bf..880c66bdf 100644 --- a/tests/Utils/StateClearer.php +++ b/tests/Utils/StateClearer.php @@ -18,7 +18,7 @@ class StateClearer * Class that implement \SimpleSAML\Utils\ClearableState and should have clearInternalState called between tests * @var array */ - private $clearableState = ['SimpleSAML\Configuration']; + private $clearableState = ['SimpleSAML\Configuration', 'SimpleSAML\Metadata\MetaDataStorageHandler']; /** * Environmental variables to unset diff --git a/tests/modules/saml/lib/IdP/SAML2Test.php b/tests/modules/saml/lib/IdP/SAML2Test.php index c6701cbd3..00f7da691 100644 --- a/tests/modules/saml/lib/IdP/SAML2Test.php +++ b/tests/modules/saml/lib/IdP/SAML2Test.php @@ -2,7 +2,12 @@ namespace SimpleSAML\Test\Module\saml\IdP; -class SAML2Test extends \PHPUnit_Framework_TestCase +use SimpleSAML\Configuration; +use SimpleSAML\IdP; +use SimpleSAML\Module\saml\IdP\SAML2; +use SimpleSAML\Test\Utils\ClearStateTestCase; + +class SAML2Test extends ClearStateTestCase { public function testProcessSOAPAuthnRequest() { @@ -37,4 +42,140 @@ class SAML2Test extends \PHPUnit_Framework_TestCase \SimpleSAML\Module\saml\IdP\SAML2::processSOAPAuthnRequest($state); } + + /** + * Default values for the state array expected to be generated at the start of logins + * @var array + */ + private $defaultExpectedAuthState = [ + 'Responder' =>['\SimpleSAML\Module\saml\IdP\SAML2', 'sendResponse'], + '\SimpleSAML\Auth\State.exceptionFunc' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'handleAuthError'], + 'saml:RelayState' => null, + 'saml:RequestId' => null, + 'saml:IDPList' => Array (), + 'saml:ProxyCount' => null, + 'saml:RequesterID' => null, + 'ForceAuthn' => false, + 'isPassive' => false, + 'saml:ConsumerURL' => 'SP-specific', + 'saml:Binding' => 'SP-specific', + 'saml:NameIDFormat' => null, + 'saml:AllowCreate' => true, + 'saml:Extensions' => null, + 'saml:RequestedAuthnContext' => null]; + + /** + * Test that invoking the idp initiated endpoint with the minimum necessary parameters works. + */ + public function testIdPInitiatedLoginMinimumParams() + { + $state = $this->idpInitiatedHelper(['spentityid' => 'https://some-sp-entity-id']); + $this->assertEquals('https://some-sp-entity-id', $state['SPMetadata']['entityid']); + + $this->assertStringStartsWith( + 'http://idp.examlple.com/saml2/idp/SSOService.php?spentityid=https%3A%2F%2Fsome-sp-entity-id&cookie', + $state['\SimpleSAML\Auth\State.restartURL'] + ); + unset($state['saml:AuthnRequestReceivedAt']); // timestamp can't be tested in equality assertion + unset($state['SPMetadata']); // entityid asserted above + unset($state['\SimpleSAML\Auth\State.restartURL']); // url contains a cookie time which varies by test + + $expectedState = $this->defaultExpectedAuthState; + $expectedState[ 'saml:ConsumerURL'] = 'https://example.com/Shibboleth.sso/SAML2/POST'; + $expectedState[ 'saml:Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'; + + $this->assertEquals($expectedState, $state); + } + + /** + * Test that invoking the idp initiated endpoint with the optional parameters works. + */ + public function testIdPInitiatedLoginOptionalParams() + { + $state = $this->idpInitiatedHelper([ + 'spentityid' => 'https://some-sp-entity-id', + 'RelayState' => 'http://relay', + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS', + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + + ]); + $this->assertEquals('https://some-sp-entity-id', $state['SPMetadata']['entityid']); + + //currently only spentityid and relay state are used in the restart url. + $this->assertStringStartsWith( + 'http://idp.examlple.com/saml2/idp/SSOService.php?' + . 'spentityid=https%3A%2F%2Fsome-sp-entity-id&RelayState=http%3A%2F%2Frelay&cookieTime', + $state['\SimpleSAML\Auth\State.restartURL'] + ); + unset($state['saml:AuthnRequestReceivedAt']); // timestamp can't be tested in equality assertion + unset($state['SPMetadata']); // entityid asserted above + unset($state['\SimpleSAML\Auth\State.restartURL']); // url contains a cookie time which varies by test + + $expectedState = $this->defaultExpectedAuthState; + $expectedState['saml:ConsumerURL'] = 'https://example.com/Shibboleth.sso/SAML2/ECP'; + $expectedState['saml:Binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS'; + $expectedState['saml:NameIDFormat'] = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'; + $expectedState['saml:RelayState'] = 'http://relay'; + + $this->assertEquals($expectedState, $state); + } + + /** + * Invoke IDP initiated login with the given query parameters. + * Callers should validate the return state array or confirm appropriate exceptions are returned. + * + * @param array $queryParams + * @return string[] The state array used for handling the authentication request. + */ + private function idpInitiatedHelper(array $queryParams) + { + /** @var $idpStub \PHPUnit_Framework_MockObject_MockObject|IdP */ + $idpStub = $this->getMockBuilder(IdP::class) + ->disableOriginalConstructor() + ->getMock(); + $idpMetadata = Configuration::loadFromArray([ + 'entityid' => 'https://idp-entity.id', + 'saml20.ecp' => true, //enable additional bindings so we can test selection logic + ]); + + $idpStub->method("getConfig") + ->willReturn($idpMetadata); + + // phpcs:disable + $spMetadataXml = <<< 'EOT' +<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://some-sp-entity-id"> + <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol"> + <md:AssertionConsumerService index="1" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/Shibboleth.sso/SAML2/POST" /> + <md:AssertionConsumerService index="2" Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://example.com/Shibboleth.sso/SAML2/ECP" /> + </md:SPSSODescriptor> +</EntityDescriptor> +EOT; + // phpcs:enable + + \SimpleSAML\Configuration::loadFromArray([ + 'baseurlpath' => 'https://idp.example.com/', + 'metadata.sources' => [ + ["type" => "xml", 'xml' => $spMetadataXml], + ], + ], '', 'simplesaml'); + + // Since we aren't really running on a webserver some of the url calculations done, such as for restart url + // won't line up perfectly + $_REQUEST = $_REQUEST + $queryParams; + $_SERVER['HTTP_HOST'] = 'idp.examlple.com'; + $_SERVER['REQUEST_URI'] = '/saml2/idp/SSOService.php?' . http_build_query($queryParams); + + + $state = []; + $idpStub->expects($this->once()) + ->method('handleAuthenticationRequest') + ->with($this->callback(function ($arg) use (&$state) { + $state = $arg; + return true; + })); + + SAML2::receiveAuthnRequest($idpStub); + + return $state; + } } -- GitLab