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