diff --git a/lib/SAML2/ArtifactResolve.php b/lib/SAML2/ArtifactResolve.php index c7de093ba6e77e4c28001e1bdb653ac4733d01cb..b0e100649cc6836733e8cb4160a4cd8f0f7e4a02 100644 --- a/lib/SAML2/ArtifactResolve.php +++ b/lib/SAML2/ArtifactResolve.php @@ -42,7 +42,7 @@ class SAML2_ArtifactResolve extends SAML2_Request { * @param String The $artifact. */ public function setArtifact($artifact) { - + assert('is_string($artifact)'); $this->artifact = $artifact; } @@ -53,7 +53,10 @@ class SAML2_ArtifactResolve extends SAML2_Request { */ public function toUnsignedXML() { - throw new Exception('Not SUPPORTED'); + $root = parent::toUnsignedXML(); + $artifactelement = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'Artifact', $this->artifact); + $root->appendChild($artifactelement); + return $root; } diff --git a/lib/SAML2/HTTPArtifact.php b/lib/SAML2/HTTPArtifact.php index 0251a69ea4f17115406a03fca69b947a34f0e1de..962a1633bef2df3b8a9769e2fd8bff26f62c3ece 100644 --- a/lib/SAML2/HTTPArtifact.php +++ b/lib/SAML2/HTTPArtifact.php @@ -10,6 +10,8 @@ */ class SAML2_HTTPArtifact extends SAML2_Binding { + private $spMetadata; + /** * Create the redirect URL for a message. * @@ -50,7 +52,7 @@ class SAML2_HTTPArtifact extends SAML2_Binding { /** - * Receive a SAMLart. + * Receive a SAML 2 message sent using the HTTP-Artifact binding. * * Throws an exception if it is unable receive the message. * @@ -58,7 +60,81 @@ class SAML2_HTTPArtifact extends SAML2_Binding { */ public function receive() { - throw new Exception('Receiving SAML2 Artifact messages not supported.'); + if (array_key_exists('SAMLart', $_REQUEST)) { + $artifact = base64_decode($_REQUEST['SAMLart']); + $endpointIndex = bin2hex(substr($artifact,2,2)); + $sourceId = bin2hex(substr($artifact,4,20)); + + }else{ + throw new Execption('Missing SAMLArt parameter.'); + } + + $metadataHandler = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + + $idpmetadata = $metadataHandler->getMetaDataConfigForSha1($sourceId, 'saml20-idp-remote'); + + + $endpoint = NULL; + foreach ($idpmetadata->getEndpoints('ArtifactResolutionService') as $ep) { + if ($ep['index'] === hexdec($endpointIndex)) { + $endpoint = $ep; + break; + } + } + + if ($endpoint === NULL) { + throw new Exception('No ArtifactResolutionService with the correct index.'); + } + + SimpleSAML_Logger::debug("ArtifactResolutionService endpoint being used is := " . $endpoint['Location']); + + //Construct the ArtifactResolve Request + $ar = new SAML2_ArtifactResolve(); + + /* Set the request attributes */ + + $ar->setIssuer($this->spMetadata->getString('entityid')); + $ar->setArtifact($_REQUEST['SAMLart']); + $ar->setDestination($endpoint['Location']); + + /* Sign the request */ + sspmod_saml2_Message::addSign($this->spMetadata, $idpmetadata, $ar); // Shoaib - moved from the SOAPClient. + + $soap = new SAML2_SOAPClient(); + + // Send message through SoapClient + $artifactResponse = $soap->send($ar, $this->spMetadata); + + if (!$artifactResponse->isSuccess()) { + throw new Exception('Received error from ArtifactResolutionService.'); + } + + $xml = $artifactResponse->getAny(); + $samlresponse = SAML2_Message::fromXML($xml); + $samlresponse->addValidator(array(get_class($this), 'validateSignature'), $artifactResponse); + + + if (isset($_REQUEST['RelayState'])) { + $samlresponse->setRelayState($_REQUEST['RelayState']); + } + + return $samlresponse; + } + + + public function setSPMetadata($sp){ + $this->spMetadata = $sp; + } + + + /** + * A validator which returns TRUE if the ArtifactResponse was signed with the given key + * + * @return TRUE + */ + public static function validateSignature(SAML2_ArtifactResponse $message, XMLSecurityKey $key) { + + return $message->validate($key); } } diff --git a/lib/SAML2/SOAPClient.php b/lib/SAML2/SOAPClient.php new file mode 100644 index 0000000000000000000000000000000000000000..2c272f176e09bd3b40fe0e8d490ef98a184088be --- /dev/null +++ b/lib/SAML2/SOAPClient.php @@ -0,0 +1,113 @@ +<?php +/** + * Implementation of the SAML 2.0 SOAP binding. + * + * @author Shoaib Ali + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_SOAPClient { + + const START_SOAP_ENVELOPE = '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"><soap-env:Header/><soap-env:Body>'; + const END_SOAP_ENVELOPE = '</soap-env:Body></soap-env:Envelope>'; + + /** + * This function sends the SOAP message to the service location and returns SOAP response + * + * @param $ar SAML2_ArtifactResolve object. + * @return $soapresponse string + */ + public function send(SAML2_ArtifactResolve $ar, SimpleSAML_Configuration $spMetadata) { + + $issuer = $ar->getIssuer(); + + $options = array( + 'uri' => $issuer, + 'location' => $ar->getDestination(), + ); + + // Determine if we are going to do a MutualSSL connection between the IdP and SP - Shoaib + if ($spMetadata->hasValue('saml.SOAPClient.certificate')) { + $options['local_cert'] = SimpleSAML_Utilities::resolveCert($spMetadata->getString('saml.SOAPClient.certificate')); + if ($spMetadata->hasValue('saml.SOAPClient.privatekey_pass')) { + $options['passphrase'] = $spMetadata->getString('saml.SOAPClient.privatekey_pass'); + } + } else { + /* Use the SP certificate and privatekey if it is configured. */ + $privateKey = SimpleSAML_Utilities::loadPrivateKey($spMetadata); + $publicKey = SimpleSAML_Utilities::loadPublicKey($spMetadata); + if ($privateKey !== NULL && $publicKey !== NULL && isset($publicKey['PEM'])) { + $keyCertData = $privateKey['PEM'] . $publicKey['PEM']; + $file = SimpleSAML_Utilities::getTempDir() . '/' . sha1($keyCertData) . '.pem'; + if (!file_exists($file)) { + SimpleSAML_Utilities::writeFile($file, $keyCertData); + } + $options['local_cert'] = $file; + if (isset($privateKey['password'])) { + $options['passphrase'] = $privateKey['password']; + } + } + } + + $x = new SoapClient(NULL, $options); + + // Add soap-envelopes + $request = $ar->toSignedXML(); + $request = self::START_SOAP_ENVELOPE . $request->ownerDocument->saveXML($request) . self::END_SOAP_ENVELOPE; + + $action = 'http://www.oasis-open.org/committees/security'; + $version = '1.1'; + $destination = $ar->getDestination(); + + + /* Perform SOAP Request over HTTP */ + $soapresponsexml = $x->__doRequest($request, $destination, $action, $version); + + + // Convert to SAML2_Message (DOMElement) + $dom = new DOMDocument(); + if (!$dom->loadXML($soapresponsexml)) { + throw new Exception('Not a SOAP response.'); + } + + $soapfault = $this->getSOAPFault($dom); + if (isset($soapfault)) { + throw new Exception($soapfault); + } + //Extract the message from the response + $xml = $dom->firstChild; /* Soap Envelope */ + $samlresponse = SAML2_Utils::xpQuery($dom->firstChild, '/soap-env:Envelope/soap-env:Body/*[1]'); + $samlresponse = SAML2_Message::fromXML($samlresponse[0]); + + + simpleSAML_Logger::debug("Valid ArtifactResponse received from IdP"); + + return $samlresponse; + + } + + + /* + * Extracts the SOAP Fault from SOAP message + * @param $soapmessage Soap response needs to be type DOMDocument + * @return $soapfaultstring string|NULL + */ + private function getSOAPFault($soapmessage) { + + $soapfault = SAML2_Utils::xpQuery($soapmessage->firstChild, '/soap-env:Envelope/soap-env:Body/soap-env:Fault'); + + if (empty($soapfault)) { + /* No fault. */ + return NULL; + } + $soapfaultelement = $soapfault[0]; + $soapfaultstring = "Unknown fault string found"; // There is a fault element but we havn't found out what the fault string is + // find out the fault string + $faultstringelement = SAML2_Utils::xpQuery($soapfaultelement, './soap-env:faultstring') ; + if (!empty($faultstringelement)) { + return $faultstringelement[0]->textContent; + } + return $soapfaultstring; + } + +} diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php index 7494bf5a238cdbc058fc5d313bce18a1a84add80..91c999cd0fa08f2159466500db9b5565bc75774c 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandler.php @@ -307,6 +307,34 @@ class SimpleSAML_Metadata_MetaDataStorageHandler { return SimpleSAML_Configuration::loadFromArray($metadata, $set . '/' . var_export($entityId, TRUE)); } + public function getMetaDataConfigForSha1($sha1, $set) { + assert('is_string($sha1)'); + assert('is_string($set)'); + + + $result = array(); + + foreach($this->sources as $source) { + $srcList = $source->getMetadataSet($set); + + + /* $result is the last argument to array_merge because we want the content already + * in $result to have precedence. + */ + $result = array_merge($srcList, $result); + } + foreach($result as $remote_provider ){ + + if(sha1($remote_provider['entityid'])==$sha1){ + $remote_provider['metadata-set'] = $set; + + return SimpleSAML_Configuration::loadFromArray($remote_provider, $set . '/' . var_export($remote_provider['entityid'], TRUE)); + } + } + + return null; + } + } ?> \ No newline at end of file diff --git a/modules/saml/lib/Auth/Source/SP.php b/modules/saml/lib/Auth/Source/SP.php index 7224b6130de87c22e15e7bb238149f5c5b3f7067..f869414eac3fd3b62d5dde76fd5de85e51fff878 100644 --- a/modules/saml/lib/Auth/Source/SP.php +++ b/modules/saml/lib/Auth/Source/SP.php @@ -183,7 +183,6 @@ class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source { $ar = sspmod_saml2_Message::buildAuthnRequest($this->metadata, $idpMetadata); $ar->setAssertionConsumerServiceURL(SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->authId)); - $ar->setProtocolBinding(SAML2_Const::BINDING_HTTP_POST); if (isset($state['SimpleSAML_Auth_Default.ReturnURL'])) { $ar->setRelayState($state['SimpleSAML_Auth_Default.ReturnURL']); diff --git a/modules/saml/www/sp/saml2-acs.php b/modules/saml/www/sp/saml2-acs.php index f42e83ba61eb41807fabe6f777655b749b912a74..4c7160169f95b5f4f3d6c03efecdb699aadbe5ef 100644 --- a/modules/saml/www/sp/saml2-acs.php +++ b/modules/saml/www/sp/saml2-acs.php @@ -6,8 +6,13 @@ $sourceId = substr($_SERVER['PATH_INFO'], 1); $source = SimpleSAML_Auth_Source::getById($sourceId, 'sspmod_saml_Auth_Source_SP'); +$spMetadata = $source->getMetadata(); $b = SAML2_Binding::getCurrentBinding(); +if ($b instanceof SAML2_HTTPArtifact) { + $b->setSPMetadata($spMetadata); +} + $response = $b->receive(); if (!($response instanceof SAML2_Response)) { throw new SimpleSAML_Error_BadRequest('Invalid message received to AssertionConsumerService endpoint.'); @@ -40,7 +45,6 @@ if ($idp === NULL) { SimpleSAML_Logger::debug('Received SAML2 Response from ' . var_export($idp, TRUE) . '.'); $idpMetadata = $source->getIdPmetadata($idp); -$spMetadata = $source->getMetadata(); try { $assertion = sspmod_saml2_Message::processResponse($spMetadata, $idpMetadata, $response); diff --git a/modules/saml2/lib/Message.php b/modules/saml2/lib/Message.php index 918cbe626ca769e0bd18317543055ac94599bb64..38de9951ff0df6511aa3bcc01f40060a8a2045e6 100644 --- a/modules/saml2/lib/Message.php +++ b/modules/saml2/lib/Message.php @@ -385,6 +385,15 @@ class sspmod_saml2_Message { $ar->setForceAuthn($spMetadata->getBoolean('ForceAuthn', FALSE)); $ar->setIsPassive($spMetadata->getBoolean('IsPassive', FALSE)); + $protbind = $spMetadata->getValueValidate('ProtocolBinding', array( + SAML2_Const::BINDING_HTTP_POST, + SAML2_Const::BINDING_HTTP_ARTIFACT, + SAML2_Const::BINDING_HTTP_REDIRECT, + ), SAML2_Const::BINDING_HTTP_POST); + + /* Shoaib - setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST */ + $ar->setProtocolBinding($protbind); + if ($spMetadata->hasValue('AuthnContextClassRef')) { $accr = $spMetadata->getArrayizeString('AuthnContextClassRef'); $ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr)); diff --git a/www/saml2/sp/AssertionConsumerService.php b/www/saml2/sp/AssertionConsumerService.php index 2fa55b819c8e3776e7539d0a40dc77cec9f2f0e4..bfe0c7766719c60642816901403d26e54d14e680 100644 --- a/www/saml2/sp/AssertionConsumerService.php +++ b/www/saml2/sp/AssertionConsumerService.php @@ -63,13 +63,16 @@ if (array_key_exists(SimpleSAML_Auth_ProcessingChain::AUTHPARAM, $_REQUEST)) { } -if (empty($_REQUEST['SAMLResponse'])) - SimpleSAML_Utilities::fatalError($session->getTrackID(), 'ACSPARAMS', $exception); - - try { + $metadataHandler = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); + $sp = $metadataHandler->getMetaDataCurrentEntityID(); + $spMetadata = $metadataHandler->getMetaDataConfig($sp, 'saml20-sp-hosted'); $b = SAML2_Binding::getCurrentBinding(); + if ($b instanceof SAML2_HTTPArtifact) { + $b->setSPMetadata($spMetadata); + } + $response = $b->receive(); if (!($response instanceof SAML2_Response)) { throw new SimpleSAML_Error_BadRequest('Invalid message received to AssertionConsumerService endpoint.'); @@ -80,11 +83,8 @@ try { throw new Exception('Missing <saml:Issuer> in message delivered to AssertionConsumerService.'); } - $metadataHandler = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler(); - $sp = $metadataHandler->getMetaDataCurrentEntityID(); $idpMetadata = $metadataHandler->getMetaDataConfig($idp, 'saml20-idp-remote'); - $spMetadata = $metadataHandler->getMetaDataConfig($sp, 'saml20-sp-hosted'); /* Fetch the request information if it exists, fall back to RelayState if not. */ $requestId = $response->getInResponseTo(); diff --git a/www/saml2/sp/initSSO.php b/www/saml2/sp/initSSO.php index 02058b20c784facc673bd1d99d4e31c69a24039f..4146d103f68c210fd4a3aade32ac5c54b0b57ded 100644 --- a/www/saml2/sp/initSSO.php +++ b/www/saml2/sp/initSSO.php @@ -136,7 +136,6 @@ try { $assertionConsumerServiceURL = $metadata->getGenerated('AssertionConsumerService', 'saml20-sp-hosted'); $ar->setAssertionConsumerServiceURL($assertionConsumerServiceURL); - $ar->setProtocolBinding(SAML2_Const::BINDING_HTTP_POST); $ar->setRelayState($_REQUEST['RelayState']); if ($isPassive) {