From 96e7c25ebbeca7f5ff896b8b5edb7ade09eefcc1 Mon Sep 17 00:00:00 2001
From: Olav Morken <olav.morken@uninett.no>
Date: Thu, 8 Oct 2009 09:55:07 +0000
Subject: [PATCH] saml: Add SAML 1 artifact support.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1830 44740490-163a-0410-bde0-09ae8108e29a
---
 docs/simplesamlphp-reference-idp-remote.txt |   9 +-
 lib/SimpleSAML/Bindings/Shib13/Artifact.php | 172 ++++++++++++++++++++
 lib/SimpleSAML/XML/Shib13/AuthnResponse.php |  30 ++++
 modules/saml/docs/sp.txt                    |   8 +
 modules/saml/lib/Auth/Source/SP.php         |  13 +-
 modules/saml/www/sp/saml1-acs.php           |  19 ++-
 6 files changed, 245 insertions(+), 6 deletions(-)
 create mode 100644 lib/SimpleSAML/Bindings/Shib13/Artifact.php

diff --git a/docs/simplesamlphp-reference-idp-remote.txt b/docs/simplesamlphp-reference-idp-remote.txt
index d9cc44da4..fbe7de315 100644
--- a/docs/simplesamlphp-reference-idp-remote.txt
+++ b/docs/simplesamlphp-reference-idp-remote.txt
@@ -122,7 +122,14 @@ These options overrides the options set in `saml20-sp-hosted`.
 Shibboleth 1.3 options
 ----------------------
 
-There are no options specific for a Shibboleth 1.3 IdP.
+`saml1.useartifact`
+:   Request that the IdP returns the result to the artifact binding.
+    The default is to use the POST binding, set this option to TRUE to use the artifact binding instead.
+
+:   This option can be set for all IdPs connected to a SP by setting it in the entry for the SP in `config/authsources.php`.
+
+:   *Note*: This option only works with the `saml:SP` authentication source.
+
 
 
 Examples
