From 435d3394b67252a667402f84b40de2f8324976c8 Mon Sep 17 00:00:00 2001
From: Bobby Lawrence <dub357@gmail.com>
Date: Tue, 18 Jan 2022 10:36:16 -0500
Subject: [PATCH] =?UTF-8?q?added=20ability=20to=20skip=20validating=20the?=
 =?UTF-8?q?=20AuthnRequest=20response=20destinatio=E2=80=A6=20(#1304)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Added ability to skip validating the AuthnRequest response destination against metadata if the message is signed and the endpoint doesn't already exist in the SP metadata.
---
 docs/simplesamlphp-reference-sp-remote.md |  6 +++++
 modules/saml/lib/IdP/SAML2.php            | 31 ++++++++++++++++++++---
 modules/saml/lib/Message.php              |  6 +++--
 3 files changed, 38 insertions(+), 5 deletions(-)

diff --git a/docs/simplesamlphp-reference-sp-remote.md b/docs/simplesamlphp-reference-sp-remote.md
index 2235b35b0..176a71483 100644
--- a/docs/simplesamlphp-reference-sp-remote.md
+++ b/docs/simplesamlphp-reference-sp-remote.md
@@ -289,6 +289,12 @@ The following options can be set:
 :   Note that this option also exists in the IdP-hosted metadata.
     The value in the SP-remote metadata overrides the value in the IdP-hosted metadata.
 
+`skipEndpointValidationWhenSigned`
+:   Whether to skip validating that the AssertionConsumerServiceURL sent in authentication
+    requests exist in SP metadata.  Only allowed for signed requests.
+    This option must be a simple boolean (true/false - although a value of false essentially has
+    no effect) or a callable.  When used as a callable, the static class method must accept the 
+    SP metadata config as a parameter and return a boolean.
 
 ### Encrypting assertions
 
diff --git a/modules/saml/lib/IdP/SAML2.php b/modules/saml/lib/IdP/SAML2.php
index f141ff0cf..ee2dc1132 100644
--- a/modules/saml/lib/IdP/SAML2.php
+++ b/modules/saml/lib/IdP/SAML2.php
@@ -190,6 +190,7 @@ class SAML2
      * @param string|null               $AssertionConsumerServiceURL AssertionConsumerServiceURL from request.
      * @param string|null               $ProtocolBinding ProtocolBinding from request.
      * @param int|null                  $AssertionConsumerServiceIndex AssertionConsumerServiceIndex from request.
+     * @param bool                      $authnRequestSigned Whether or not the authn request was signed.
      *
      * @return array|null  Array with the Location and Binding we should use for the response.
      */
@@ -198,7 +199,8 @@ class SAML2
         Configuration $spMetadata,
         string $AssertionConsumerServiceURL = null,
         string $ProtocolBinding = null,
-        int $AssertionConsumerServiceIndex = null
+        int $AssertionConsumerServiceIndex = null,
+        bool $authnRequestSigned = false
     ): ?array {
         /* We want to pick the best matching endpoint in the case where for example
          * only the ProtocolBinding is given. We therefore pick endpoints with the
@@ -251,6 +253,26 @@ class SAML2
             return $firstFalse;
         }
 
+        $skipEndpointValidation = false;
+        if ($authnRequestSigned === true) {         
+            $skipEndpointValidationWhenSigned = $spMetadata->getValue('skipEndpointValidationWhenSigned', false);
+            if (is_bool($skipEndpointValidationWhenSigned) === true) {
+                $skipEndpointValidation = $skipEndpointValidationWhenSigned;
+            } elseif (is_callable($skipEndpointValidationWhenSigned) === true) {
+                $shouldSkipEndpointValidation = $skipEndpointValidationWhenSigned($spMetadata);
+                if (is_bool($shouldSkipEndpointValidation) === true) {
+                    $skipEndpointValidation = $shouldSkipEndpointValidation;
+                }
+            }
+        }
+
+        if (($AssertionConsumerServiceURL !== null) && ($skipEndpointValidation === true)) {
+            Logger::info(
+                'AssertionConsumerService specified in AuthnRequest not in metadata, using anyway because AuthnRequest signed and skipEndpointValidationWhenSigned was true'
+            );
+            return ['Location' => $AssertionConsumerServiceURL, 'Binding' => $ProtocolBinding];
+        }
+        
         Logger::warning('Authentication request specifies invalid AssertionConsumerService:');
         if ($AssertionConsumerServiceURL !== null) {
             Logger::warning('AssertionConsumerServiceURL: ' . var_export($AssertionConsumerServiceURL, true));
@@ -292,6 +314,8 @@ class SAML2
             $supportedBindings[] = Constants::BINDING_PAOS;
         }
 
+        $authnRequestSigned = false;
+        
         if (isset($_REQUEST['spentityid']) || isset($_REQUEST['providerId'])) {
             /* IdP initiated authentication. */
 
@@ -372,7 +396,7 @@ class SAML2
             $spEntityId = $issuer->getValue();
             $spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
 
-            \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $request);
+            $authnRequestSigned = \SimpleSAML\Module\saml\Message::validateMessage($spMetadata, $idpMetadata, $request);
 
             $relayState = $request->getRelayState();
 
@@ -424,7 +448,8 @@ class SAML2
             $spMetadata,
             $consumerURL,
             $protocolBinding,
-            $consumerIndex
+            $consumerIndex,
+            $authnRequestSigned
         );
         if ($acsEndpoint === null) {
             throw new Exception('Unable to use any of the ACS endpoints found for SP \'' . $spEntityId . '\'');
diff --git a/modules/saml/lib/Message.php b/modules/saml/lib/Message.php
index 3074dc85c..5a91a32cd 100644
--- a/modules/saml/lib/Message.php
+++ b/modules/saml/lib/Message.php
@@ -192,6 +192,7 @@ class Message
      * @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
      * @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
      * @param \SAML2\Message $message The message we should check the signature on.
+     * @return bool Whether or not the message was validated.
      *
      * @throws \SimpleSAML\Error\Exception if message validation is enabled, but there is no signature in the message.
      */
@@ -199,7 +200,7 @@ class Message
         Configuration $srcMetadata,
         Configuration $dstMetadata,
         \SAML2\Message $message
-    ): void {
+    ): bool {
         $enabled = null;
         if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) {
             $enabled = $srcMetadata->getBoolean('validate.logout', null);
@@ -228,12 +229,13 @@ class Message
         }
 
         if (!$enabled) {
-            return;
+            return false;
         } elseif (!self::checkSign($srcMetadata, $message)) {
             throw new SSP_Error\Exception(
                 'Validation of received messages enabled, but no signature found on message.'
             );
         }
+        return true;
     }
 
 
-- 
GitLab