It will parse the string into a DOM document and validate this document against the schema.
public static validateXML ( string | DOMDocument $xml, string $schema, boolean $debug = false ) : string | DOMDocument | ||
$xml | string | DOMDocument | The XML string or document which should be validated. |
$schema | string | The schema filename which should be used. |
$debug | boolean | To disable/enable the debug mode |
Résultat | string | DOMDocument | $dom string that explains the problem or the DOMDocument |
/** * Checks if the Logout Request recieved is valid. * * @return boolean If the Logout Request is or not valid */ public function isValid($retrieveParametersFromServer = false) { $this->_error = null; try { $dom = new DOMDocument(); $dom = OneLogin_Saml2_Utils::loadXML($dom, $this->_logoutRequest); $idpData = $this->_settings->getIdPData(); $idPEntityId = $idpData['entityId']; if ($this->_settings->isStrict()) { $security = $this->_settings->getSecurityData(); if ($security['wantXMLValidation']) { $res = OneLogin_Saml2_Utils::validateXML($dom, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive()); if (!$res instanceof DOMDocument) { throw new Exception("Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"); } } $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery(); // Check NotOnOrAfter if ($dom->documentElement->hasAttribute('NotOnOrAfter')) { $na = OneLogin_Saml2_Utils::parseSAML2Time($dom->documentElement->getAttribute('NotOnOrAfter')); if ($na <= time()) { throw new Exception('Timing issues (please check your clock settings)'); } } // Check destination if ($dom->documentElement->hasAttribute('Destination')) { $destination = $dom->documentElement->getAttribute('Destination'); if (!empty($destination)) { if (strpos($destination, $currentURL) === false) { throw new Exception("The LogoutRequest was received at {$currentURL} instead of {$destination}"); } } } $nameId = $this->getNameId($dom, $this->_settings->getSPkey()); // Check issuer $issuer = $this->getIssuer($dom); if (!empty($issuer) && $issuer != $idPEntityId) { throw new Exception("Invalid issuer in the Logout Request"); } if ($security['wantMessagesSigned']) { if (!isset($_GET['Signature'])) { throw new Exception("The Message of the Logout Request is not signed and the SP require it"); } } } if (isset($_GET['Signature'])) { if (!isset($_GET['SigAlg'])) { $signAlg = XMLSecurityKey::RSA_SHA1; } else { $signAlg = $_GET['SigAlg']; } if ($retrieveParametersFromServer) { $signedQuery = 'SAMLRequest=' . OneLogin_Saml2_Utils::extractOriginalQueryParam('SAMLRequest'); if (isset($_GET['RelayState'])) { $signedQuery .= '&RelayState=' . OneLogin_Saml2_Utils::extractOriginalQueryParam('RelayState'); } $signedQuery .= '&SigAlg=' . OneLogin_Saml2_Utils::extractOriginalQueryParam('SigAlg'); } else { $signedQuery = 'SAMLRequest=' . urlencode($_GET['SAMLRequest']); if (isset($_GET['RelayState'])) { $signedQuery .= '&RelayState=' . urlencode($_GET['RelayState']); } $signedQuery .= '&SigAlg=' . urlencode($signAlg); } if (!isset($idpData['x509cert']) || empty($idpData['x509cert'])) { throw new Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required'); } $cert = $idpData['x509cert']; $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); $objKey->loadKey($cert, false, true); if ($signAlg != XMLSecurityKey::RSA_SHA1) { try { $objKey = OneLogin_Saml2_Utils::castKey($objKey, $signAlg, 'public'); } catch (Exception $e) { throw new Exception('Invalid signAlg in the recieved Logout Request'); } } if (!$objKey->verifySignature($signedQuery, base64_decode($_GET['Signature']))) { throw new Exception('Signature validation failed. Logout Request rejected'); } } return true; } catch (Exception $e) { $this->_error = $e->getMessage(); $debug = $this->_settings->isDebugActive(); if ($debug) { echo $this->_error; } return false; } }
/** * Tests the validateXML method of the OneLogin_Saml2_Utils * * @covers OneLogin_Saml2_Utils::validateXML */ public function testValidateXML() { $metadataUnloaded = '<xml><EntityDescriptor>'; $this->assertEquals(OneLogin_Saml2_Utils::validateXML($metadataUnloaded, 'saml-schema-metadata-2.0.xsd'), 'unloaded_xml'); $metadataInvalid = file_get_contents(TEST_ROOT . '/data/metadata/noentity_metadata_settings1.xml'); $this->assertEquals(OneLogin_Saml2_Utils::validateXML($metadataInvalid, 'saml-schema-metadata-2.0.xsd'), 'invalid_xml'); $metadataExpired = file_get_contents(TEST_ROOT . '/data/metadata/expired_metadata_settings1.xml'); $res = OneLogin_Saml2_Utils::validateXML($metadataExpired, 'saml-schema-metadata-2.0.xsd'); $this->assertTrue($res instanceof DOMDocument); $metadataOk = file_get_contents(TEST_ROOT . '/data/metadata/metadata_settings1.xml'); $res2 = OneLogin_Saml2_Utils::validateXML($metadataOk, 'saml-schema-metadata-2.0.xsd'); $this->assertTrue($res instanceof DOMDocument); $dom = new DOMDocument(); $dom->load($metadataOk); $res2 = OneLogin_Saml2_Utils::validateXML($dom, 'saml-schema-metadata-2.0.xsd'); $this->assertTrue($res instanceof DOMDocument); }
/** * Validates an XML SP Metadata. * * @param string $xml Metadata's XML that will be validate * * @return Array The list of found errors */ public function validateMetadata($xml) { assert('is_string($xml)'); $errors = array(); $res = OneLogin_Saml2_Utils::validateXML($xml, 'saml-schema-metadata-2.0.xsd', $this->_debug); if (!$res instanceof DOMDocument) { $errors[] = $res; } else { $dom = $res; $element = $dom->documentElement; if ($element->tagName !== 'md:EntityDescriptor') { $errors[] = 'noEntityDescriptor_xml'; } else { $validUntil = $cacheDuration = $expireTime = null; if ($element->hasAttribute('validUntil')) { $validUntil = OneLogin_Saml2_Utils::parseSAML2Time($element->getAttribute('validUntil')); } if ($element->hasAttribute('cacheDuration')) { $cacheDuration = $element->getAttribute('cacheDuration'); } $expireTime = OneLogin_Saml2_Utils::getExpireTime($cacheDuration, $validUntil); if (isset($expireTime) && time() > $expireTime) { $errors[] = 'expired_xml'; } } } // TODO: Support Metadata Sign Validation return $errors; }
/** * Determines if the SAML LogoutResponse is valid * * @param string $requestId The ID of the LogoutRequest sent by this SP to the IdP * * @throws Exception * @return bool Returns if the SAML LogoutResponse is or not valid */ public function isValid($requestId = null) { $this->_error = null; try { $idpData = $this->_settings->getIdPData(); $idPEntityId = $idpData['entityId']; if ($this->_settings->isStrict()) { $res = OneLogin_Saml2_Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive()); if (!$res instanceof DOMDocument) { throw new Exception("Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"); } $security = $this->_settings->getSecurityData(); // Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided if (isset($requestId) && $this->document->documentElement->hasAttribute('InResponseTo')) { $inResponseTo = $this->document->documentElement->getAttribute('InResponseTo'); if ($requestId != $inResponseTo) { throw new Exception("The InResponseTo of the Logout Response: {$inResponseTo}, does not match the ID of the Logout request sent by the SP: {$requestId}"); } } // Check issuer $issuer = $this->getIssuer(); if (empty($issuer) || $issuer != $idPEntityId) { throw new Exception("Invalid issuer in the Logout Response"); } $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery(); // Check destination if ($this->document->documentElement->hasAttribute('Destination')) { $destination = $this->document->documentElement->getAttribute('Destination'); if (!empty($destination)) { if (strpos($destination, $currentURL) === false) { throw new Exception("The LogoutResponse was received at {$currentURL} instead of {$destination}"); } } } if ($security['wantMessagesSigned']) { if (!isset($_GET['Signature'])) { throw new Exception("The Message of the Logout Response is not signed and the SP requires it"); } } } if (isset($_GET['Signature'])) { if (!isset($_GET['SigAlg'])) { $signAlg = XMLSecurityKey::RSA_SHA1; } else { $signAlg = $_GET['SigAlg']; } $signedQuery = 'SAMLResponse=' . urlencode($_GET['SAMLResponse']); if (isset($_GET['RelayState'])) { $signedQuery .= '&RelayState=' . urlencode($_GET['RelayState']); } $signedQuery .= '&SigAlg=' . urlencode($signAlg); if (!isset($idpData['x509cert']) || empty($idpData['x509cert'])) { throw new Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required'); } $cert = $idpData['x509cert']; $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); $objKey->loadKey($cert, false, true); if ($signAlg != XMLSecurityKey::RSA_SHA1) { try { $objKey = OneLogin_Saml2_Utils::castKey($objKey, $signAlg, 'public'); } catch (Exception $e) { throw new Exception('Invalid signAlg in the recieved Logout Response'); } } if (!$objKey->verifySignature($signedQuery, base64_decode($_GET['Signature']))) { throw new Exception('Signature validation failed. Logout Response rejected'); } } return true; } catch (Exception $e) { $this->_error = $e->getMessage(); $debug = $this->_settings->isDebugActive(); if ($debug) { echo $this->_error; } return false; } }
/** * Determines if the SAML Response is valid using the certificate. * * @param string $requestId The ID of the AuthNRequest sent by this SP to the IdP * * @throws Exception * @return bool Validate the document */ public function isValid($requestId = null) { $this->_error = null; try { // Check SAML version if ($this->document->documentElement->getAttribute('Version') != '2.0') { throw new Exception('Unsupported SAML version'); } if (!$this->document->documentElement->hasAttribute('ID')) { throw new Exception('Missing ID attribute on SAML Response'); } $status = $this->checkStatus(); $singleAssertion = $this->validateNumAssertions(); if (!$singleAssertion) { throw new Exception('SAML Response must contain 1 assertion'); } $idpData = $this->_settings->getIdPData(); $idPEntityId = $idpData['entityId']; $spData = $this->_settings->getSPData(); $spEntityId = $spData['entityId']; $signedElements = array(); if ($this->encrypted) { $signNodes = $this->decryptedDocument->getElementsByTagName('Signature'); } else { $signNodes = $this->document->getElementsByTagName('Signature'); } foreach ($signNodes as $signNode) { $signedElements[] = $signNode->parentNode->localName; } if (!empty($signedElements)) { // Check SignedElements if (!$this->validateSignedElements($signedElements)) { throw new Exception('Found an unexpected Signature Element. SAML Response rejected'); } } if ($this->_settings->isStrict()) { $security = $this->_settings->getSecurityData(); if ($security['wantXMLValidation']) { $res = OneLogin_Saml2_Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive()); if (!$res instanceof DOMDocument) { throw new Exception("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"); } } $currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery(); if ($this->document->documentElement->hasAttribute('InResponseTo')) { $responseInResponseTo = $this->document->documentElement->getAttribute('InResponseTo'); } // Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided if (isset($requestId) && isset($responseInResponseTo)) { if ($requestId != $responseInResponseTo) { throw new Exception("The InResponseTo of the Response: {$responseInResponseTo}, does not match the ID of the AuthNRequest sent by the SP: {$requestId}"); } } if (!$this->encrypted && $security['wantAssertionsEncrypted']) { throw new Exception("The assertion of the Response is not encrypted and the SP requires it"); } if ($security['wantNameIdEncrypted']) { $encryptedIdNodes = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData'); if ($encryptedIdNodes->length == 0) { throw new Exception("The NameID of the Response is not encrypted and the SP requires it"); } } // Validate Asserion timestamps $validTimestamps = $this->validateTimestamps(); if (!$validTimestamps) { throw new Exception('Timing issues (please check your clock settings)'); } // EncryptedAttributes are not supported $encryptedAttributeNodes = $this->_queryAssertion('/saml:AttributeStatement/saml:EncryptedAttribute'); if ($encryptedAttributeNodes->length > 0) { throw new Exception("There is an EncryptedAttribute in the Response and this SP not support them"); } // Check destination if ($this->document->documentElement->hasAttribute('Destination')) { $destination = $this->document->documentElement->getAttribute('Destination'); if (!empty($destination)) { if (strpos($destination, $currentURL) !== 0) { $currentURLrouted = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery(); if (strpos($destination, $currentURLrouted) !== 0) { throw new Exception("The response was received at {$currentURL} instead of {$destination}"); } } } } // Check audience $validAudiences = $this->getAudiences(); if (!empty($validAudiences) && !in_array($spEntityId, $validAudiences)) { throw new Exception("{$spEntityId} is not a valid audience for this Response"); } // Check the issuers $issuers = $this->getIssuers(); foreach ($issuers as $issuer) { if (empty($issuer) || $issuer != $idPEntityId) { throw new Exception("Invalid issuer in the Assertion/Response"); } } // Check the session Expiration $sessionExpiration = $this->getSessionNotOnOrAfter(); if (!empty($sessionExpiration) && $sessionExpiration <= time()) { throw new Exception("The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response"); } // Check the SubjectConfirmation, at least one SubjectConfirmation must be valid $anySubjectConfirmation = false; $subjectConfirmationNodes = $this->_queryAssertion('/saml:Subject/saml:SubjectConfirmation'); foreach ($subjectConfirmationNodes as $scn) { if ($scn->hasAttribute('Method') && $scn->getAttribute('Method') != OneLogin_Saml2_Constants::CM_BEARER) { continue; } $subjectConfirmationDataNodes = $scn->getElementsByTagName('SubjectConfirmationData'); if ($subjectConfirmationDataNodes->length == 0) { continue; } else { $scnData = $subjectConfirmationDataNodes->item(0); if ($scnData->hasAttribute('InResponseTo')) { $inResponseTo = $scnData->getAttribute('InResponseTo'); if ($responseInResponseTo != $inResponseTo) { continue; } } if ($scnData->hasAttribute('Recipient')) { $recipient = $scnData->getAttribute('Recipient'); if (!empty($recipient) && strpos($recipient, $currentURL) === false) { continue; } } if ($scnData->hasAttribute('NotOnOrAfter')) { $noa = OneLogin_Saml2_Utils::parseSAML2Time($scnData->getAttribute('NotOnOrAfter')); if ($noa <= time()) { continue; } } if ($scnData->hasAttribute('NotBefore')) { $nb = OneLogin_Saml2_Utils::parseSAML2Time($scnData->getAttribute('NotBefore')); if ($nb > time()) { continue; } } $anySubjectConfirmation = true; break; } } if (!$anySubjectConfirmation) { throw new Exception("A valid SubjectConfirmation was not found on this Response"); } if ($security['wantAssertionsSigned'] && !in_array('Assertion', $signedElements)) { throw new Exception("The Assertion of the Response is not signed and the SP requires it"); } if ($security['wantMessagesSigned'] && !in_array('Response', $signedElements)) { throw new Exception("The Message of the Response is not signed and the SP requires it"); } } if (!empty($signedElements)) { $cert = $idpData['x509cert']; $fingerprint = $idpData['certFingerprint']; $fingerprintalg = $idpData['certFingerprintAlgorithm']; // Only validates the first signed element if (in_array('Response', $signedElements)) { $documentToValidate = $this->document; } else { $documentToValidate = $signNodes->item(0)->parentNode; if ($this->encrypted) { $encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/saml:Subject/saml:EncryptedID'); if ($encryptedIDNodes->length > 0) { throw new Exception('Unsigned SAML Response that contains a signed and encrypted Assertion with encrypted nameId is not supported.'); } } } if (!OneLogin_Saml2_Utils::validateSign($documentToValidate, $cert, $fingerprint, $fingerprintalg)) { throw new Exception('Signature validation failed. SAML Response rejected'); } } else { throw new Exception('No Signature found. SAML Response rejected'); } return true; } catch (Exception $e) { $this->_error = $e->getMessage(); $debug = $this->_settings->isDebugActive(); if ($debug) { echo $this->_error; } return false; } }
/** * Tests the builder method of the OneLogin_Saml2_Metadata * * @covers OneLogin_Saml2_Metadata::builder */ public function testBuilderWithAttributeConsumingServiceWithMultipleAttributeValue() { $settingsDir = TEST_ROOT . '/settings/'; include $settingsDir . 'settings4.php'; $settings = new OneLogin_Saml2_Settings($settingsInfo); $spData = $settings->getSPData(); $security = $settings->getSecurityData(); $organization = $settings->getOrganization(); $contacts = $settings->getContacts(); $metadata = OneLogin_Saml2_Metadata::builder($spData, $security['authnRequestsSigned'], $security['wantAssertionsSigned'], null, null, $contacts, $organization); $this->assertContains('<md:ServiceName xml:lang="en">Service Name</md:ServiceName>', $metadata); $this->assertContains('<md:ServiceDescription xml:lang="en">Service Description</md:ServiceDescription>', $metadata); $this->assertContains('<md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="uid" isRequired="true" />', $metadata); $this->assertContains('<saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">userType</saml:AttributeValue>', $metadata); $this->assertContains('<saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">admin</saml:AttributeValue>', $metadata); $result = \OneLogin_Saml2_Utils::validateXML($metadata, 'saml-schema-metadata-2.0.xsd'); $this->assertInstanceOf('DOMDocument', $result); }