Skip to content
Snippets Groups Projects
Commit b5ea42a5 authored by Olav Morken's avatar Olav Morken
Browse files

Changed SAML2 AuthnResponse processing.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@231 44740490-163a-0410-bde0-09ae8108e29a
parent 4a53f2bd
No related branches found
No related tags found
No related merge requests found
...@@ -34,38 +34,55 @@ class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse { ...@@ -34,38 +34,55 @@ class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse {
/** /**
* This variable contains an XML validator for this message. * This variable contains an XML validator for this message.
*/ */
private $validator = null; private $validator = NULL;
function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore) {
$this->configuration = $configuration;
$this->metadata = $metadatastore;
}
/**
public function validate() { * This varaible contains the entitiyid of the IdP which issued this message.
*/
$dom = $this->getDOM(); private $issuer = NULL;
/* Validate the signature. */
$this->validator = new SimpleSAML_XML_Validator($dom, 'ID');
// Get the issuer of the response. /**
$issuer = $this->getIssuer(); * This variable contains the NameID of this subject. It is an associative array with
* two keys:
* - 'Format' The type of the NameID.
* - 'value' Tha value of the NameID.
*
* This variable will be set by the processSubject function. A exception will be thrown if the response
* contains two different NameIDs.
*/
private $nameid = NULL;
/* Get the metadata of the issuer. */
$md = $this->metadata->getMetaData($issuer, 'saml20-idp-remote');
/* Get fingerprint for the certificate of the issuer. */ /**
$issuerFingerprint = $md['certFingerprint']; * This variable contains the SessionIndex, as set by a AuthnStatement element in an assertion.
*/
private $sessionIndex = NULL;
/**
* This associative array contains the attribute we extract from the response.
*/
private $attributes = array();
/* Validate the fingerprint. */
$this->validator->validateFingerprint($issuerFingerprint);
return true; function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore) {
$this->configuration = $configuration;
$this->metadata = $metadatastore;
} }
/* The following methods aren't used anymore. They are included because it is required by inheritance.
* TODO: Remove them.
*/
public function validate() { throw new Exception('TODO!'); }
public function createSession() { throw new Exception('TODO!'); }
public function getAttributes() { throw new Exception('TODO!'); }
public function getIssuer() { throw new Exception('TODO!'); }
public function getNameID() { throw new Exception('TODO!'); }
/** /**
* This function runs an xPath query on this authentication response. * This function runs an xPath query on this authentication response.
* *
...@@ -94,128 +111,314 @@ class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse { ...@@ -94,128 +111,314 @@ class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse {
} }
public function createSession() { /**
* This function checks if the user has added the given id to 'saml2.relaxvalidation'
SimpleSAML_Session::init(true, 'saml2'); * in the saml2-idp-remote configuration.
$session = SimpleSAML_Session::getInstance(); *
$session->setAttributes($this->getAttributes()); * @param $id The id which identifies a part of the verification which may be relaxed.
* @return TRUE if this id is added to the list, FALSE if not.
$session->setNameID($this->getNameID()); */
$session->setSessionIndex($this->getSessionIndex()); private function isValidationRelaxed($id) {
$session->setIdP($this->getIssuer());
assert('is_string($id)');
return $session; assert('$this->issuer != NULL');
/* Get the metadata of the issuer. */
$md = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');
if(!array_key_exists('saml2.relaxvalidation', $md)) {
/* The user hasn't added a saml2.relaxvalidation option. */
return FALSE;
}
$rv = $md['saml2.relaxvalidation'];
if(!is_array($rv)) {
throw new Exception('saml2.relaxvalidation must be an array.');
}
return in_array($id, $rv, TRUE);
} }
// TODO: Not tested, but neigther is it used. /**
function getSessionIndex() { * This function finds the issuer of this response. It will first search the Response element,
$token = $this->getDOM(); * and if it isn't found there, it will search all Assertion elements.
if ($token instanceof DOMDocument) { */
$xPath = new DOMXpath($token); private function findIssuer() {
$xPath->registerNamespace('mysaml', self::SAML2_ASSERT_NS);
$xPath->registerNamespace('mysamlp', self::SAML2_PROTOCOL_NS); /* First check the Response element. */
$issuer = $this->doXPathQuery('/samlp:Response/saml:Issuer')->item(0);
$query = '/mysamlp:Response/mysaml:Assertion/mysaml:AuthnStatement'; if($issuer !== NULL) {
$nodelist = $xPath->query($query); return $issuer->textContent;
if ($node = $nodelist->item(0)) {
return $node->getAttribute('SessionIndex');
}
} }
return NULL;
/* Then we search the Assertion elements. */
$issuers = $this->doXPathQuery('/samlp:Response/saml:Assertion/saml:Issuer');
if($issuers->length === 0) {
throw new Exception('Unable to determine the issuer of this SAML2 AuthnResponse message.');
}
/* Since all Issuer elements should be equal in this version of simpleSAMLphp, we pick
* the first Issuer element we find.
*/
return $issuers->item(0)->textContent;
} }
/**
* This function validates the signature element of this response. It will throw an exception
* if it is unable to validate the signature.
*/
private function validateSignature() {
public function getAttributes() { $dom = $this->getDOM();
/* Validate the signature. */
$this->validator = new SimpleSAML_XML_Validator($dom, 'ID');
$md = $this->metadata->getMetadata($this->getIssuer(), 'saml20-idp-remote'); /* Get the metadata of the issuer. */
$md = $this->metadata->getMetaData($this->issuer, 'saml20-idp-remote');
$base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false; /* Get fingerprint for the certificate of the issuer. */
$issuerFingerprint = $md['certFingerprint'];
/* Validate the fingerprint. */
$this->validator->validateFingerprint($issuerFingerprint);
}
/**
* This function processes a Subject node. It will throw
* an Exception if the subject cannot be confirmed. On successful verification,
* the data stored about this subject will be saved.
*/
private function processSubject($subject) {
/* We currently require urn:oasis:names:tc:SAML:2.0:cm:bearer subject confirmation. */
$bearerValidated = false;
$attributes = array(); /* Iterate over the SubjectConfirmation nodes, looking for it. */
$token = $this->getDOM(); foreach($this->doXPathQuery('saml:SubjectConfirmation', $subject) as $subjectConfirmation) {
$method = $subjectConfirmation->getAttributeNode('Method');
if($method === NULL) {
throw new Exception('SubjectConfirmation is missing the required Method attribute.');
}
if($method->value !== 'urn:oasis:names:tc:SAML:2.0:cm:bearer') {
throw new Exception('Unhandled SubjectConfirmationData: ' . $method->value);
}
$subjectConfirmationData = $this->doXPathQuery('saml:SubjectConfirmationData', $subjectConfirmation);
if($subjectConfirmationData === NULL) {
throw new Exception('Bearer confirmation node without verification data.');
}
if($this->validator === NULL) { /* TODO: Verify this subject. */
throw new Exception('Called getAttributes on a SAML2 AuthnResponse which hasn\'t been validated.');
} }
if ( !($token instanceof DOMDocument)) { /* We expect the subject node to contain a NameID element which identifies this subject. */
throw new Exception('Called getAttributes on a SAML2 AuthnResponse which doesn\'t contain a message.'); $nameid = $this->doXPathQuery('saml:NameID', $subject)->item(0);
if($nameid === NULL) {
throw new Exception('Could not find the NameID node in a Subject node.');
} }
$assertions = $this->doXPathQuery('/samlp:Response/saml:Assertion'); $format = $nameid->getAttribute('Format');
foreach($assertions as $assertion) { $value = $nameid->textContent;
if(!$this->validator->isNodeValidated($assertion)) { if($this->nameid === NULL) {
throw new Exception('A SAML2 AuthnResponse contained an Assertion which isn\'t verified by the signature.'); /* We haven't saved a nameID earlier. Save it now. */
} $this->nameid = array('Format' => $format, 'value' => $value);
return;
}
foreach($this->doXPathQuery('saml:Conditions', $assertion) as $condition) { /* We have saved a nameID earlier. Verify that this nameID is equal. */
if($this->nameid['Format'] !== $format || $this->nameid['value'] !== $value) {
throw new Exception('Multiple assertions with different nameIDs is unsupported by simpleSAMLphp');
}
}
$start = $condition->getAttribute("NotBefore");
$end = $condition->getAttribute("NotOnOrAfter");
if (! SimpleSAML_Utilities::checkDateConditions($start, $end)) { /**
throw new Exception("Date check failed (between $start and $end). Check if the clocks on the SP and IdP are synchronized. Alternatively you can get this message, when you move back in history or refresh an old page."); * This function processes a Conditions node. It will throw an exception if any of the conditions
} * are invalid.
*/
private function processConditions($conditions) {
/* First verify the NotBefore and NotOnOrAfter attributes if they are present. */
$notBefore = $conditions->getAttribute("NotBefore");
$notOnOrAfter = $conditions->getAttribute("NotOnOrAfter");
if (! SimpleSAML_Utilities::checkDateConditions($notBefore, $notOnOrAfter)) {
throw new Exception('Date check failed (between ' . $notBefore . ' and ' . $notOnOrAfter . ').' .
' Check if the clocks on the SP and IdP are synchronized. Alternatively' .
' you can get this message, when you move back in history or refresh an old page.');
}
if($this->doXPathQuery('Condition', $conditions)->length > 0) {
if(!$this->isValidationRelaxed('unknowncondition')) {
throw new Exception('A Conditions node in a SAML2 AuthnResponse contained a' .
' Condition node. This is unsupported by simpleSAMLphp. To disable this' .
' check, add \'unknowncondition\' to the \'saml2.relaxvalidation\' list in' .
' \'saml2-idp-remote\'.');
} }
}
foreach($this->doXPathQuery('saml:AttributeStatement/saml:Attribute/saml:AttributeValue', $assertion) as $attribute) {
$name = $attribute->parentNode->getAttribute('Name'); $spEntityId = $this->metadata->getMetaDataCurrentEntityID('saml20-sp-hosted');
$value = $attribute->textContent;
if(!array_key_exists($name, $attributes)) { /* The specification says that every AudienceRestriction element must be valid, but only one
$attributes[$name] = array(); * Audience element in each AudienceRestriction element must be valid.
} */
foreach($this->doXPathQuery('AudienceRestriction', $conditions) as $ar) {
if ($base64) { $validAudience = false;
foreach(explode('_', $value) AS $v) { foreach($this->doXPathQuery('Audience', $ar) as $a) {
$attributes[$name][] = base64_decode($v); if($a->textContent === $spEntityId) {
} $validAudience = true;
} else {
$attributes[$name][] = $value;
} }
} }
if(!$validAudience) {
throw new Exception('Could not verify audience of SAML2 AuthnResponse.');
}
} }
return $attributes; /* We ignore OneTimeUse and ProxyRestriction conditions. */
} }
public function getIssuer() { /**
$dom = $this->getDOM(); * This function processes a AuthnStatement node. It will throw an exception if the statement is
$issuer = null; * invalid.
if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { */
if ($issuerNodes->length > 0) { private function processAuthnStatement($authnStatement) {
$issuer = $issuerNodes->item(0)->textContent; /* Extract the SessionIndex. */
$sessionIndex = $authnStatement->getAttributeNode('SessionIndex');
if($sessionIndex !== NULL) {
$sessionIndex = $sessionIndex->value;
if($this->sessionIndex === NULL) {
$this->sessionIndex = $sessionIndex;
} elseif($this->sessionIndex !== $sessionIndex) {
throw new Exception('Got two different session indexes in a SAML2 AuthnResponse.');
} }
} }
return $issuer;
} }
public function getNameID() {
/**
$dom = $this->getDOM(); * This function processes a AttributeStatement node.
$nameID = array(); */
private function processAttributeStatement($attributeStatement) {
if ($dom instanceof DOMDocument) {
$xPath = new DOMXpath($dom); $md = $this->metadata->getMetadata($this->issuer, 'saml20-idp-remote');
$xPath->registerNamespace('mysaml', self::SAML2_ASSERT_NS); $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false;
$xPath->registerNamespace('mysamlp', self::SAML2_PROTOCOL_NS);
foreach($this->doXPathQuery('saml:Attribute/saml:AttributeValue', $attributeStatement) as $attribute) {
$query = '/mysamlp:Response/mysaml:Assertion/mysaml:Subject/mysaml:NameID';
$nodelist = $xPath->query($query); $name = $attribute->parentNode->getAttribute('Name');
if ($node = $nodelist->item(0)) { $value = $attribute->textContent;
$nameID["value"] = $node->nodeValue; if(!array_key_exists($name, $this->attributes)) {
//$nameID["NameQualifier"] = $node->getAttribute('NameQualifier'); $this->attributes[$name] = array();
//$nameID["SPNameQualifier"] = $node->getAttribute('SPNameQualifier'); }
$nameID["Format"] = $node->getAttribute('Format');
if ($base64) {
foreach(explode('_', $value) AS $v) {
$this->attributes[$name][] = base64_decode($v);
}
} else {
$this->attributes[$name][] = $value;
} }
} }
return $nameID; }
/**
* This function processes a Assertion node. It will throw an exception if the assertion is invalid.
*/
private function processAssertion($assertion) {
/* Make sure that the assertion is signed. */
if(!$this->validator->isNodeValidated($assertion)) {
throw new Exception('A SAML2 AuthnResponse contained an Assertion which isn\'t verified by' .
' the signature.');
}
$subject = $this->doXPathQuery('saml:Subject', $assertion)->item(0);
if($subject === NULL) {
if(!$this->isValidationRelaxed('nosubject')) {
throw new Exception('Could not find required Subject information in a SAML2' .
' AuthnResponse. To disable this check, add \'nosubject\' to the' .
' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
}
} else {
$this->processSubject($subject);
}
$conditions = $this->doXPathQuery('saml:Conditions', $assertion)->item(0);
if($conditions === NULL) {
if(!$this->isValidationRelaxed('noconditions')) {
throw new Exception('Could not find required Conditions node in a SAML2' .
' AuthnResponse. To disable this check, add \'noconditions\' to the' .
' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
}
} else {
$this->processConditions($conditions);
}
$authnStatement = $this->doXPathQuery('saml:AuthnStatement', $assertion)->item(0);
if($authnStatement === NULL) {
if(!$this->isValidationRelaxed('noauthnstatement')) {
throw new Exception('Could not find required AuthnStatement node in a SAML2' .
' AuthnResponse. To disable this check, add \'noauthnstatement\' to the' .
' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
}
} else {
$this->processAuthnStatement($authnStatement);
}
$attributeStatement = $this->doXPathQuery('saml:AttributeStatement', $assertion)->item(0);
if($attributeStatement === NULL) {
if(!$this->isValidationRelaxed('noattributestatement')) {
throw new Exception('Could not find required AttributeStatement in a SAML2' .
' AuthnResponse. To disable this check, add \'noattributestatement\' to the' .
' \'saml2.relaxvalidation\' list in \'saml2-idp-remote\'.');
}
} else {
$this->processAttributeStatement($attributeStatement);
}
}
/**
* This function processes a response message and adds information from it to the
* current session if it is valid. It throws an exception if it is invalid.
*/
public function process() {
/* Find the issuer of this response. */
$this->issuer = $this->findIssuer();
/* Validate the signature element. */
$this->validateSignature();
/* Process all assertions. */
$assertions = $this->doXPathQuery('/samlp:Response/saml:Assertion');
foreach($assertions as $assertion) {
$this->processAssertion($assertion);
}
if($this->nameid === NULL) {
throw new Exception('No nameID found in AuthnResponse.');
}
/* Update the session information */
SimpleSAML_Session::init(true, 'saml2');
$session = SimpleSAML_Session::getInstance();
$session->setAttributes($this->attributes);
$session->setNameID($this->nameid);
$session->setSessionIndex($this->sessionIndex);
$session->setIdP($this->issuer);
} }
......
...@@ -31,6 +31,25 @@ $metadata = array( ...@@ -31,6 +31,25 @@ $metadata = array(
'request.signing' => false, 'request.signing' => false,
'certificate' => "idp.example.org.crt", 'certificate' => "idp.example.org.crt",
/*
* It is possible to relax some parts of the validation of SAML2 messages.
* To relax a part, add the id to the 'saml2.relaxvalidation' array.
*
* Valid ids:
* - 'unknowncondition' Disables errors when encountering unknown <Condition> nodes.
* - 'nosubject' Ignore missing <Subject> in <Assertion>.
* - 'noconditions' Ignore missing <Conditions> in <Assertion>.
* - 'noauthnstatement' Ignore missing <AuthnStatement> in <Assertion>.
* - 'noattributestatement' Ignore missing <AttributeStatement> in <Assertion>.
*
* Example:
* 'saml2.relaxvalidation' => array('unknowncondition', 'noattributestatement'),
*
* Default:
* 'saml2.relaxvalidation' => array(),
*/
'saml2.relaxvalidation' => array(),
), ),
......
...@@ -41,25 +41,17 @@ try { ...@@ -41,25 +41,17 @@ try {
$binding = new SimpleSAML_Bindings_SAML20_HTTPPost($config, $metadata); $binding = new SimpleSAML_Bindings_SAML20_HTTPPost($config, $metadata);
$authnResponse = $binding->decodeResponse($_POST); $authnResponse = $binding->decodeResponse($_POST);
$authnResponse->validate();
$session = $authnResponse->createSession();
if (isset($session)) {
$attributes = $session->getAttributes();
$logger->log(LOG_NOTICE, $session->getTrackID(), 'SAML2.0', 'SP.AssertionConsumerService', 'AuthnResponse', '-', $authnResponse->process();
'Successfully created local session from Authentication Response');
$logger->log(LOG_NOTICE, $session->getTrackID(), 'SAML2.0', 'SP.AssertionConsumerService', 'AuthnResponse', '-',
'Successfully created local session from Authentication Response');
$relayState = $authnResponse->getRelayState(); $relayState = $authnResponse->getRelayState();
if (isset($relayState)) { if (isset($relayState)) {
SimpleSAML_Utilities::redirect($relayState); SimpleSAML_Utilities::redirect($relayState);
} else {
throw new Exception('Could not find RelayState parameter, you are stucked here.');
}
} else { } else {
throw new Exception('Unkown error. Could not get session.'); throw new Exception('Could not find RelayState parameter, you are stuck here.');
} }
} catch(Exception $exception) { } catch(Exception $exception) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment