diff --git a/lib/SAML2/Assertion.php b/lib/SAML2/Assertion.php new file mode 100644 index 0000000000000000000000000000000000000000..0fc0dbc91413354641eb94bb3b6c5ce69d296391 --- /dev/null +++ b/lib/SAML2/Assertion.php @@ -0,0 +1,1075 @@ +<?php + +/** + * Class representing a SAML 2 assertion. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_Assertion implements SAML2_SignedElement { + + /** + * The identifier of this assertion. + * + * @var string + */ + private $id; + + + /** + * The issue timestamp of this assertion, as an UNIX timestamp. + * + * @var int + */ + private $issueInstant; + + + /** + * The entity id of the issuer of this assertion. + * + * @var string + */ + private $issuer; + + + /** + * The NameId of the subject in the assertion. + * + * If the NameId is NULL, no subject was included in the assertion. + * + * @var array|NULL + */ + private $nameId; + + + /** + * The earliest time this assertion is valid, as an UNIX timestamp. + * + * @var int + */ + private $notBefore; + + + /** + * The time this assertion expires, as an UNIX timestamp. + * + * @var int + */ + private $notOnOrAfter; + + + /** + * The destination URL for this assertion. + * + * @var string|NULL + */ + private $destination; + + + /** + * The id of the request this assertion is sent as a response to. + * + * This should be NULL if this isn't a response to a request. + * + * @var string|NULL + */ + private $inResponseTo; + + + /** + * The set of audiences that are allowed to receive this assertion. + * + * This is an array of valid service providers. + * + * If no restrictions on the audience are present, this variable contains NULL. + * + * @var array|NULL + */ + private $validAudiences; + + + /** + * The session expiration timestamp. + * + * @var int|NULL + */ + private $sessionNotOnOrAfter; + + + /** + * The session index for this user on the IdP. + * + * Contains NULL if no session index is present. + * + * @var string|NULL + */ + private $sessionIndex; + + + /** + * The authentication context for this assertion. + * + * @var string|NULL + */ + private $authnContext; + + + /** + * The attributes, as an associative array. + * + * @var array + */ + private $attributes; + + + /** + * The NameFormat used on all attributes. + * + * If more than one NameFormat is used, this will contain + * the unspecified nameformat. + * + * @var string + */ + private $nameFormat; + + + /** + * The private key we should use to sign the assertion. + * + * The private key can be NULL, in which case the assertion is sent unsigned. + * + * @var XMLSecurityKey|NULL + */ + private $signatureKey; + + + /** + * List of certificates that should be included in the assertion. + * + * @var array + */ + private $certificates; + + + /** + * The data needed to verify the signature. + * + * @var array|NULL + */ + private $signatureData; + + + + /** + * Constructor for SAML 2 assertions. + * + * @param DOMElement|NULL $xml The input assertion. + */ + public function __construct(DOMElement $xml = NULL) { + + $this->id = SimpleSAML_Utilities::generateID(); + $this->issueInstant = time(); + $this->issuer = ''; + $this->attributes = array(); + $this->nameFormat = SAML2_Const::NAMEFORMAT_UNSPECIFIED; + $this->certificates = array(); + + if ($xml === NULL) { + return; + } + + if (!$xml->hasAttribute('ID')) { + throw new Exception('Missing ID attribute on SAML assertion.'); + } + $this->id = $xml->getAttribute('ID'); + + if ($xml->getAttribute('Version') !== '2.0') { + /* Currently a very strict check. */ + throw new Exception('Unsupported version: ' . $xml->getAttribute('Version')); + } + + $this->issueInstant = SimpleSAML_Utilities::parseSAML2Time($xml->getAttribute('IssueInstant')); + + $issuer = SAML2_Utils::xpQuery($xml, './saml:Issuer'); + if (empty($issuer)) { + throw new Exception('Missing <saml:Issuer> in assertion.'); + } + $this->issuer = $issuer[0]->textContent; + + $this->parseSubject($xml); + $this->parseConditions($xml); + $this->parseAuthnStatement($xml); + $this->parseAttributes($xml); + $this->parseSignature($xml); + } + + + /** + * Parse subject in assertion. + * + * @param DOMElement $xml The assertion XML element. + */ + private function parseSubject(DOMElement $xml) { + + $subject = SAML2_Utils::xpQuery($xml, './saml:Subject'); + if (empty($subject)) { + /* No Subject node. */ + return; + } elseif (count($subject) > 1) { + throw new Exception('More than one <saml:Subject> in <saml:Assertion>.'); + } + $subject = $subject[0]; + + $nameId = SAML2_Utils::xpQuery($subject, './saml:NameID'); + if (empty($nameId)) { + throw new Exception('Missing <saml:NameID> in <saml:Subject>.'); + } elseif (count($nameId) > 1) { + throw new Exception('More than one <saml:NameID> in <saml:Subject>.'); + } + $nameId = $nameId[0]; + $this->nameId = SAML2_Utils::parseNameId($nameId); + + $subjectConfirmation = SAML2_Utils::xpQuery($subject, './saml:SubjectConfirmation'); + if (empty($subjectConfirmation)) { + throw new Exception('Missing <saml:SubjectConfirmation> in <saml:Subject>.'); + } elseif (count($subjectConfirmation) > 1) { + throw new Exception('More than one <saml:SubjectConfirmation> in <saml:Subject>.'); + } + $subjectConfirmation = $subjectConfirmation[0]; + + if (!$subjectConfirmation->hasAttribute('Method')) { + throw new Exception('Missing required attribute "Method" on <saml:SubjectConfirmation>-node.'); + } + $method = $subjectConfirmation->getAttribute('Method'); + + if ($method !== SAML2_Const::CM_BEARER) { + throw new Exception('Unsupported subject confirmation method: ' . var_export($method, TRUE)); + } + + $confirmationData = SAML2_Utils::xpQuery($subjectConfirmation, './saml:SubjectConfirmationData'); + if (empty($confirmationData)) { + return; + } elseif (count($confirmationData) > 1) { + throw new Exception('More than one <saml:SubjectConfirmationData> in <saml:SubjectConfirmation> is currently unsupported.'); + } + $confirmationData = $confirmationData[0]; + + if ($confirmationData->hasAttribute('NotBefore')) { + $notBefore = SimpleSAML_Utilities::parseSAML2Time($confirmationData->getAttribute('NotBefore')); + if ($this->notBefore === NULL || $this->notBefore < $notBefore) { + $this->notBefore = $notBefore; + } + } + if ($confirmationData->hasAttribute('NotOnOrAfter')) { + $notOnOrAfter = SimpleSAML_Utilities::parseSAML2Time($confirmationData->getAttribute('NotOnOrAfter')); + if ($this->notOnOrAfter === NULL || $this->notOnOrAfter > $notOnOrAfter) { + $this->notOnOrAfter = $notOnOrAfter; + } + } + if ($confirmationData->hasAttribute('InResponseTo')) { + $this->inResponseTo = $confirmationData->getAttribute('InResponseTo');; + } + if ($confirmationData->hasAttribute('Recipient')) { + $this->destination = $confirmationData->getAttribute('Recipient');; + } + } + + + /** + * Parse conditions in assertion. + * + * @param DOMElement $xml The assertion XML element. + */ + private function parseConditions(DOMElement $xml) { + + $conditions = SAML2_Utils::xpQuery($xml, './saml:Conditions'); + if (empty($conditions)) { + /* No <saml:Conditions> node. */ + return; + } elseif (count($conditions) > 1) { + throw new Exception('More than one <saml:Conditions> in <saml:Assertion>.'); + } + $conditions = $conditions[0]; + + if ($conditions->hasAttribute('NotBefore')) { + $notBefore = SimpleSAML_Utilities::parseSAML2Time($conditions->getAttribute('NotBefore')); + if ($this->notBefore === NULL || $this->notBefore < $notBefore) { + $this->notBefore = $notBefore; + } + } + if ($conditions->hasAttribute('NotOnOrAfter')) { + $notOnOrAfter = SimpleSAML_Utilities::parseSAML2Time($conditions->getAttribute('NotOnOrAfter')); + if ($this->notOnOrAfter === NULL || $this->notOnOrAfter > $notOnOrAfter) { + $this->notOnOrAfter = $notOnOrAfter; + } + } + + + for ($node = $conditions->firstChild; $node !== NULL; $node = $node->nextSibling) { + if ($node instanceof DOMText) { + continue; + } + if ($node->namespaceURI !== SAML2_Const::NS_SAML) { + throw new Exception('Unknown namespace of condition: ' . var_export($node->namespaceURI, TRUE)); + } + switch ($node->localName) { + case 'AudienceRestriction': + $audiences = SAML2_Utils::xpQuery($node, './saml:Audience'); + foreach ($audiences as &$audience) { + $audience = $audience->textContent; + } + if ($this->validAudiences === NULL) { + /* The first (and probably last) AudienceRestriction element. */ + $this->validAudiences = $audiences; + + } else { + /* + * The set of AudienceRestriction are ANDed together, so we need + * the subset that are present in all of them. + */ + $this->validAudiences = array_intersect($this->validAudiences, $audiences); + } + break; + case 'OneTimeUse': + /* Currently ignored. */ + break; + case 'ProxyRestriction': + /* Currently ignored. */ + break; + default: + throw new Exception('Unknown condition: ' . var_export($node->localName, TRUE)); + } + } + + } + + + /** + * Parse AuthnStatement in assertion. + * + * @param DOMElement $xml The assertion XML element. + */ + private function parseAuthnStatement(DOMElement $xml) { + + $as = SAML2_Utils::xpQuery($xml, './saml:AuthnStatement'); + if (empty($as)) { + return; + } elseif (count($as) > 1) { + throw new Exception('More that one <saml:AuthnStatement> in <saml:Assertion> not supported.'); + } + $as = $as[0]; + $this->authnStatement = array(); + + if (!$as->hasAttribute('AuthnInstant')) { + throw new Exception('Missing required AuthnInstant attribute on <saml:AuthnStatement>.'); + } + + if ($as->hasAttribute('SessionNotOnOrAfter')) { + $this->sessionNotOnOrAfter = SimpleSAML_Utilities::parseSAML2Time($as->getAttribute('SessionNotOnOrAfter')); + } + + if ($as->hasAttribute('SessionIndex')) { + $this->sessionIndex = $as->getAttribute('SessionIndex'); + } + + $ac = SAML2_Utils::xpQuery($as, './saml:AuthnContext'); + if (empty($ac)) { + throw new Exception('Missing required <saml:AuthnContext> in <saml:AuthnStatement>.'); + } elseif (count($ac) > 1) { + throw new Exception('More than one <saml:AuthnContext> in <saml:AuthnStatement>.'); + } + $ac = $ac[0]; + + $accr = SAML2_Utils::xpQuery($ac, './saml:AuthnContextClassRef'); + if (empty($accr)) { + throw new Exception('Missing almost-required <saml:AuthnContextClassRef> in <saml:AuthnContext>.'); + } elseif (count($accr) > 1) { + throw new Exception('More than one <saml:AuthnContextClassRef> in <saml:AuthnContext>.'); + } + $accr = $accr[0]; + + $this->authnContext = $accr->textContent; + } + + + /** + * Parse attribute statements in assertion. + * + * @param DOMElement $xml The XML element with the assertion. + */ + private function parseAttributes(DOMElement $xml) { + + $firstAttribute = TRUE; + $attributes = SAML2_Utils::xpQuery($xml, './saml:AttributeStatement/saml:Attribute'); + foreach ($attributes as $attribute) { + if (!$attribute->hasAttribute('Name')) { + throw new Exception('Missing name on <saml:Attribute> element.'); + } + $name = $attribute->getAttribute('Name'); + + if ($attribute->hasAttribute('NameFormat')) { + $nameFormat = $attribute->getAttribute('NameFormat'); + } else { + $nameFormat = SAML2_Const::NAMEFORMAT_UNSPECIFIED; + } + + if ($firstAttribute) { + $this->nameFormat = $nameFormat; + $firstAttribute = FALSE; + } else { + if ($this->nameFormat !== $nameFormat) { + $this->nameFormat = SAML2_Const::NAMEFORMAT_UNSPECIFIED; + } + } + + if (!array_key_exists($name, $this->attributes)) { + $this->attributes[$name] = array(); + } + + $values = SAML2_Utils::xpQuery($attribute, './saml:AttributeValue'); + foreach ($values as $value) { + $this->attributes[$name][] = $value->textContent; + } + } + } + + + /** + * Parse signature on assertion. + * + * @param DOMElement $xml The assertion XML element. + */ + private function parseSignature(DOMElement $xml) { + + /* Validate the signature element of the message. */ + $sig = SAML2_Utils::validateElement($xml); + if ($sig !== FALSE) { + $this->certificates = $sig['Certificates']; + $this->signatureData = $sig; + } + } + + + /** + * Validate this assertion against a public key. + * + * If no signature was present on the assertion, we will return FALSE. + * Otherwise, TRUE will be returned. An exception is thrown if the + * signature validation fails. + * + * @param XMLSecurityKey $key The key we should check against. + * @return boolean TRUE if successful, FALSE if it is unsigned. + */ + public function validate(XMLSecurityKey $key) { + assert('$key->type === XMLSecurityKey::RSA_SHA1'); + + if ($this->signatureData === NULL) { + return FALSE; + } + + SAML2_Utils::validateSignature($this->signatureData, $key); + + return TRUE; + } + + + /** + * Retrieve the identifier of this assertion. + * + * @return string The identifier of this assertion. + */ + public function getId() { + return $this->id; + } + + + /** + * Set the identifier of this assertion. + * + * @param string $id The new identifier of this assertion. + */ + public function setId($id) { + assert('is_string($id)'); + + $this->id = $id; + } + + + /** + * Retrieve the issue timestamp of this assertion. + * + * @return int The issue timestamp of this assertion, as an UNIX timestamp. + */ + public function getIssueInstant() { + return $this->issueInstant; + } + + + /** + * Set the issue timestamp of this assertion. + * + * @param int $issueInstant The new issue timestamp of this assertion, as an UNIX timestamp. + */ + public function setIssueInstant($issueInstant) { + assert('is_int($issueInstant)'); + + $this->issueInstant = $issueInstant; + } + + + /** + * Retrieve the issuer if this assertion. + * + * @return string The issuer of this assertion. + */ + public function getIssuer() { + return $this->issuer; + } + + + /** + * Set the issuer of this message. + * + * @param string $issuer The new issuer of this assertion. + */ + public function setIssuer($issuer) { + assert('is_string($issuer)'); + + $this->issuer = $issuer; + } + + + /** + * Retrieve the NameId of the subject in the assertion. + * + * The returned NameId is in the format used by SAML2_Utils::addNameId(). + * + * @see SAML2_Utils::addNameId() + * @return array|NULL The name identifier of the assertion. + */ + public function getNameId() { + return $this->nameId; + } + + + /** + * Set the NameId of the subject in the assertion. + * + * The NameId must be in the format accepted by SAML2_Utils::addNameId(). + * + * @see SAML2_Utils::addNameId() + * @param array|NULL $nameId The name identifier of the assertion. + */ + public function setNameId($nameId) { + assert('is_array($nameId) || is_null($nameId)'); + + $this->nameId = $nameId; + } + + + /** + * Retrieve the earliest timestamp this assertion is valid. + * + * This function returns NULL if there are no restrictions on how early the + * assertion can be used. + * + * @return int|NULL The earliest timestamp this assertion is valid. + */ + public function getNotBefore() { + + return $this->notBefore; + } + + + /** + * Set the earliest timestamp this assertion can be used. + * + * Set this to NULL if no limit is required. + * + * @param int|NULL $notBefore The earliest timestamp this assertion is valid. + */ + public function setNotBefore($notBefore) { + assert('is_int($notBefore) || is_null($notBefore)'); + + $this->notBefore = $notBefore; + } + + + /** + * Retrieve the expiration timestamp of this assertion. + * + * This function returns NULL if there are no restrictions on how + * late the assertion can be used. + * + * @return int|NULL The latest timestamp this assertion is valid. + */ + public function getNotOnOrAfter() { + + return $this->notOnOrAfter; + } + + + /** + * Set the expiration timestamp of this assertion. + * + * Set this to NULL if no limit is required. + * + * @param int|NULL $notOnOrAfter The latest timestamp this assertion is valid. + */ + public function setNotOnOrAfter($notOnOrAfter) { + assert('is_int($notOnOrAfter) || is_null($notOnOrAfter)'); + + $this->notOnOrAfter = $notOnOrAfter; + } + + + /** + * Retrieve the destination URL of this assertion. + * + * This function returns NULL if there are no restrictions on which URL can + * receive the assertion. + * + * @return string|NULL The destination URL of this assertion. + */ + public function getDestination() { + + return $this->destination; + } + + + /** + * Set the destination URL of this assertion. + * + * @return string|NULL The destination URL of this assertion. + */ + public function setDestination($destination) { + assert('is_string($destination) || is_null($destination)'); + + $this->destination = $destination; + } + + + /** + * Retrieve the request this assertion is sent in response to. + * + * Can be NULL, in which case this assertion isn't sent in response to a specific request. + * + * @return string|NULL The id of the request this assertion is sent in response to. + */ + public function getInResponseTo() { + + return $this->inResponseTo; + } + + + /** + * Set the request this assertion is sent in response to. + * + * Can be set to NULL, in which case this assertion isn't sent in response to a specific request. + * + * @param string|NULL $inResponseTo The id of the request this assertion is sent in response to. + */ + public function setInResponseTo($inResponseTo) { + assert('is_string($inResponseTo) || is_null($inResponseTo)'); + + $this->inResponseTo = $inResponseTo; + } + + + /** + * Retrieve the audiences that are allowed to receive this assertion. + * + * This may be NULL, in which case all audiences are allowed. + * + * @return array|NULL The allowed audiences. + */ + public function getValidAudiences() { + + return $this->validAudiences; + } + + + /** + * Set the audiences that are allowed to receive this assertion. + * + * This may be NULL, in which case all audiences are allowed. + * + * @param array|NULL $validAudiences The allowed audiences. + */ + public function setValidAudiences(array $validAudiences = NULL) { + + $this->validAudiences = $validAudiences; + } + + + /** + * Retrieve the session expiration timestamp. + * + * This function returns NULL if there are no restrictions on the + * session lifetime. + * + * @return int|NULL The latest timestamp this session is valid. + */ + public function getSessionNotOnOrAfter() { + + return $this->sessionNotOnOrAfter; + } + + + /** + * Set the session expiration timestamp. + * + * Set this to NULL if no limit is required. + * + * @param int|NULL $sessionLifetime The latest timestamp this session is valid. + */ + public function setSessionNotOnOrAfter($sessionNotOnOrAfter) { + assert('is_int($sessionNotOnOrAfter) || is_null($sessionNotOnOrAfter)'); + + $this->sessionNotOnOrAfter = $sessionNotOnOrAfter; + } + + + /** + * Retrieve the session index of the user at the IdP. + * + * @return string|NULL The session index of the user at the IdP. + */ + public function getSessionIndex() { + + return $this->sessionIndex; + } + + + /** + * Set the session index of the user at the IdP. + * + * Note that the authentication context must be set before the + * session index can be inluded in the assertion. + * + * @param string|NULL $sessionIndex The session index of the user at the IdP. + */ + public function setSessionIndex($sessionIndex) { + assert('is_string($sessionIndex) || is_null($sessionIndex)'); + + $this->sessionIndex = $sessionIndex; + } + + + /** + * Retrieve the authentication method used to authenticate the user. + * + * This will return NULL if no authentication statement was + * included in the assertion. + * + * @return string|NULL The authentication method. + */ + public function getAuthnContext() { + + return $this->authnContext; + } + + + /** + * Set the authentication method used to authenticate the user. + * + * If this is set to NULL, no authentication statement will be + * included in the assertion. The default is NULL. + * + * @param string|NULL $authnContext The authentication method. + */ + public function setAuthnContext($authnContext) { + assert('is_string($authnContext) || is_null($authnContext)'); + + $this->authnContext = $authnContext; + } + + + /** + * Retrieve all attributes. + * + * @return array All attributes, as an associative array. + */ + public function getAttributes() { + + return $this->attributes; + } + + + /** + * Replace all attributes. + * + * @param array $attributes All new attributes, as an associative array. + */ + public function setAttributes(array $attributes) { + + $this->attributes = $attributes; + } + + + /** + * Retrieve the NameFormat used on all attributes. + * + * If more than one NameFormat is used in the received attributes, this + * returns the unspecified NameFormat. + * + * @return string The NameFormat used on all attributes. + */ + public function getAttributeNameFormat() { + return $this->nameFormat; + } + + + /** + * Set the NameFormat used on all attributes. + * + * @param string $nameFormat The NameFormat used on all attributes. + */ + public function setAttributeNameFormat($nameFormat) { + assert('is_string($nameFormat)'); + + $this->nameFormat = $nameFormat; + } + + + /** + * Retrieve the private key we should use to sign the assertion. + * + * @return XMLSecurityKey|NULL The key, or NULL if no key is specified. + */ + public function getSignatureKey() { + return $this->signatureKey; + } + + + /** + * Set the private key we should use to sign the assertion. + * + * If the key is NULL, the assertion will be sent unsigned. + * + * @param XMLSecurityKey|NULL $key + */ + public function setSignatureKey(XMLsecurityKey $signatureKey = NULL) { + $this->signatureKey = $signatureKey; + } + + + /** + * Set the certificates that should be included in the assertion. + * + * The certificates should be strings with the PEM encoded data. + * + * @param array $certificates An array of certificates. + */ + public function setCertificates(array $certificates) { + $this->certificates = $certificates; + } + + + /** + * Retrieve the certificates that are included in the assertion. + * + * @return array An array of certificates. + */ + public function getCertificates() { + return $this->certificates; + } + + + /** + * Convert this assertion to an XML element. + * + * @return DOMElement This assertion. + */ + public function toXML() { + + $document = new DOMDocument(); + + $root = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:' . 'Assertion'); + $document->appendChild($root); + + /* Ugly hack to add another namespace declaration to the root element. */ + $root->setAttributeNS(SAML2_Const::NS_SAMLP, 'samlp:tmp', 'tmp'); + $root->removeAttributeNS(SAML2_Const::NS_SAMLP, 'tmp'); + $root->setAttributeNS(SAML2_Const::NS_XSI, 'xsi:tmp', 'tmp'); + $root->removeAttributeNS(SAML2_Const::NS_XSI, 'tmp'); + $root->setAttributeNS(SAML2_Const::NS_XS, 'xs:tmp', 'tmp'); + $root->removeAttributeNS(SAML2_Const::NS_XS, 'tmp'); + + $root->setAttribute('ID', $this->id); + $root->setAttribute('Version', '2.0'); + $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant)); + + $issuer = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:Issuer'); + $issuer->appendChild($document->createTextNode($this->issuer)); + $root->appendChild($issuer); + + $this->addSubject($root); + $this->addConditions($root); + $this->addAuthnStatement($root); + $this->addAttributeStatement($root); + + if ($this->signatureKey !== NULL) { + SAML2_Utils::insertSignature($this->signatureKey, $this->certificates, $root, $issuer->nextSibling); + } + + return $root; + } + + + /** + * Add a Subject-node to the assertion. + * + * @param DOMElement $root The assertion element we should add the subject to. + */ + private function addSubject(DOMElement $root) { + + if ($this->nameId === NULL) { + /* We don't have anything to create a Subject node for. */ + return; + } + + $subject = $root->ownerDocument->createElementNS(SAML2_Const::NS_SAML, 'saml:Subject'); + $root->appendChild($subject); + + SAML2_Utils::addNameId($subject, $this->nameId); + + $sc = $root->ownerDocument->createElementNS(SAML2_Const::NS_SAML, 'saml:SubjectConfirmation'); + $subject->appendChild($sc); + + $sc->setAttribute('Method', SAML2_Const::CM_BEARER); + + $scd = $root->ownerDocument->createElementNS(SAML2_Const::NS_SAML, 'saml:SubjectConfirmationData'); + $sc->appendChild($scd); + + if ($this->notBefore !== NULL) { + $scd->setAttribute('NotBefore', gmdate('Y-m-d\TH:i:s\Z', $this->notBefore)); + } + if ($this->notOnOrAfter !== NULL) { + $scd->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter)); + } + if ($this->destination !== NULL) { + $scd->setAttribute('Recipient', $this->destination); + } + if ($this->inResponseTo !== NULL) { + $scd->setAttribute('InResponseTo', $this->inResponseTo); + } + } + + + /** + * Add a Conditions-node to the assertion. + * + * @param DOMElement $root The assertion element we should add the conditions to. + */ + private function addConditions(DOMElement $root) { + + $document = $root->ownerDocument; + + $conditions = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:Conditions'); + $root->appendChild($conditions); + + if ($this->notBefore !== NULL) { + $conditions->setAttribute('NotBefore', gmdate('Y-m-d\TH:i:s\Z', $this->notBefore)); + } + if ($this->notOnOrAfter !== NULL) { + $conditions->setAttribute('NotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->notOnOrAfter)); + } + + if ($this->validAudiences !== NULL) { + $ar = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AudienceRestriction'); + $conditions->appendChild($ar); + + foreach ($this->validAudiences as $audience) { + $a = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:Audience'); + $ar->appendChild($a); + + $a->appendChild($document->createTextNode($audience)); + } + } + } + + + /** + * Add a AuthnStatement-node to the assertion. + * + * @param DOMElement $root The assertion element we should add the authentication statement to. + */ + private function addAuthnStatement(DOMElement $root) { + + if ($this->authnContext === NULL) { + /* No authentication context => no authentication statement. */ + return; + } + + $document = $root->ownerDocument; + + $as = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AuthnStatement'); + $root->appendChild($as); + + $as->setAttribute('AuthnInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant)); + + if ($this->sessionNotOnOrAfter !== NULL) { + $as->setAttribute('SessionNotOnOrAfter', gmdate('Y-m-d\TH:i:s\Z', $this->sessionNotOnOrAfter)); + } + if ($this->sessionIndex !== NULL) { + $as->setAttribute('SessionIndex', $this->sessionIndex); + } + + $ac = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AuthnContext'); + $as->appendChild($ac); + + $accr = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AuthnContextClassRef'); + $ac->appendChild($accr); + + $accr->appendChild($document->createTextNode($this->authnContext)); + } + + + /** + * Add an AttributeStatement-node to the assertion. + * + * @param DOMElement $root The assertion element we should add the subject to. + */ + private function addAttributeStatement(DOMElement $root) { + + if (empty($this->attributes)) { + return; + } + + $document = $root->ownerDocument; + + $attributeStatement = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AttributeStatement'); + $root->appendChild($attributeStatement); + + foreach ($this->attributes as $name => $values) { + $attribute = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:Attribute'); + $attributeStatement->appendChild($attribute); + $attribute->setAttribute('Name', $name); + + if ($this->nameFormat !== SAML2_Const::NAMEFORMAT_UNSPECIFIED) { + $attribute->setAttribute('NameFormat', $this->nameFormat); + } + + foreach ($values as $value) { + if (is_string($value)) { + $type = 'xs:string'; + } elseif (is_int($value)) { + $type = 'xs:integer'; + } else { + $type = NULL; + } + + $attributeValue = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:AttributeValue'); + $attribute->appendChild($attributeValue); + if ($type !== NULL) { + $attributeValue->setAttributeNS(SAML2_Const::NS_XSI, 'xsi:type', $type); + } + $attributeValue->appendChild($document->createTextNode($value)); + } + } + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/AuthnRequest.php b/lib/SAML2/AuthnRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..ca7bf9d9d697af7da666cd5e982bc1ec68b0135c --- /dev/null +++ b/lib/SAML2/AuthnRequest.php @@ -0,0 +1,253 @@ +<?php + +/** + * Class for SAML 2 authentication request messages. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_AuthnRequest extends SAML2_Request { + + /** + * The options for what type of name identifier should be returned. + * + * @var array + */ + private $nameIdPolicy; + + /** + * Whether the Identity Provider must authenticate the user again. + * + * @var bool + */ + private $forceAuthn; + + + /** + * Set to TRUE if this request is passive. + * + * @var bool. + */ + private $isPassive; + + + /** + * The URL of the asertion consumer service where the response should be delivered. + * + * @var string|NULL + */ + private $assertionConsumerServiceURL; + + + /** + * What binding should be used when sending the response. + * + * @var string|NULL + */ + private $protocolBinding; + + + /** + * Constructor for SAML 2 authentication request messages. + * + * @param DOMElement|NULL $xml The input message. + */ + public function __construct(DOMElement $xml = NULL) { + parent::__construct('AuthnRequest', $xml); + + $this->nameIdPolicy = array(); + $this->forceAuthn = FALSE; + $this->isPassive = FALSE; + + if ($xml === NULL) { + return; + } + + $this->forceAuthn = SAML2_Utils::parseBoolean($xml, 'ForceAuthn', FALSE); + $this->isPassive = SAML2_Utils::parseBoolean($xml, 'IsPassive', FALSE); + + if ($xml->hasAttribute('AssertionConsumerServiceURL')) { + $this->assertionConsumerServiceURL = $xml->getAttribute('AssertionConsumerServiceURL'); + } + + if ($xml->hasAttribute('ProtocolBinding')) { + $this->protocolBinding = $xml->getAttribute('ProtocolBinding'); + } + + $nameIdPolicy = SAML2_Utils::xpQuery($xml, './samlp:NameIDPolicy'); + if (!empty($nameIdPolicy)) { + $nameIdPolicy = $nameIdPolicy[0]; + if ($nameIdPolicy->hasAttribute('Format')) { + $this->nameIdPolicy['Format'] = $nameIdPolicy->getAttribute('Format'); + } + if ($nameIdPolicy->hasAttribute('SPNameQualifier')) { + $this->nameIdPolicy['SPNameQualifier'] = $nameIdPolicy->getAttribute('SPNameQualifier'); + } + if ($nameIdPolicy->hasAttribute('AllowCreate')) { + $this->nameIdPolicy['AllowCreate'] = SAML2_Utils::parseBoolean($nameIdPolicy, 'AllowCreate', FALSE); + } + } + } + + + /** + * Retrieve the NameIdPolicy. + * + * @see SAML2_AuthnRequest::setNameIdPolicy() + * @return array The NameIdPolicy. + */ + public function getNameIdPolicy() { + return $this->nameIdPolicy; + } + + + /** + * Set the NameIDPolicy. + * + * This function accepts an array with the following options: + * - 'Format' + * - 'SPNameQualifier' + * - 'AllowCreate' + * + * @param array $nameIdPolicy The NameIDPolicy. + */ + public function setNameIdPolicy(array $nameIdPolicy) { + + $this->nameIdPolicy = $nameIdPolicy; + } + + + /** + * Retrieve the value of the ForceAuthn attribute. + * + * @return bool The ForceAuthn attribute. + */ + public function getForceAuthn() { + return $this->forceAuthn; + } + + + /** + * Set the value of the ForceAuthn attribute. + * + * @param bool $forceAuthn The ForceAuthn attribute. + */ + public function setForceAuthn($forceAuthn) { + assert('is_bool($forceAuthn)'); + + $this->forceAuthn = $forceAuthn; + } + + + /** + * Retrieve the value of the IsPassive attribute. + * + * @return bool The IsPassive attribute. + */ + public function getIsPassive() { + return $this->isPassive; + } + + + /** + * Set the value of the IsPassive attribute. + * + * @param bool $isPassive The IsPassive attribute. + */ + public function setIsPassive($isPassive) { + assert('is_bool($isPassive)'); + + $this->isPassive = $isPassive; + } + + + /** + * Retrieve the value of the AssertionConsumerServiceURL attribute. + * + * @return string|NULL The AssertionConsumerServiceURL attribute. + */ + public function getAssertionConsumerServiceURL() { + return $this->assertionConsumerServiceURL; + } + + + /** + * Set the value of the AssertionConsumerServiceURL attribute. + * + * @param string|NULL $assertionConsumerServiceURL The AssertionConsumerServiceURL attribute. + */ + public function setAssertionConsumerServiceURL($assertionConsumerServiceURL) { + assert('is_string($assertionConsumerServiceURL) || is_null($assertionConsumerServiceURL)'); + + $this->assertionConsumerServiceURL = $assertionConsumerServiceURL; + } + + + /** + * Retrieve the value of the ProtocolBinding attribute. + * + * @return string|NULL The ProtocolBinding attribute. + */ + public function getProtocolBinding() { + return $this->protocolBinding; + } + + + /** + * Set the value of the ProtocolBinding attribute. + * + * @param string $protocolBinding The ProtocolBinding attribute. + */ + public function setProtocolBinding($protocolBinding) { + assert('is_string($protocolBinding) || is_null($protocolBinding)'); + + $this->protocolBinding = $protocolBinding; + } + + + /** + * Convert this authentication request to an XML element. + * + * @return DOMElement This authentication request. + */ + public function toUnsignedXML() { + + $root = parent::toUnsignedXML(); + + if ($this->forceAuthn) { + $root->setAttribute('ForceAuthn', 'true'); + } + + if ($this->isPassive) { + $root->setAttribute('IsPassive', 'true'); + } + + if ($this->assertionConsumerServiceURL !== NULL) { + $root->setAttribute('AssertionConsumerServiceURL', $this->assertionConsumerServiceURL); + } + + if ($this->protocolBinding !== NULL) { + $root->setAttribute('ProtocolBinding', $this->protocolBinding); + } + + if (!empty($this->nameIdPolicy)) { + $nameIdPolicy = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'NameIDPolicy'); + if (array_key_exists('Format', $this->nameIdPolicy)) { + $nameIdPolicy->setAttribute('Format', $this->nameIdPolicy['Format']); + } + if (array_key_exists('SPNameQualifier', $this->nameIdPolicy)) { + $nameIdPolicy->setAttribute('SPNameQualifier', $this->nameIdPolicy['SPNameQualifier']); + } + if (array_key_exists('AllowCreate', $this->nameIdPolicy) && $this->nameIdPolicy['AllowCreate']) { + $nameIdPolicy->setAttribute('AllowCreate', 'true'); + } + $root->appendChild($nameIdPolicy); + } + + return $root; + } + +} + + +?> \ No newline at end of file diff --git a/lib/SAML2/Binding.php b/lib/SAML2/Binding.php new file mode 100644 index 0000000000000000000000000000000000000000..957ec4d732675b7045c2fa7c2bc1c60a2dfcde15 --- /dev/null +++ b/lib/SAML2/Binding.php @@ -0,0 +1,111 @@ +<?php + +/** + * Base class for SAML 2 bindings. + * + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SAML2_Binding { + + /** + * The destination of messages. + * + * This can be NULL, in which case the destination in the message is used. + */ + protected $destination; + + + /** + * Retrieve a binding with the given URN. + * + * Will throw an exception if it is unable to locate the binding. + * + * @param string $urn The URN of the binding. + * @return SAML2_Binding The binding. + */ + public static function getBinding($urn) { + assert('is_string($urn)'); + + switch ($urn) { + case SAML2_Const::BINDING_HTTP_POST: + return new SAML2_HTTPPost(); + case SAML2_Const::BINDING_HTTP_REDIRECT: + return new SAML2_HTTPRedirect(); + default: + throw new Exception('Unsupported binding: ' . var_export($urn, TRUE)); + } + } + + + /** + * Guess the current binding. + * + * This function guesses the current binding and creates an instance + * of SAML2_Binding matching that binding. + * + * An exception will be thrown if it is unable to guess the binding. + * + * @return SAML2_Binding The binding. + */ + public static function getCurrentBinding() { + switch ($_SERVER['REQUEST_METHOD']) { + case 'GET': + return new SAML2_HTTPRedirect(); + case 'POST': + return new SAML2_HTTPPost(); + default: + throw new Exception('Unable to find the current binding.'); + } + } + + + /** + * Retrieve the destination of a message. + * + * @return string|NULL $destination The destination the message will be delivered to. + */ + public function getDestination() { + + return $this->destination; + } + + + /** + * Override the destination of a message. + * + * Set to NULL to use the destination set in the message. + * + * @param string|NULL $destination The destination the message should be delivered to. + */ + public function setDestination($destination) { + assert('is_string($destination) || is_null($destination)'); + + $this->destination = $destination; + } + + + /** + * Send a SAML 2 message. + * + * This function will send a message using the specified binding. + * The message will be delivered to the destination set in the message. + * + * @param SAML2_Message $message The message which should be sent. + */ + abstract public function send(SAML2_Message $message); + + + /** + * Receive a SAML 2 message. + * + * This function will extract the message from the current request. + * An exception will be thrown if we are unable to process the message. + * + * @return SAML2_Message The received message. + */ + abstract public function receive(); + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/Const.php b/lib/SAML2/Const.php index cdb6aac124f4684eba3caa852490c3776990e90e..c88d61d8a37a5ccb7daf2b6948e50f52c0c6f0e9 100644 --- a/lib/SAML2/Const.php +++ b/lib/SAML2/Const.php @@ -8,6 +8,17 @@ */ class SAML2_Const { + /** + * Password authentication context. + */ + const AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'; + + /** + * Unspecified authentication context. + */ + const AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'; + + /** * The URN for the HTTP-POST binding. */ diff --git a/lib/SAML2/EncryptedAssertion.php b/lib/SAML2/EncryptedAssertion.php new file mode 100644 index 0000000000000000000000000000000000000000..5c22dfe3764472b296f36c202b3c042b50d9ff3c --- /dev/null +++ b/lib/SAML2/EncryptedAssertion.php @@ -0,0 +1,160 @@ +<?php + +/** + * Class handling encrypted assertions. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_EncryptedAssertion { + + /** + * The current encrypted assertion. + * + * @var DOMElement + */ + private $encryptedData; + + + /** + * Constructor for SAML 2 encrypted assertions. + * + * @param DOMElement|NULL $xml The encrypted assertion XML element. + */ + public function __construct(DOMElement $xml = NULL) { + if ($xml === NULL) { + return; + } + + $data = SAML2_Utils::xpQuery($xml, './xenc:EncryptedData'); + if (count($data) === 0) { + throw new Exception('Missing encrypted data in <saml:EncryptedAssertion>.'); + } elseif (count($data) > 1) { + throw new Exception('More than one encrypted data element in <saml:EncryptedAssertion>.'); + } + $this->encryptedData = $data[0]; + } + + + /** + * Set the assertion. + * + * @param SAML2_Assertion $assertion The assertion. + * @param XMLSecurityKey $key The key we should use to encrypt the assertion. + */ + public function setAssertion(SAML2_Assertion $assertion, XMLSecurityKey $key) { + + $xml = $assertion->toXML(); + + $enc = new XMLSecEnc(); + $enc->setNode($xml); + $enc->type = XMLSecEnc::Element; + + switch ($key->type) { + case XMLSecurityKey::TRIPLEDES_CBC: + case XMLSecurityKey::AES128_CBC: + case XMLSecurityKey::AES192_CBC: + case XMLSecurityKey::AES256_CBC: + $symmetricKey = $key; + break; + + case XMLSecurityKey::RSA_1_5: + $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); + $symmetricKey->generateSessionKey(); + + $enc->encryptKey($key, $symmetricKey); + + break; + + default: + throw new Exception('Unknown key type for encryption: ' . $key->type); + } + + $this->encryptedData = $enc->encryptNode($symmetricKey); + } + + + /** + * Retrieve the assertion. + * + * @param XMLSecurityKey $key The key we should use to decrypt the assertion. + * @return SAML2_Assertion The decrypted assertion. + */ + public function getAssertion(XMLSecurityKey $inputKey) { + + $enc = new XMLSecEnc(); + + $enc->setNode($this->encryptedData); + $enc->type = $this->encryptedData->getAttribute("Type"); + + $symmetricKey = $enc->locateKey($this->encryptedData); + if (!$symmetricKey) { + throw new Exception('Could not locate key algorithm in encrypted data.'); + } + + $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey); + if (!$symmetricKeyInfo) { + throw new Exception('Could not locate <dsig:KeyInfo> for the encrypted key.'); + } + + if ($symmetricKeyInfo->isEncrypted) { + /* Make sure that the input key format is the same as the one used to encrypt the key. */ + if ($inputKey->getAlgorith() !== $symmetricKeyInfo->getAlgorith()) { + throw new Exception('Algorithm mismatch between input key and key used to encrypt ' . + ' the symmetric key for the message. Key was: ' . + var_export($inputKey->getAlgorith(), TRUE) . '; message was: ' . + var_export($symmetricKeyInfo->getAlgorith(), TRUE)); + } + + $encKey = $symmetricKeyInfo->encryptedCtx; + $symmetricKeyInfo->key = $inputKey->key; + $key = $encKey->decryptKey($symmetricKeyInfo); + $symmetricKey->loadkey($key); + } else { + /* Make sure that the input key has the correct format. */ + if ($inputKey->getAlgorith() !== $symmetricKey->getAlgorith()) { + throw new Exception('Algorithm mismatch between input key and key in message. ' . + 'Key was: ' . var_export($inputKey->getAlgorith(), TRUE) . '; message was: ' . + var_export($symmetricKey->getAlgorith(), TRUE)); + } + $symmetricKey = $inputKey; + } + + $decrypted = $enc->decryptNode($symmetricKey, FALSE); + + /* + * This is a workaround for the case where only a subset of the XML + * tree was serialized for encryption. In that case, we may miss the + * namespaces needed to parse the XML. + */ + $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$decrypted.'</root>'; + $newDoc = new DOMDocument(); + if (!$newDoc->loadXML($xml)) { + throw new Exception('Failed to parse decrypted XML. Maybe the wrong sharedkey was used?'); + } + $assertionXML = $newDoc->firstChild->firstChild; + if ($assertionXML === NULL) { + throw new Exception('Missing encrypted assertion within <saml:EncryptedAssertion>.'); + } + return new SAML2_Assertion($assertionXML); + } + + + /** + * Convert this encrypted assertion to an XML element. + * + * @return DOMElement This encrypted assertion. + */ + public function toXML() { + + $document = new DOMDocument(); + + $root = $document->createElementNS(SAML2_Const::NS_SAML, 'saml:' . 'EncryptedAssertion'); + $document->appendChild($root); + + $root->appendChild($document->importNode($this->encryptedData, TRUE)); + + return $root; + } + +} \ No newline at end of file diff --git a/lib/SAML2/HTTPPost.php b/lib/SAML2/HTTPPost.php new file mode 100644 index 0000000000000000000000000000000000000000..3a0a85c8b51a17da6f40e8bfa515241c316d9535 --- /dev/null +++ b/lib/SAML2/HTTPPost.php @@ -0,0 +1,104 @@ +<?php + +/** + * Class which implements the HTTP-POST binding. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_HTTPPost extends SAML2_Binding { + + /** + * Send a SAML 2 message using the HTTP-POST binding. + * + * Note: This function never returns. + * + * @param SAML2_Message $message The message we should send. + */ + public function send(SAML2_Message $message) { + + if ($this->destination === NULL) { + $destination = $message->getDestination(); + } else { + $destination = $this->destination; + } + $relayState = $message->getRelayState(); + + $msgStr = $message->toSignedXML(); + $msgStr = $msgStr->ownerDocument->saveXML($msgStr); + $msgStr = base64_encode($msgStr); + $msgStr = htmlspecialchars($msgStr); + + if ($message instanceof SAML2_Request) { + $msgType = 'SAMLRequest'; + } else { + $msgType = 'SAMLResponse'; + } + + $destination = htmlspecialchars($destination); + + if ($relayState !== NULL) { + $relayState = '<input type="hidden" name="RelayState" value="' . htmlspecialchars($relayState) . '">'; + } else { + $relayState = ''; + } + + $out = <<<END +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="content-type" content="text/html; charset=utf-8" /> +<title>POST data</title> +</head> +<body onload="document.forms[0].submit()"> +<noscript> +<p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p> +</noscript> +<form method="post" action="$destination"> +<input type="hidden" name="$msgType" value="$msgStr" /> +$relayState +<noscript><input type="submit" value="Submit" /></noscript> +</form> +</body> +</html> +END; + echo($out); + exit(0); + } + + + /** + * Receive a SAML 2 message sent using the HTTP-POST binding. + * + * Throws an exception if it is unable receive the message. + * + * @return SAML2_Message The received message. + */ + public function receive() { + + if (array_key_exists('SAMLRequest', $_POST)) { + $msg = $_POST['SAMLRequest']; + } elseif (array_key_exists('SAMLResponse', $_POST)) { + $msg = $_POST['SAMLResponse']; + } else { + throw new Exception('Missing SAMLRequest or SAMLResponse parameter.'); + } + + $msg = base64_decode($msg); + + $document = new DOMDocument(); + $document->loadXML($msg); + $xml = $document->firstChild; + + $msg = SAML2_Message::fromXML($xml); + + if (array_key_exists('RelayState', $_POST)) { + $msg->setRelayState($_POST['RelayState']); + } + + return $msg; + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/HTTPRedirect.php b/lib/SAML2/HTTPRedirect.php new file mode 100644 index 0000000000000000000000000000000000000000..71a32b42a1dc58382c694ceff955fe2fbc40cf77 --- /dev/null +++ b/lib/SAML2/HTTPRedirect.php @@ -0,0 +1,229 @@ +<?php + +/** + * Class which implements the HTTP-Redirect binding. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_HTTPRedirect extends SAML2_Binding { + + const DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE'; + + /** + * Create the redirect URL for a message. + * + * @param SAML2_Message $message The message. + * @return string The URL the user should be redirected to in order to send a message. + */ + public function getRedirectURL(SAML2_Message $message) { + + if ($this->destination === NULL) { + $destination = $message->getDestination(); + } else { + $destination = $this->destination; + } + + $relayState = $message->getRelayState(); + + $key = $message->getSignatureKey(); + + $msgStr = $message->toUnsignedXML(); + $msgStr = $msgStr->ownerDocument->saveXML($msgStr); + $msgStr = gzdeflate($msgStr); + $msgStr = base64_encode($msgStr); + + /* Build the query string. */ + + if ($message instanceof SAML2_Request) { + $msg = 'SAMLRequest='; + } else { + $msg = 'SAMLResponse='; + } + $msg .= urlencode($msgStr); + + if ($relayState !== NULL) { + $msg .= '&RelayState=' . urlencode($relayState); + } + + if ($key !== NULL) { + /* Add the signature. */ + $msg .= '&SigAlg=' . urlencode(XMLSecurityKey::RSA_SHA1); + + $signature = $key->signData($msg); + $msg .= '&Signature=' . urlencode(base64_encode($signature)); + } + + if (strpos($destination, '?') === FALSE) { + $destination .= '?' . $msg; + } else { + $destination .= '&' . $msg; + } + + return $destination; + } + + + /** + * Send a SAML 2 message using the HTTP-Redirect binding. + * + * Note: This function never returns. + * + * @param SAML2_Message $message The message we should send. + */ + public function send(SAML2_Message $message) { + + $destination = $this->getRedirectURL($message); + SimpleSAML_Utilities::redirect($destination); + } + + + /** + * Receive a SAML 2 message sent using the HTTP-Redirect binding. + * + * Throws an exception if it is unable receive the message. + * + * @return SAML2_Message The received message. + */ + public function receive() { + + $data = self::parseQuery(); + + if (array_key_exists('SAMLRequest', $data)) { + $msg = $data['SAMLRequest']; + } elseif (array_key_exists('SAMLResponse', $data)) { + $msg = $data['SAMLResponse']; + } else { + throw new Execption('Missing SAMLRequest or SAMLResponse parameter.'); + } + + if (array_key_exists('SAMLEncoding', $data)) { + $encoding = $data['SAMLEncoding']; + } else { + $encoding = self::DEFLATE; + } + + $msg = base64_decode($msg); + switch ($encoding) { + case self::DEFLATE: + $msg = gzinflate($msg); + break; + default: + throw new Exception('Unknown SAMLEncoding: ' . var_export($encoding, TRUE)); + } + + $document = new DOMDocument(); + $document->loadXML($msg); + $xml = $document->firstChild; + + $msg = SAML2_Message::fromXML($xml); + + if (array_key_exists('Signature', $data)) { + /* Save the signature validation data until we need it. */ + $signatureValidationData = array( + 'Signature' => $data['Signature'], + 'Query' => $data['SignedQuery'], + ); + } + + + if (array_key_exists('RelayState', $data)) { + $msg->setRelayState($data['RelayState']); + } + + if (array_key_exists('Signature', $data)) { + if (!array_key_exists('SigAlg', $data)) { + throw new Exception('Missing signature algorithm.'); + } + + $signData = array( + 'Signature' => $data['Signature'], + 'SigAlg' => $data['SigAlg'], + 'Query' => $data['SignedQuery'], + ); + $msg->addValidator(array(get_class($this), 'validateSignature'), $signData); + } + + return $msg; + } + + + /** + * Helper function to parse query data. + * + * This function returns the query string split into key=>value pairs. + * It also adds a new parameter, SignedQuery, which contains the data that is + * signed. + * + * @return string The query data that is signed. + */ + private static function parseQuery() { + /* + * Parse the query string. We need to do this ourself, so that we get access + * to the raw (urlencoded) values. This is required because different software + * can urlencode to different values. + */ + $data = array(); + $relayState = ''; + $sigAlg = ''; + foreach (explode('&', $_SERVER['QUERY_STRING']) as $e) { + list($name, $value) = explode('=', $e, 2); + $name = urldecode($name); + $data[$name] = urldecode($value); + + switch ($name) { + case 'SAMLRequest': + case 'SAMLResponse': + $sigQuery = $name . '=' . $value; + break; + case 'RelayState': + $relayState = '&RelayState=' . $value; + break; + case 'SigAlg': + $sigAlg = '&SigAlg=' . $value; + break; + } + } + + $data['SignedQuery'] = $sigQuery . $relayState . $sigAlg; + + return $data; + } + + + /** + * Validate the signature on a HTTP-Redirect message. + * + * Throws an exception if we are unable to validate the signature. + * + * @param array $data The data we need to validate the query string. + * @param XMLSecurityKey $key The key we should validate the query against. + */ + public static function validateSignature(array $data, XMLSecurityKey $key) { + assert('array_key_exists("Query", $data)'); + assert('array_key_exists("SigAlg", $data)'); + assert('array_key_exists("Signature", $data)'); + + $query = $data['Query']; + $sigAlg = $data['SigAlg']; + $signature = $data['Signature']; + + $signature = base64_decode($signature); + + switch ($sigAlg) { + case XMLSecurityKey::RSA_SHA1: + if ($key->type !== XMLSecurityKey::RSA_SHA1) { + throw new Exception('Invalid key type for validating signature on query string.'); + } + if (!$key->verifySignature($query,$signature)) { + throw new Exception('Unable to validate signature on query string.'); + } + break; + default: + throw new Exception('Unknown signature algorithm: ' . var_export($sigAlg, TRUE)); + } + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/LogoutRequest.php b/lib/SAML2/LogoutRequest.php new file mode 100644 index 0000000000000000000000000000000000000000..f239c9a01d4ab73209f95a0cadb62ac242e6a54a --- /dev/null +++ b/lib/SAML2/LogoutRequest.php @@ -0,0 +1,123 @@ +<?php + +/** + * Class for SAML 2 logout request messages. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_LogoutRequest extends SAML2_Request { + + + /** + * The name identifier of the session that should be terminated. + * + * @var array + */ + private $nameId; + + + /** + * The session index of the session that should be terminated. + * + * @var string|NULL + */ + private $sessionIndex; + + + /** + * Constructor for SAML 2 logout request messages. + * + * @param DOMElement|NULL $xml The input message. + */ + public function __construct(DOMElement $xml = NULL) { + parent::__construct('LogoutRequest', $xml); + + if ($xml === NULL) { + return; + } + + $nameId = SAML2_Utils::xpQuery($xml, './saml:NameID'); + if (empty($nameId)) { + throw new Exception('Missing NameID in logout request.'); + } + $this->nameId = SAML2_Utils::parseNameId($nameId[0]); + + $sessionIndex = SAML2_Utils::xpQuery($xml, './samlp:SessionIndex'); + if (!empty($sessionIndex)) { + $this->sessionIndex = $sessionIndex[0]->textContent; + } + } + + + /** + * Retrieve the name identifier of the session that should be terminated. + * + * @return array The name identifier of the session that should be terminated. + */ + public function getNameId() { + return $this->nameId; + } + + + /** + * Set the name identifier of the session that should be terminated. + * + * The name identifier must be in the format accepted by SAML2_message::buildNameId(). + * + * @see SAML2_message::buildNameId() + * @param array $nameId The name identifier of the session that should be terminated. + */ + public function setNameId($nameId) { + assert('is_array($nameId)'); + + $this->nameId = $nameId; + } + + + /** + * Retrieve the sesion index of the session that should be terminated. + * + * @return string|NULL The sesion index of the session that should be terminated. + */ + public function getSessionIndex() { + return $this->sessionIndex; + } + + + /** + * Set the sesion index of the session that should be terminated. + * + * @param string|NULL $sessionIndex The sesion index of the session that should be terminated. + */ + public function setSessionIndex($sessionIndex) { + assert('is_string($sessionIndex)'); + + $this->sessionIndex = $sessionIndex; + } + + + /** + * Convert this logout request message to an XML element. + * + * @return DOMElement This logout request. + */ + public function toUnsignedXML() { + + $root = parent::toUnsignedXML(); + + SAML2_Utils::addNameId($root, $this->nameId); + + if ($this->sessionIndex !== NULL) { + $sessionIndex = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'SessionIndex'); + $sessionIndex->appendChild($this->document->createTextNode($this->sessionIndex)); + $root->appendChild($sessionIndex); + } + + return $root; + } + +} + + +?> \ No newline at end of file diff --git a/lib/SAML2/LogoutResponse.php b/lib/SAML2/LogoutResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..b242d679aeb55f5a1e47a360beb9e1d1902e391f --- /dev/null +++ b/lib/SAML2/LogoutResponse.php @@ -0,0 +1,25 @@ +<?php + +/** + * Class for SAML 2 LogoutResponse messages. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_LogoutResponse extends SAML2_StatusResponse { + + /** + * Constructor for SAML 2 response messages. + * + * @param string $tagName The tag name of the root element. + * @param DOMElement|NULL $xml The input message. + */ + public function __construct(DOMElement $xml = NULL) { + parent::__construct('LogoutResponse', $xml); + + /* No new fields added by LogoutResponse. */ + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/Message.php b/lib/SAML2/Message.php new file mode 100644 index 0000000000000000000000000000000000000000..2a4c262b9feea60fbe9e9d17adcfd135489ff49b --- /dev/null +++ b/lib/SAML2/Message.php @@ -0,0 +1,474 @@ +<?php + +/** + * Base class for all SAML 2 messages. + * + * Implements what is common between the samlp:RequestAbstractType and + * samlp:StatusResponseType element types. + * + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SAML2_Message implements SAML2_SignedElement { + + /** + * The name of the root element of the DOM tree for the message. + * + * Used when creating a DOM tree from the message. + * + * @var string + */ + private $tagName; + + + /** + * The identifier of this message. + * + * @var string + */ + private $id; + + + /** + * The issue timestamp of this message, as an UNIX timestamp. + * + * @var int + */ + private $issueInstant; + + + /** + * The destination URL of this message if it is known. + * + * @var string|NULL + */ + private $destination; + + + /** + * The entity id of the issuer of this message, or NULL if unknown. + * + * @var string|NULL + */ + private $issuer; + + + /** + * The RelayState associated with this message. + * + * @var string|NULL + */ + private $relayState; + + + /** + * The DOMDocument we are currently building. + * + * This variable is used while generating XML from this message. It holds the + * DOMDocument of the XML we are generating. + * + * @var DOMDocument + */ + protected $document; + + + /** + * The private key we should use to sign the message. + * + * The private key can be NULL, in which case the message is sent unsigned. + * + * @var XMLSecurityKey|NULL + */ + private $signatureKey; + + + /** + * List of certificates that should be included in the message. + * + * @var array + */ + private $certificates; + + + /** + * Available methods for validating this message. + * + * @var array + */ + private $validators; + + + /** + * Initialize a message. + * + * This constructor takes an optional parameter with a DOMElement. If this + * parameter is given, the message will be initialized with data from that + * XML element. + * + * If no XML element is given, the message is initialized with suitable + * default values. + * + * @param string $tagName The tag name of the root element. + * @param DOMElement|NULL $xml The input message. + */ + protected function __construct($tagName, DOMElement $xml = NULL) { + assert('is_string($tagName)'); + $this->tagName = $tagName; + + $this->id = SimpleSAML_Utilities::generateID(); + $this->issueInstant = time(); + $this->certificates = array(); + $this->validators = array(); + + if ($xml === NULL) { + return; + } + + if (!$xml->hasAttribute('ID')) { + throw new Exception('Missing ID attribute on SAML message.'); + } + $this->id = $xml->getAttribute('ID'); + + if ($xml->getAttribute('Version') !== '2.0') { + /* Currently a very strict check. */ + throw new Exception('Unsupported version: ' . $xml->getAttribute('Version')); + } + + $this->issueInstant = SimpleSAML_Utilities::parseSAML2Time($xml->getAttribute('IssueInstant')); + + if ($xml->hasAttribute('Destination')) { + $this->destination = $xml->getAttribute('Destination'); + } + + $issuer = SAML2_Utils::xpQuery($xml, './saml:Issuer'); + if (!empty($issuer)) { + $this->issuer = $issuer[0]->textContent; + } + + + /* Validate the signature element of the message. */ + $sig = SAML2_Utils::validateElement($xml); + if ($sig !== FALSE) { + $this->certificates = $sig['Certificates']; + $this->validators[] = array( + 'Function' => array('SAML2_Utils', 'validateSignature'), + 'Data' => $sig, + ); + } + + } + + + /** + * Add a method for validating this message. + * + * This function is used by the HTTP-Redirect binding, to make it possible to + * check the signature against the one included in the query string. + * + * @param callback $function The function which should be called. + * @param mixed $data The data that should be included as the first parameter to the function. + */ + public function addValidator($function, $data) { + assert('is_callable($function)'); + + $this->validators[] = array( + 'Function' => $function, + 'Data' => $data, + ); + } + + + /** + * Validate this message against a public key. + * + * TRUE is returned on success, FALSE is returned if we don't have any + * signature we can validate. An exception is thrown if the signature + * validation fails. + * + * @param XMLSecurityKey $key The key we should check against. + * @return boolean TRUE on success, FALSE when we don't have a signature. + */ + public function validate(XMLSecurityKey $key) { + + if (count($this->validators) === 0) { + return FALSE; + } + + $exceptions = array(); + + foreach ($this->validators as $validator) { + $function = $validator['Function']; + $data = $validator['Data']; + + try { + call_user_func($function, $data, $key); + /* We were able to validate the message with this validator. */ + return TRUE; + } catch (Exception $e) { + $exceptions[] = $e; + } + } + + /* No validators were able to validate the message. */ + throw $exceptions[0]; + } + + + /** + * Retrieve the identifier of this message. + * + * @return string The identifier of this message. + */ + public function getId() { + return $this->id; + } + + + /** + * Set the identifier of this message. + * + * @param string $id The new identifier of this message. + */ + public function setId($id) { + assert('is_string($id)'); + + $this->id = $id; + } + + + /** + * Retrieve the issue timestamp of this message. + * + * @return int The issue timestamp of this message, as an UNIX timestamp. + */ + public function getIssueInstant() { + return $this->issueInstant; + } + + + /** + * Set the issue timestamp of this message. + * + * @param int $issueInstant The new issue timestamp of this message, as an UNIX timestamp. + */ + public function setIssueInstant($issueInstant) { + assert('is_int($issueInstant)'); + + $this->issueInstant = $issueInstant; + } + + + /** + * Retrieve the destination of this message. + * + * @return string|NULL The destination of this message, or NULL if no destination is given. + */ + public function getDestination() { + return $this->destination; + } + + + /** + * Set the destination of this message. + * + * @param string|NULL $destination The new destination of this message. + */ + public function setDestination($destination) { + assert('is_string($destination) || is_null($destination)'); + + $this->destination = $destination; + } + + + /** + * Retrieve the issuer if this message. + * + * @return string|NULL The issuer of this message, or NULL if no issuer is given. + */ + public function getIssuer() { + return $this->issuer; + } + + + /** + * Set the issuer of this message. + * + * @param string|NULL $issuer The new issuer of this message. + */ + public function setIssuer($issuer) { + assert('is_string($issuer) || is_null($issuer)'); + + $this->issuer = $issuer; + } + + + /** + * Retrieve the RelayState associated with this message. + * + * @return string|NULL The RelayState, or NULL if no RelayState is given. + */ + public function getRelayState() { + return $this->relayState; + } + + + /** + * Set the RelayState associated with this message. + * + * @param string|NULL $relayState The new RelayState. + */ + public function setRelayState($relayState) { + assert('is_string($relayState) || is_null($relayState)'); + + $this->relayState = $relayState; + } + + + /** + * Convert this message to an unsigned XML document. + * + * This method does not sign the resulting XML document. + * + * @return DOMElement The root element of the DOM tree. + */ + public function toUnsignedXML() { + + $this->document = new DOMDocument(); + + $root = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'samlp:' . $this->tagName); + $this->document->appendChild($root); + + /* Ugly hack to add another namespace declaration to the root element. */ + $root->setAttributeNS(SAML2_Const::NS_SAML, 'saml:tmp', 'tmp'); + $root->removeAttributeNS(SAML2_Const::NS_SAML, 'tmp'); + + $root->setAttribute('ID', $this->id); + $root->setAttribute('Version', '2.0'); + $root->setAttribute('IssueInstant', gmdate('Y-m-d\TH:i:s\Z', $this->issueInstant)); + + if ($this->destination !== NULL) { + $root->setAttribute('Destination', $this->destination); + } + + if ($this->issuer !== NULL) { + $issuer = $this->document->createElementNS(SAML2_Const::NS_SAML, 'saml:Issuer'); + $issuer->appendChild($this->document->createTextNode($this->issuer)); + $root->appendChild($issuer); + } + + return $root; + } + + + /** + * Convert this message to a signed XML document. + * + * This method sign the resulting XML document if the private key for + * the signature is set. + * + * @return DOMElement The root element of the DOM tree. + */ + public function toSignedXML() { + + $root = $this->toUnsignedXML(); + + if ($this->signatureKey === NULL) { + /* We don't have a key to sign it with. */ + return $root; + } + + + /* Find the position we should insert the signature node at. */ + if ($this->issuer !== NULL) { + /* + * We have an issuer node. The signature node should come + * after the issuer node. + */ + $issuerNode = $root->firstChild; + $insertBefore = $issuerNode->nextSibling; + } else { + /* No issuer node - the signature element should be the first element. */ + $insertBefore = $root->firstChild; + } + + + SAML2_Utils::insertSignature($this->signatureKey, $this->certificates, $root, $insertBefore); + + return $root; + } + + + /** + * Retrieve the private key we should use to sign the message. + * + * @return XMLSecurityKey|NULL The key, or NULL if no key is specified. + */ + public function getSignatureKey() { + return $this->signatureKey; + } + + + /** + * Set the private key we should use to sign the message. + * + * If the key is NULL, the message will be sent unsigned. + * + * @param XMLSecurityKey|NULL $key + */ + public function setSignatureKey(XMLsecurityKey $signatureKey = NULL) { + $this->signatureKey = $signatureKey; + } + + + /** + * Set the certificates that should be included in the message. + * + * The certificates should be strings with the PEM encoded data. + * + * @param array $certificates An array of certificates. + */ + public function setCertificates(array $certificates) { + $this->certificates = $certificates; + } + + + /** + * Retrieve the certificates that are included in the message. + * + * @return array An array of certificates. + */ + public function getCertificates() { + return $this->certificates; + } + + + /** + * Convert an XML element into a message. + * + * @param DOMElement $xml The root XML element. + * @return SAML2_Message The message. + */ + public static function fromXML(DOMElement $xml) { + + if ($xml->namespaceURI !== SAML2_Const::NS_SAMLP) { + throw new Exception('Unknown namespace of SAML message: ' . var_export($xml->namespaceURI, TRUE)); + } + + switch ($xml->localName) { + case 'AuthnRequest': + return new SAML2_AuthnRequest($xml); + case 'LogoutResponse': + return new SAML2_LogoutResponse($xml); + case 'LogoutRequest': + return new SAML2_LogoutRequest($xml); + case 'Response': + return new SAML2_Response($xml); + default: + throw new Exception('Unknown SAML message: ' . var_export($xml->localName, TRUE)); + } + + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/Request.php b/lib/SAML2/Request.php new file mode 100644 index 0000000000000000000000000000000000000000..a38f51f26cd455e0d43ed8af9c9de1380648d3ed --- /dev/null +++ b/lib/SAML2/Request.php @@ -0,0 +1,17 @@ +<?php + +/** + * Base class for all SAML 2 request messages. + * + * Implements samlp:RequestAbstractType. All of the elements in that type is + * stored in the SAML2_Message class, and this class is therefore empty. It + * is included mainly to make it easy to separate requests from responses. + * + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SAML2_Request extends SAML2_Message { + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/Response.php b/lib/SAML2/Response.php new file mode 100644 index 0000000000000000000000000000000000000000..d45c73f35e3aa1309107d8453e878381f86638cb --- /dev/null +++ b/lib/SAML2/Response.php @@ -0,0 +1,86 @@ +<?php + +/** + * Class for SAML 2 Response messages. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_Response extends SAML2_StatusResponse { + + /** + * The assertions in this response. + */ + private $assertions; + + + /** + * Constructor for SAML 2 response messages. + * + * @param DOMElement|NULL $xml The input message. + */ + public function __construct(DOMElement $xml = NULL) { + parent::__construct('Response', $xml); + + $this->assertions = array(); + + if ($xml === NULL) { + return; + } + + for ($node = $xml->firstChild; $node !== NULL; $node = $node->nextSibling) { + if ($node->namespaceURI !== SAML2_Const::NS_SAML) { + continue; + } + + if ($node->localName === 'Assertion') { + $this->assertions[] = new SAML2_Assertion($node); + } elseif($node->localName === 'EncryptedAssertion') { + $this->assertions[] = new SAML2_EncryptedAssertion($node); + } + } + } + + + /** + * Retrieve the assertions in this response. + * + * @return array Array of SAML2_Assertion and SAML2_EncryptedAssertion objects. + */ + public function getAssertions() { + return $this->assertions; + } + + + /** + * Set the assertions that should be included in this response. + * + * @param array The assertions. + */ + public function setAssertions(array $assertions) { + + $this->assertions = $assertions; + } + + + /** + * Convert the response message to an XML element. + * + * @return DOMElement This response. + */ + public function toUnsignedXML() { + + $root = parent::toUnsignedXML(); + + foreach ($this->assertions as $assertion) { + $node = $assertion->toXML(); + $node = $root->ownerDocument->importNode($node, TRUE); + $root->appendChild($node); + } + + return $root; + } + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/SignedElement.php b/lib/SAML2/SignedElement.php new file mode 100644 index 0000000000000000000000000000000000000000..aef6eaf5c162d10eb46c32c5300e20f59a11bb84 --- /dev/null +++ b/lib/SAML2/SignedElement.php @@ -0,0 +1,31 @@ +<?php + + +/** + * Interface to a SAML 2 element which may be signed. + * + * @package simpleSAMLphp + * @version $Id$ + */ +interface SAML2_SignedElement { + + /** + * Validate this element against a public key. + * + * If no signature is present, FALSE is returned. If a signature is present, + * but cannot be verified, an exception will be thrown. + * + * @param XMLSecurityKey $key The key we should check against. + * @return boolean TRUE if successful, FALSE if we don't have a signature that can be verified. + */ + public function validate(XMLSecurityKey $key); + + + /** + * Retrieve the certificates that are included in the element (if any). + * + * @return array An array of certificates. + */ + public function getCertificates(); + +} \ No newline at end of file diff --git a/lib/SAML2/StatusResponse.php b/lib/SAML2/StatusResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..8093da30cb804b0ad8e957b449ecd30a0c18f004 --- /dev/null +++ b/lib/SAML2/StatusResponse.php @@ -0,0 +1,195 @@ +<?php + +/** + * Base class for all SAML 2 response messages. + * + * Implements samlp:StatusResponseType. All of the elements in that type is + * stored in the SAML2_Message class, and this class is therefore more + * or less empty. It is included mainly to make it easy to separate requests from + * responses. + * + * The status code is represented as an array on the following form: + * array( + * 'Code' => '<top-level status code>', + * 'SubCode' => '<second-level status code>', + * 'Message' => '<status message>', + * ) + * + * Only the 'Code' field is required. The others will be set to NULL if they + * aren't present. + * + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class SAML2_StatusResponse extends SAML2_Message { + + /** + * The ID of the request this is a response to, or NULL if this is an unsolicited response. + * + * @var string|NULL + */ + private $inResponseTo; + + + /** + * The status code of the response. + * + * @var array + */ + private $status; + + + /** + * Constructor for SAML 2 response messages. + * + * @param string $tagName The tag name of the root element. + * @param DOMElement|NULL $xml The input message. + */ + protected function __construct($tagName, DOMElement $xml = NULL) { + parent::__construct($tagName, $xml); + + $this->status = array( + 'Code' => SAML2_Const::STATUS_SUCCESS, + 'SubCode' => NULL, + 'Message' => NULL, + ); + + if ($xml === NULL) { + return; + } + + if ($xml->hasAttribute('InResponseTo')) { + $this->inResponseTo = $xml->getAttribute('InResponseTo'); + } + + $status = SAML2_Utils::xpQuery($xml, './samlp:Status'); + if (empty($status)) { + throw new Exception('Missing status code on response.'); + } + $status = $status[0]; + + $statusCode = SAML2_Utils::xpQuery($status, './samlp:StatusCode'); + if (empty($statusCode)) { + throw new Exception('Missing status code in status element.'); + } + $statusCode = $statusCode[0]; + + $this->status['Code'] = $statusCode->getAttribute('Value'); + + $subCode = SAML2_Utils::xpQuery($statusCode, './samlp:StatusCode'); + if (!empty($subCode)) { + $this->status['SubCode'] = $subCode[0]->getAttribute('Value'); + } + + $message = SAML2_Utils::xpQuery($status, './samlp:StatusMessage'); + if (!empty($message)) { + $this->status['Message'] = $message[0]->textContent; + } + } + + + /** + * Determine whether this is a successful response. + * + * @return boolean TRUE if the status code is success, FALSE if not. + */ + public function isSuccess() { + assert('array_key_exists("Code", $this->status)'); + + if ($this->status['Code'] === SAML2_Const::STATUS_SUCCESS) { + return TRUE; + } + + return FALSE; + } + + + /** + * Retrieve the ID of the request this is a response to. + * + * @return string|NULL The ID of the request. + */ + public function getInResponseTo() { + return $this->inResponseTo; + } + + + /** + * Set the ID of the request this is a response to. + * + * @param string|NULL $inResponseTo The ID of the request. + */ + public function setInResponseTo($inResponseTo) { + assert('is_string($inResponseTo) || is_null($inResponseTo)'); + + $this->inResponseTo = $inResponseTo; + } + + + /** + * Retrieve the status code. + * + * @return array The status code. + */ + public function getStatus() { + return $this->status; + } + + + /** + * Set the status code. + * + * @param array $status The status code. + */ + public function setStatus(array $status) { + assert('array_key_exists("Code", $status)'); + + $this->status = $status; + if (!array_key_exists('SubCode', $status)) { + $this->status['SubCode'] = NULL; + } + if (!array_key_exists('Message', $status)) { + $this->status['Message'] = NULL; + } + } + + + /** + * Convert status response message to an XML element. + * + * @return DOMElement This status response. + */ + public function toUnsignedXML() { + + $root = parent::toUnsignedXML(); + + if ($this->inResponseTo !== NULL) { + $root->setAttribute('InResponseTo', $this->inResponseTo); + } + + $status = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'Status'); + $root->appendChild($status); + + $statusCode = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'StatusCode'); + $statusCode->setAttribute('Value', $this->status['Code']); + $status->appendChild($statusCode); + + if (!is_null($this->status['SubCode'])) { + $subStatusCode = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'StatusCode'); + $subStatusCode->setAttribute('Value', $this->status['SubCode']); + $statusCode->appendChild($subStatusCode); + } + + if (!is_null($this->status['Message'])) { + $statusMessage = $this->document->createElementNS(SAML2_Const::NS_SAMLP, 'StatusMessage'); + $statusMessage->appendChild($this->document->createTextNode($this->status['Message'])); + $status->appendChild($statusMessage); + } + + return $root; + } + + +} + +?> \ No newline at end of file diff --git a/lib/SAML2/Utils.php b/lib/SAML2/Utils.php new file mode 100644 index 0000000000000000000000000000000000000000..164c31a1e62b6a517cb5e95224400b60c401eb06 --- /dev/null +++ b/lib/SAML2/Utils.php @@ -0,0 +1,244 @@ +<?php + +/** + * Helper functions for the SAML2 library. + * + * @package simpleSAMLphp + * @version $Id$ + */ +class SAML2_Utils { + + /** + * Check the Signature in a XML element. + * + * This function expects the XML element to contain a Signature-element + * which contains a reference to the XML-element. This is common for both + * messages and assertions. + * + * Note that this function only validates the element itself. It does not + * check this against any local keys. + * + * If no Signature-element is located, this function will return FALSE. All + * other validation errors result in an exception. On successful validation + * an array will be returned. This array contains the information required to + * check the signature against a public key. + * + * @param DOMElement $root The element which should be validated. + * @return array|FALSE An array with information about the Signature-element. + */ + public static function validateElement(DOMElement $root) { + + /* Create an XML security object. */ + $objXMLSecDSig = new XMLSecurityDSig(); + + /* Both SAML messages and SAML assertions use the 'ID' attribute. */ + $objXMLSecDSig->idKeys[] = 'ID'; + + /* Locate the XMLDSig Signature element to be used. */ + $signatureElement = self::xpQuery($root, './ds:Signature'); + if (count($signatureElement) === 0) { + /* We don't have a signature element ot validate. */ + return FALSE; + } elseif (count($signatureElement) > 1) { + throw new Exception('XMLSec: more than one signature element in root.'); + } + $signatureElement = $signatureElement[0]; + $objXMLSecDSig->sigNode = $signatureElement; + + /* Canonicalize the XMLDSig SignedInfo element in the message. */ + $objXMLSecDSig->canonicalizeSignedInfo(); + + /* Validate referenced xml nodes. */ + if (!$objXMLSecDSig->validateReference()) { + throw new Exception('XMLsec: digest validation failed'); + } + + /* Check that $root is one of the signed nodes. */ + $rootSigned = FALSE; + foreach ($objXMLSecDSig->getValidatedNodes() as $signedNode) { + if ($signedNode->isSameNode($root)) { + $rootSigned = TRUE; + break; + } + } + if (!$rootSigned) { + throw new Exception('XMLSec: The root element is not signed.'); + } + + /* Now we extract all available X509 certificates in the signature element. */ + $certificates = array(); + foreach (self::xpQuery($signatureElement, './ds:KeyInfo/ds:X509Data/ds:X509Certificate') as $certNode) { + $certData = $certNode->textContent; + $certData = str_replace(array("\r", "\n", "\t", ' '), '', $certData); + $certificates[] = $certData; + } + + $ret = array( + 'Signature' => $objXMLSecDSig, + 'Certificates' => $certificates, + ); + + return $ret; + } + + + /** + * Check a signature against a key. + * + * An exception is thrown if we are unable to validate the signature. + * + * @param array $info The information returned by the validateElement()-function. + * @param XMLSecurityKey $key The publickey that should validate the Signature object. + */ + public static function validateSignature(array $info, XMLSecurityKey $key) { + assert('array_key_exists("Signature", $info)'); + + $objXMLSecDSig = $info['Signature']; + + /* Check the signature. */ + if (! $objXMLSecDSig->verify($key)) { + throw new Exception("Unable to validate Signature"); + } + } + + + /** + * Do an XPath query on an XML node. + * + * @param DOMNode $node The XML node. + * @param string $query The query. + * @return array Array with matching DOM nodes. + */ + public static function xpQuery(DOMNode $node, $query) { + assert('is_string($query)'); + static $xpCache = NULL; + + if ($xpCache === NULL || !$xpCache->document->isSameNode($node->ownerDocument)) { + $xpCache = new DOMXPath($node->ownerDocument); + $xpCache->registerNamespace('samlp', SAML2_Const::NS_SAMLP); + $xpCache->registerNamespace('saml', SAML2_Const::NS_SAML); + $xpCache->registerNamespace('ds', XMLSecurityDSig::XMLDSIGNS); + $xpCache->registerNamespace('xenc', XMLSecEnc::XMLENCNS); + } + + $results = $xpCache->query($query, $node); + $ret = array(); + for ($i = 0; $i < $results->length; $i++) { + $ret[$i] = $results->item($i); + } + + return $ret; + } + + + /** + * Parse a boolean attribute. + * + * @param DOMElement $node The element we should fetch the attribute from. + * @param string $attributeName The name of the attribute. + * @param mixed $default The value that should be returned if the attribute doesn't exist. + * @return bool|mixed The value of the attribute, or $default if the attribute doesn't exist. + */ + public static function parseBoolean(DOMElement $node, $attributeName, $default = NULL) { + assert('is_string($attributeName)'); + + if (!$node->hasAttribute($attributeName)) { + return $default; + } + $value = $node->getAttribute($attributeName); + switch (strtolower($value)) { + case '0': + case 'false': + return FALSE; + case '1': + case 'true': + return TRUE; + default: + throw new Exception('Invalid value of boolean attribute ' . var_export($attributeName, TRUE) . ': ' . var_export($value, TRUE)); + } + } + + + /** + * Create a NameID element. + * + * The NameId array can have the following elements: 'Value', 'Format', + * 'NameQualifier, 'SPNameQualifier' + * + * Only the 'Value'-element is required. + * + * @param DOMElement $node The DOM node we should append the NameId to. + * @param array $nameId The name identifier. + */ + public static function addNameId(DOMElement $node, array $nameId) { + assert('array_key_exists("Value", $nameId)'); + + $xml = $node->ownerDocument->createElementNS(SAML2_Const::NS_SAML, 'saml:NameID'); + $node->appendChild($xml); + + if (array_key_exists('NameQualifier', $nameId)) { + $xml->setAttribute('NameQualifier', $nameId['NameQualifier']); + } + if (array_key_exists('SPNameQualifier', $nameId)) { + $xml->setAttribute('SPNameQualifier', $nameId['SPNameQualifier']); + } + if (array_key_exists('Format', $nameId)) { + $xml->setAttribute('Format', $nameId['Format']); + } + + $xml->appendChild($node->ownerDocument->createTextNode($nameId['Value'])); + } + + + /** + * Parse a NameID element. + * + * @param DOMElement $xml The DOM element we should parse. + * @return array The parsed name identifier. + */ + public static function parseNameId(DOMElement $xml) { + + $ret = array('Value' => $xml->textContent); + + foreach (array('NameQualifier', 'SPNameQualifier', 'Format') as $attr) { + if ($xml->hasAttribute($attr)) { + $ret[$attr] = $xml->getAttribute($attr); + } + } + + return $ret; + } + + + /** + * Insert a Signature-node. + * + * @param XMLSecurityKey $key The key we should use to sign the message. + * @param array $certificates The certificates we should add to the signature node. + * @param DOMElement $root The XML node we should sign. + * @param DomElement $insertBefore The XML element we should insert the signature element before. + */ + public static function insertSignature(XMLSecurityKey $key, array $certificates, DOMElement $root, DOMNode $insertBefore = NULL) { + + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + + $objXMLSecDSig->addReferenceList( + array($root), + XMLSecurityDSig::SHA1, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), + array('id_name' => 'ID') + ); + + $objXMLSecDSig->sign($key); + + foreach ($certificates as $certificate) { + $objXMLSecDSig->add509Cert($certificate, TRUE); + } + + $objXMLSecDSig->insertSignature($root, $insertBefore); + + } +} + +?> \ No newline at end of file