diff --git a/lib/SimpleSAML/Bindings/Shib13/Artifact.php b/lib/SimpleSAML/Bindings/Shib13/Artifact.php
new file mode 100644
index 000000000..befa748b0
--- /dev/null
+++ b/lib/SimpleSAML/Bindings/Shib13/Artifact.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * Implementation of the Shibboleth 1.3 Artifact binding.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class SimpleSAML_Bindings_Shib13_Artifact {
+
+	/**
+	 * Parse the query string, and extract the SAMLart parameters.
+	 *
+	 * This function is required because each query contains multiple
+	 * artifact with the same parameter name.
+	 *
+	 * @return array  The artifacts.
+	 */
+	private static function getArtifacts() {
+		assert('array_key_exists("QUERY_STRING", $_SERVER)');
+
+		/* We need to process the query string manually, to capture all SAMLart parameters. */
+
+		$artifacts = array();
+
+		$elements = explode('&', $_SERVER['QUERY_STRING']);
+		foreach ($elements as $element) {
+			list($name, $value) = explode('=', $element, 2);
+			$name = urldecode($name);
+			$value = urldecode($value);
+
+			if ($name === 'SAMLart') {
+				$artifacts[] = $value;
+			}
+		}
+
+		return $artifacts;
+	}
+
+
+	/**
+	 * Build the request we will send to the IdP.
+	 *
+	 * @param array $artifacts  The artifacts we will request.
+	 * @return string  The request, as an XML string.
+	 */
+	private static function buildRequest(array $artifacts) {
+
+		$msg = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">' .
+			'<SOAP-ENV:Body>' .
+			'<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"' .
+			' RequestID="' . SimpleSAML_Utilities::generateID() . '"' .
+			' MajorVersion="1" MinorVersion="1"' .
+			' IssueInstant="' . SimpleSAML_Utilities::generateTimestamp() . '"' .
+			'>';
+
+		foreach ($artifacts as $a) {
+			$msg .= '<samlp:AssertionArtifact>' . htmlspecialchars($a) . '</samlp:AssertionArtifact>';
+		}
+
+		$msg .= '</samlp:Request>' .
+			'</SOAP-ENV:Body>' .
+			'</SOAP-ENV:Envelope>';
+
+		return $msg;
+	}
+
+
+	/**
+	 * Extract the response element from the SOAP response.
+	 *
+	 * @param string $soapResponse  The SOAP response.
+	 * @return string  The <saml1p:Response> element, as a string.
+	 */
+	private static function extractResponse($soapResponse) {
+		assert('is_string($soapResponse)');
+
+		$doc = new DOMDocument();
+		if (!$doc->loadXML($soapResponse)) {
+			throw new SimpleSAML_Error_Exception('Error parsing SAML 1 artifact response.');
+		}
+
+		$soapEnvelope = $doc->firstChild;
+		if (!SimpleSAML_Utilities::isDOMElementOfType($soapEnvelope, 'Envelope', 'http://schemas.xmlsoap.org/soap/envelope/')) {
+			throw new SimpleSAML_Error_Exception('Expected artifact response to contain a <soap:Envelope> element.');
+		}
+
+		$soapBody = SimpleSAML_Utilities::getDOMChildren($soapEnvelope, 'Body', 'http://schemas.xmlsoap.org/soap/envelope/');
+		if (count($soapBody) === 0) {
+			throw new SimpleSAML_Error_Exception('Couldn\'t find <soap:Body> in <soap:Envelope>.');
+		}
+		$soapBody = $soapBody[0];
+
+
+		$responseElement = SimpleSAML_Utilities::getDOMChildren($soapBody, 'Response', 'urn:oasis:names:tc:SAML:1.0:protocol');
+		if (count($responseElement) === 0) {
+			throw new SimpleSAML_Error_Exception('Couldn\'t find <saml1p:Response> in <soap:Body>.');
+		}
+		$responseElement = $responseElement[0];
+
+		/*
+		 * Save the <saml1p:Response> element. Note that we need to import it
+		 * into a new document, in order to preserve namespace declarations.
+		 */
+		$newDoc = new DOMDocument();
+		$newDoc->appendChild($newDoc->importNode($responseElement, TRUE));
+		$responseXML = $newDoc->saveXML();
+
+		return $responseXML;
+	}
+
+
+	/**
+	 * This function receives a SAML 1.1 artifact.
+	 *
+	 * @param SimpleSAML_Configuration $spMetadata  The metadata of the SP.
+	 * @param SimpleSAML_Configuration $idpMetadata  The metadata of the IdP.
+	 * @return string  The <saml1p:Response> element, as an XML string.
+	 */
+	public static function receive(SimpleSAML_Configuration $spMetadata, SimpleSAML_Configuration $idpMetadata) {
+
+		$artifacts = self::getArtifacts();
+		$request = self::buildRequest($artifacts);
+
+		$url = 'https://skjak.uninett.no:1245/test...';
+		$url = $idpMetadata->getString('ArtifactResolutionService');
+
+		$certData = SimpleSAML_Utilities::loadPublicKey($idpMetadata->toArray(), TRUE);
+		if (!array_key_exists('PEM', $certData)) {
+			throw new SimpleSAML_Error_Exception('Missing one of certData or certificate in metadata for '
+				. var_export($idpMetadata->getString('entityid'), TRUE));
+		}
+		$certData = $certData['PEM'];
+
+		$file = SimpleSAML_Utilities::getTempDir() . '/' . sha1($certData) . '.crt';
+		if (!file_exists($file)) {
+			SimpleSAML_Utilities::writeFile($file, $certData);
+		}
+
+		$globalConfig = SimpleSAML_Configuration::getInstance();
+		$spKeyCertFile = $globalConfig->getPathValue('certdir', 'cert/') . $spMetadata->getString('privatekey');
+
+		$opts = array(
+			'ssl' => array(
+				'verify_peer' => TRUE,
+				'cafile' => $file,
+				'local_cert' => $spKeyCertFile,
+				'capture_peer_cert' => TRUE,
+				'capture_peer_chain' => TRUE,
+			),
+			'http' => array(
+				'method' => 'POST',
+				'content' => $request,
+				'header' => 'SOAPAction: http://www.oasis-open.org/committees/security' . "\r\n" .
+					'Content-Type: text/xml',
+			),
+		);
+		$context = stream_context_create($opts);
+
+		/* Fetch the artifact. */
+		$response = file_get_contents($url, FALSE, $context);
+		if ($response === FALSE) {
+			throw new SimpleSAML_Error_Exception('Failed to retrieve assertion from IdP.');
+		}
+
+		/* Find the response in the SOAP message. */
+		$response = self::extractResponse($response);
+
+		return $response;
+	}
+
+}
\ No newline at end of file
diff --git a/lib/SimpleSAML/XML/Shib13/AuthnResponse.php b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php
index e4380206c..5a3ba1406 100644
--- a/lib/SimpleSAML/XML/Shib13/AuthnResponse.php
+++ b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php
@@ -15,6 +15,14 @@ class SimpleSAML_XML_Shib13_AuthnResponse {
 	private $validator = null;
 
 
+	/**
+	 * Whether this response was validated by some external means (e.g. SSL).
+	 *
+	 * @var bool
+	 */
+	private $messageValidated = FALSE;
+
+
 	const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol';
 	const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion';
 
@@ -34,6 +42,18 @@ class SimpleSAML_XML_Shib13_AuthnResponse {
 	private $relayState = null;
 
 
+	/**
+	 * Set whether this message was validated externally.
+	 *
+	 * @param bool $messageValidated  TRUE if the message is already validated, FALSE if not.
+	 */
+	public function setMessageValidated($messageValidated) {
+		assert('is_bool($messageValidated)');
+
+		$this->messageValidated = $messageValidated;
+	}
+
+
 	public function setXML($xml) {
 		assert('is_string($xml)');
 
@@ -55,6 +75,11 @@ class SimpleSAML_XML_Shib13_AuthnResponse {
 	public function validate() {
 		assert('$this->dom instanceof DOMDocument');
 
+		if ($this->messageValidated) {
+			/* This message was validated externally. */
+			return TRUE;
+		}
+
 		/* Validate the signature. */
 		$this->validator = new SimpleSAML_XML_Validator($this->dom, array('ResponseID', 'AssertionID'));
 
@@ -90,6 +115,11 @@ class SimpleSAML_XML_Shib13_AuthnResponse {
 	 */
 	private function isNodeValidated($node) {
 
+		if ($this->messageValidated) {
+			/* This message was validated externally. */
+			return TRUE;
+		}
+
 		if($this->validator === NULL) {
 			return FALSE;
 		}
diff --git a/modules/saml/docs/sp.txt b/modules/saml/docs/sp.txt
index d2270c5a8..621193e7b 100644
--- a/modules/saml/docs/sp.txt
+++ b/modules/saml/docs/sp.txt
@@ -130,6 +130,14 @@ Options
 
 :   *Note*: SAML 2 specific.
 
+`saml1.useartifact`
+:   Request that the IdP returns the result to the artifact binding.
+    The default is to use the POST binding, set this option to TRUE to use the artifact binding instead.
+
+:   This option can also be set in the `shib13-idp-remote` metadata, in which case the setting in `shib13-idp-remote` takes precedence.
+
+:   *Note*: SAML 1 specific.
+
 `redirect.sign`
 :   Whether authentication requests, logout requests and logout responses sent from this SP should be signed. The default is `FALSE`.
 
diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php
index 7fc81189f..249bf4310 100644
--- a/modules/saml/lib/Auth/Source/SP.php
+++ b/modules/saml/lib/Auth/Source/SP.php
@@ -149,7 +149,18 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
 		$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:ssosent-saml1');
 		$ar->setRelayState($id);
 
-		$url = $ar->createRedirect($idpEntityId, SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId));
+		$useArtifact = $idpMetadata->getBoolean('saml1.useartifact', NULL);
+		if ($useArtifact === NULL) {
+			$useArtifact = $this->metadata->getBoolean('saml1.useartifact', FALSE);
+		}
+
+		if ($useArtifact) {
+			$shire = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId . '/artifact');
+		} else {
+			$shire = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId);
+		}
+
+		$url = $ar->createRedirect($idpEntityId, $shire);
 
 		SimpleSAML_Logger::debug('Starting SAML 1 SSO to ' . var_export($idpEntityId, TRUE) .
 			' from ' . var_export($this->entityId, TRUE) . '.');
diff --git a/modules/saml/www/sp/saml1-acs.php b/modules/saml/www/sp/saml1-acs.php
index 336d47fa4..1568f06c2 100644
--- a/modules/saml/www/sp/saml1-acs.php
+++ b/modules/saml/www/sp/saml1-acs.php
@@ -1,7 +1,7 @@
 <?php
 
-if (!array_key_exists('SAMLResponse', $_REQUEST)) {
-	throw new SimpleSAML_Error_BadRequest('Missing SAMLResponse parameter.');
+if (!array_key_exists('SAMLResponse', $_REQUEST) && !array_key_exists('SAMLart', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing SAMLResponse or SAMLart parameter.');
 }
 
 if (!array_key_exists('TARGET', $_REQUEST)) {
@@ -26,15 +26,26 @@ if ($state['saml:sp:AuthId'] !== $sourceId) {
 	throw new SimpleSAML_Error_Exception('The authentication source id in the URL does not match the authentication source which sent the request.');
 }
 
+$spMetadata = $source->getMetadata();
+
 $idpEntityId = $state['saml:idp'];
 $idpMetadata = $source->getIdPMetadata($idpEntityId);
 
-$responseXML = $_REQUEST['SAMLResponse'];
-$responseXML = base64_decode($responseXML);
+if (array_key_exists('SAMLart', $_REQUEST)) {
+	$responseXML = SimpleSAML_Bindings_Shib13_Artifact::receive($spMetadata, $idpMetadata);
+	$isValidated = TRUE; /* Artifact binding validated with ssl certificate. */
+} elseif (array_key_exists('SAMLResponse', $_REQUEST)) {
+	$responseXML = $_REQUEST['SAMLResponse'];
+	$responseXML = base64_decode($responseXML);
+	$isValidated = FALSE; /* Must check signature on response. */
+} else {
+	assert('FALSE');
+}
 
 $response = new SimpleSAML_XML_Shib13_AuthnResponse();
 $response->setXML($responseXML);
 
+$response->setMessageValidated($isValidated);
 $response->validate();
 
 $responseIssuer = $response->getIssuer();
-- 
GitLab