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