From 89f37281be2865a76a9963d7f1dc7b59317461dd Mon Sep 17 00:00:00 2001
From: Patrick Radtke <patrick@cirrusidentity.com>
Date: Fri, 19 Oct 2018 16:26:18 -0700
Subject: [PATCH] Add shib idp initiated compatability

---
 modules/saml/lib/IdP/SAML2.php           | 15 ++++--
 tests/modules/saml/lib/IdP/SAML2Test.php | 64 ++++++++++++++++++++++--
 2 files changed, 73 insertions(+), 6 deletions(-)

diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php
index c846ed83a..764e0d1c3 100644
--- a/modules/saml/lib/IdP/SAML2.php
+++ b/modules/saml/lib/IdP/SAML2.php
@@ -265,7 +265,7 @@ class SAML2
             $supportedBindings[] = \SAML2\Constants::BINDING_PAOS;
         }
 
-        if (isset($_REQUEST['spentityid'])) {
+        if (isset($_REQUEST['spentityid']) || isset($_REQUEST['providerId'])) {
             /* IdP initiated authentication. */
 
             if (isset($_REQUEST['cookieTime'])) {
@@ -279,11 +279,13 @@ class SAML2
                 }
             }
 
-            $spEntityId = (string) $_REQUEST['spentityid'];
+            $spEntityId = (string) isset($_REQUEST['spentityid']) ? $_REQUEST['spentityid'] : $_REQUEST['providerId'];
             $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
 
             if (isset($_REQUEST['RelayState'])) {
                 $relayState = (string) $_REQUEST['RelayState'];
+            } elseif (isset($_REQUEST['target'])) {
+                $relayState = (string) $_REQUEST['target'];
             } else {
                 $relayState = null;
             }
@@ -300,13 +302,20 @@ class SAML2
                 $nameIDFormat = null;
             }
 
+            if (isset($_REQUEST['ConsumerURL'])) {
+                $consumerURL = (string)$_REQUEST['ConsumerURL'];
+            } elseif (isset($_REQUEST['shire'])) {
+                $consumerURL = (string)$_REQUEST['shire'];
+            } else {
+                $consumerURL = null;
+            }
+
             $requestId = null;
             $IDPList = [];
             $ProxyCount = null;
             $RequesterID = null;
             $forceAuthn = false;
             $isPassive = false;
-            $consumerURL = null;
             $consumerIndex = null;
             $extensions = null;
             $allowCreate = true;
diff --git a/tests/modules/saml/lib/IdP/SAML2Test.php b/tests/modules/saml/lib/IdP/SAML2Test.php
index 00f7da691..ef94baf9a 100644
--- a/tests/modules/saml/lib/IdP/SAML2Test.php
+++ b/tests/modules/saml/lib/IdP/SAML2Test.php
@@ -28,6 +28,9 @@ class SAML2Test extends ClearStateTestCase
         $_SERVER['PHP_AUTH_PW'] = 'password';
         unset($_SERVER['PHP_AUTH_USER']);
         $state = [];
+        Configuration::loadFromArray([
+            'baseurlpath' => 'https://idp.example.com/',
+        ], '', 'simplesaml');
 
         \SimpleSAML\Module\saml\IdP\SAML2::processSOAPAuthnRequest($state);
     }
@@ -52,7 +55,7 @@ class SAML2Test extends ClearStateTestCase
         '\SimpleSAML\Auth\State.exceptionFunc' => ['\SimpleSAML\Module\saml\IdP\SAML2', 'handleAuthError'],
         'saml:RelayState' => null,
         'saml:RequestId' => null,
-        'saml:IDPList' => Array (),
+        'saml:IDPList' => [],
         'saml:ProxyCount' => null,
         'saml:RequesterID' => null,
         'ForceAuthn' => false,
@@ -75,7 +78,7 @@ class SAML2Test extends ClearStateTestCase
         $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
@@ -120,6 +123,61 @@ class SAML2Test extends ClearStateTestCase
         $this->assertEquals($expectedState, $state);
     }
 
+    /**
+     * Test that invoking the idp initiated endpoint using minimum shib params works
+     */
+    public function testIdPInitShibCompatyMinimumParams()
+    {
+        //https://wiki.shibboleth.net/confluence/display/IDP30/UnsolicitedSSOConfiguration
+        // Shib uses the param providerId instead of spentityid
+        $state = $this->idpInitiatedHelper(['providerId' => '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 using minimum shib params works
+     */
+    public function testIdPInitShibCompatOptionalParams()
+    {
+        $state = $this->idpInitiatedHelper([
+            'providerId' => 'https://some-sp-entity-id',
+            'target' => 'http://relay',
+            'shire' => 'https://example.com/Shibboleth.sso/SAML2/ECP',
+        ]);
+        $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: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.
@@ -152,7 +210,7 @@ class SAML2Test extends ClearStateTestCase
 EOT;
         // phpcs:enable
 
-        \SimpleSAML\Configuration::loadFromArray([
+        Configuration::loadFromArray([
             'baseurlpath' => 'https://idp.example.com/',
             'metadata.sources' => [
                 ["type" => "xml", 'xml' => $spMetadataXml],
-- 
GitLab