public function validate(Response $response, Result $result) { $destination = $response->getDestination(); if (!$this->expectedDestination->equals(new Destination($destination))) { $result->addError(sprintf('Destination in response "%s" does not match the expected destination "%s"', $destination, $this->expectedDestination)); } }
/** * @test * @group signature */ public function signed_message_with_valid_signature_is_validated_correctly() { $pattern = Certificate::CERTIFICATE_PATTERN; preg_match($pattern, CertificatesMock::PUBLIC_KEY_PEM, $matches); $config = new IdentityProvider(array('certificateData' => $matches[1])); $validator = new PublicKeyValidator(new SimpleTestLogger(), new KeyLoader()); $doc = DOMDocumentFactory::fromFile(__DIR__ . '/response.xml'); $response = new Response($doc->firstChild); $response->setSignatureKey(CertificatesMock::getPrivateKey()); $response->setCertificates(array(CertificatesMock::PUBLIC_KEY_PEM)); // convert to signed response $response = new Response($response->toSignedXML()); $this->assertTrue($validator->canValidate($response, $config), 'Cannot validate the element'); $this->assertTrue($validator->hasValidSignature($response, $config), 'The signature is not valid'); }
public static function handleLoginRequest(IPerson $Person) { try { $binding = Binding::getCurrentBinding(); } catch (Exception $e) { return static::throwUnauthorizedError('Cannot obtain SAML2 binding'); } $request = $binding->receive(); // build response $response = new Response(); $response->setInResponseTo($request->getId()); $response->setRelayState($request->getRelayState()); $response->setDestination($request->getAssertionConsumerServiceURL()); // build assertion $assertion = new Assertion(); $assertion->setIssuer(static::$issuer); $assertion->setSessionIndex(ContainerSingleton::getInstance()->generateId()); $assertion->setNotBefore(time() - 30); $assertion->setNotOnOrAfter(time() + 300); $assertion->setAuthnContext(SAML2_Constants::AC_PASSWORD); // build subject confirmation $sc = new SubjectConfirmation(); $sc->Method = SAML2_Constants::CM_BEARER; $sc->SubjectConfirmationData = new SubjectConfirmationData(); $sc->SubjectConfirmationData->NotOnOrAfter = $assertion->getNotOnOrAfter(); $sc->SubjectConfirmationData->Recipient = $request->getAssertionConsumerServiceURL(); $sc->SubjectConfirmationData->InResponseTo = $request->getId(); $assertion->setSubjectConfirmation([$sc]); // set NameID $assertion->setNameId(['Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', 'Value' => $Person->Username . '@' . static::$issuer]); // set additional attributes $assertion->setAttributes(['User.Email' => [$Person->Email], 'User.Username' => [$Person->Username]]); // attach assertion to response $response->setAssertions([$assertion]); // create signature $privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, ['type' => 'private']); $privateKey->loadKey(static::$privateKey); $response->setSignatureKey($privateKey); $response->setCertificates([static::$certificate]); // prepare response $responseXML = $response->toSignedXML(); $responseString = $responseXML->ownerDocument->saveXML($responseXML); // dump response and quit # header('Content-Type: text/xml'); # die($responseString); // send response $responseBinding = new HTTPPost(); $responseBinding->send($response); }
/** * @test * @group signature */ public function signed_message_with_valid_signature_is_validated_correctly() { $pattern = Certificate::CERTIFICATE_PATTERN; preg_match($pattern, CertificatesMock::PUBLIC_KEY_PEM, $matches); $certdata = X509::createFromCertificateData($matches[1]); $fingerprint = $certdata->getFingerprint(); $fingerprint_retry = $certdata->getFingerprint(); $this->assertTrue($fingerprint->equals($fingerprint_retry), 'Cached fingerprint does not match original'); $config = new IdentityProvider(array('certificateFingerprints' => array($fingerprint->getRaw()))); $validator = new FingerprintValidator(new SimpleTestLogger(), new FingerprintLoader()); $doc = DOMDocumentFactory::fromFile(__DIR__ . '/response.xml'); $response = new Response($doc->firstChild); $response->setSignatureKey(CertificatesMock::getPrivateKey()); $response->setCertificates(array(CertificatesMock::PUBLIC_KEY_PEM)); // convert to signed response $response = new Response($response->toSignedXML()); $this->assertTrue($validator->canValidate($response, $config), 'Cannot validate the element'); $this->assertTrue($validator->hasValidSignature($response, $config), 'The signature is not valid'); }
/** * Construct an authnresponse and send it. * Also test setting a relaystate and destination for the response. */ public function testSendAuthnResponse() { $response = new Response(); $response->setIssuer('testIssuer'); $response->setRelayState('http://example.org'); $response->setDestination('http://example.org/login?success=yes'); $response->setSignatureKey(CertificatesMock::getPrivateKey()); $hr = new HTTPPost(); $hr->send($response); }
/** * Process an assertion in a response. * * Will throw an exception if it is invalid. * * @param SimpleSAML_Configuration $spMetadata The metadata of the service provider. * @param SimpleSAML_Configuration $idpMetadata The metadata of the identity provider. * @param \SAML2\Response $response The response containing the assertion. * @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion. * @param bool $responseSigned Whether the response is signed. * @return \SAML2\Assertion The assertion, if it is valid. */ private static function processAssertion(SimpleSAML_Configuration $spMetadata, SimpleSAML_Configuration $idpMetadata, \SAML2\Response $response, $assertion, $responseSigned) { assert('$assertion instanceof \\SAML2\\Assertion || $assertion instanceof \\SAML2\\EncryptedAssertion'); assert('is_bool($responseSigned)'); $assertion = self::decryptAssertion($idpMetadata, $spMetadata, $assertion); if (!self::checkSign($idpMetadata, $assertion)) { if (!$responseSigned) { throw new SimpleSAML_Error_Exception('Neither the assertion nor the response was signed.'); } } /* At least one valid signature found. */ $currentURL = \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(); /* Check various properties of the assertion. */ $notBefore = $assertion->getNotBefore(); if ($notBefore !== NULL && $notBefore > time() + 60) { throw new SimpleSAML_Error_Exception('Received an assertion that is valid in the future. Check clock synchronization on IdP and SP.'); } $notOnOrAfter = $assertion->getNotOnOrAfter(); if ($notOnOrAfter !== NULL && $notOnOrAfter <= time() - 60) { throw new SimpleSAML_Error_Exception('Received an assertion that has expired. Check clock synchronization on IdP and SP.'); } $sessionNotOnOrAfter = $assertion->getSessionNotOnOrAfter(); if ($sessionNotOnOrAfter !== NULL && $sessionNotOnOrAfter <= time() - 60) { throw new SimpleSAML_Error_Exception('Received an assertion with a session that has expired. Check clock synchronization on IdP and SP.'); } $validAudiences = $assertion->getValidAudiences(); if ($validAudiences !== NULL) { $spEntityId = $spMetadata->getString('entityid'); if (!in_array($spEntityId, $validAudiences, TRUE)) { $candidates = '[' . implode('], [', $validAudiences) . ']'; throw new SimpleSAML_Error_Exception('This SP [' . $spEntityId . '] is not a valid audience for the assertion. Candidates were: ' . $candidates); } } $found = FALSE; $lastError = 'No SubjectConfirmation element in Subject.'; $validSCMethods = array(\SAML2\Constants::CM_BEARER, \SAML2\Constants::CM_HOK, \SAML2\Constants::CM_VOUCHES); foreach ($assertion->getSubjectConfirmation() as $sc) { if (!in_array($sc->Method, $validSCMethods)) { $lastError = 'Invalid Method on SubjectConfirmation: ' . var_export($sc->Method, TRUE); continue; } /* Is SSO with HoK enabled? IdP remote metadata overwrites SP metadata configuration. */ $hok = $idpMetadata->getBoolean('saml20.hok.assertion', NULL); if ($hok === NULL) { $hok = $spMetadata->getBoolean('saml20.hok.assertion', FALSE); } if ($sc->Method === \SAML2\Constants::CM_BEARER && $hok) { $lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed'; continue; } if ($sc->Method === \SAML2\Constants::CM_HOK && !$hok) { $lastError = 'Holder-of-Key SubjectConfirmation received, but the Holder-of-Key profile is not enabled.'; continue; } $scd = $sc->SubjectConfirmationData; if ($sc->Method === \SAML2\Constants::CM_HOK) { /* Check HoK Assertion */ if (\SimpleSAML\Utils\HTTP::isHTTPS() === FALSE) { $lastError = 'No HTTPS connection, but required for Holder-of-Key SSO'; continue; } if (isset($_SERVER['SSL_CLIENT_CERT']) && empty($_SERVER['SSL_CLIENT_CERT'])) { $lastError = 'No client certificate provided during TLS Handshake with SP'; continue; } /* Extract certificate data (if this is a certificate). */ $clientCert = $_SERVER['SSL_CLIENT_CERT']; $pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m'; if (!preg_match($pattern, $clientCert, $matches)) { $lastError = 'Error while looking for client certificate during TLS handshake with SP, the client certificate does not ' . 'have the expected structure'; continue; } /* We have a valid client certificate from the browser. */ $clientCert = str_replace(array("\r", "\n", " "), '', $matches[1]); foreach ($scd->info as $thing) { if ($thing instanceof \SAML2\XML\ds\KeyInfo) { $keyInfo[] = $thing; } } if (count($keyInfo) != 1) { $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:KeyInfo> element in <SubjectConfirmationData> allowed'; continue; } foreach ($keyInfo[0]->info as $thing) { if ($thing instanceof \SAML2\XML\ds\X509Data) { $x509data[] = $thing; } } if (count($x509data) != 1) { $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Data> element in <ds:KeyInfo> within <SubjectConfirmationData> allowed'; continue; } foreach ($x509data[0]->data as $thing) { if ($thing instanceof \SAML2\XML\ds\X509Certificate) { $x509cert[] = $thing; } } if (count($x509cert) != 1) { $lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Certificate> element in <ds:X509Data> within <SubjectConfirmationData> allowed'; continue; } $HoKCertificate = $x509cert[0]->certificate; if ($HoKCertificate !== $clientCert) { $lastError = 'Provided client certificate does not match the certificate bound to the Holder-of-Key assertion'; continue; } } if ($scd->NotBefore && $scd->NotBefore > time() + 60) { $lastError = 'NotBefore in SubjectConfirmationData is in the future: ' . $scd->NotBefore; continue; } if ($scd->NotOnOrAfter && $scd->NotOnOrAfter <= time() - 60) { $lastError = 'NotOnOrAfter in SubjectConfirmationData is in the past: ' . $scd->NotOnOrAfter; continue; } if ($scd->Recipient !== NULL && $scd->Recipient !== $currentURL) { $lastError = 'Recipient in SubjectConfirmationData does not match the current URL. Recipient is ' . var_export($scd->Recipient, TRUE) . ', current URL is ' . var_export($currentURL, TRUE) . '.'; continue; } if ($scd->InResponseTo !== NULL && $response->getInResponseTo() !== NULL && $scd->InResponseTo !== $response->getInResponseTo()) { $lastError = 'InResponseTo in SubjectConfirmationData does not match the Response. Response has ' . var_export($response->getInResponseTo(), TRUE) . ', SubjectConfirmationData has ' . var_export($scd->InResponseTo, TRUE) . '.'; continue; } $found = TRUE; break; } if (!$found) { throw new SimpleSAML_Error_Exception('Error validating SubjectConfirmation in Assertion: ' . $lastError); } /* As far as we can tell, the assertion is valid. */ /* Maybe we need to base64 decode the attributes in the assertion? */ if ($idpMetadata->getBoolean('base64attributes', FALSE)) { $attributes = $assertion->getAttributes(); $newAttributes = array(); foreach ($attributes as $name => $values) { $newAttributes[$name] = array(); foreach ($values as $value) { foreach (explode('_', $value) as $v) { $newAttributes[$name][] = base64_decode($v); } } } $assertion->setAttributes($newAttributes); } /* Decrypt the NameID element if it is encrypted. */ if ($assertion->isNameIdEncrypted()) { try { $keys = self::getDecryptionKeys($idpMetadata, $spMetadata); } catch (Exception $e) { throw new SimpleSAML_Error_Exception('Error decrypting NameID: ' . $e->getMessage()); } $blacklist = self::getBlacklistedAlgorithms($idpMetadata, $spMetadata); $lastException = NULL; foreach ($keys as $i => $key) { try { $assertion->decryptNameId($key, $blacklist); SimpleSAML\Logger::debug('Decryption with key #' . $i . ' succeeded.'); $lastException = NULL; break; } catch (Exception $e) { SimpleSAML\Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage()); $lastException = $e; } } if ($lastException !== NULL) { throw $lastException; } } return $assertion; }
/** * Construct an authnresponse and send it. * Also test setting a relaystate and destination for the response. */ public function testSendAuthnResponse() { $response = new Response(); $response->setIssuer('testIssuer'); $response->setRelayState('http://example.org'); $response->setDestination('http://example.org/login?success=yes'); $hr = new HTTPRedirect(); $hr->send($response); }
/** * @param \SAML2\Response $response * * @return \SAML2\Assertion[] */ private function processAssertions(Response $response) { $assertions = $response->getAssertions(); if (empty($assertions)) { throw new NoAssertionsFoundException('No assertions found in response from IdP.'); } if (!$this->responseIsSigned) { foreach ($assertions as $assertion) { if (!$assertion->getWasSignedAtConstruction()) { throw new UnsignedResponseException('Both the response and the assertion it containes are not signed.'); } } } return $this->assertionProcessor->processAssertions($assertions); }
/** * @return \SAML2\Response */ private function getSignedResponseWithSignedAssertion() { $doc = new \DOMDocument(); $doc->load(__DIR__ . '/response.xml'); $response = new Response($doc->firstChild); $response->setSignatureKey(CertificatesMock::getPrivateKey()); $response->setCertificates(array(CertificatesMock::PUBLIC_KEY_PEM)); $assertions = $response->getAssertions(); $assertion = $assertions[0]; $assertion->setSignatureKey(CertificatesMock::getPrivateKey()); $assertion->setCertificates(array(CertificatesMock::PUBLIC_KEY_PEM)); return new Response($response->toSignedXML()); }
/** * @group Message */ public function testConvertIssuerToXML() { // first, try with common Issuer objects (Format=entity) $response = new Response(); $issuer = new XML\saml\Issuer(); $issuer->value = 'https://gateway.stepup.org/saml20/sp/metadata'; $response->setIssuer($issuer); $xml = $response->toUnsignedXML(); $xml_issuer = Utils::xpQuery($xml, './saml_assertion:Issuer'); $xml_issuer = $xml_issuer[0]; $this->assertFalse($xml_issuer->hasAttributes()); $this->assertEquals($issuer->value, $xml_issuer->textContent); // now, try an Issuer with another format and attributes $issuer->Format = Constants::NAMEID_UNSPECIFIED; $issuer->NameQualifier = 'SomeNameQualifier'; $issuer->SPNameQualifier = 'SomeSPNameQualifier'; $issuer->SPProvidedID = 'SomeSPProvidedID'; $response->setIssuer($issuer); $xml = $response->toUnsignedXML(); $xml_issuer = Utils::xpQuery($xml, './saml_assertion:Issuer'); $xml_issuer = $xml_issuer[0]; $this->assertTrue($xml_issuer->hasAttributes()); $this->assertEquals($issuer->value, $xml_issuer->textContent); $this->assertEquals($issuer->NameQualifier, $xml_issuer->getAttribute('NameQualifier')); $this->assertEquals($issuer->SPNameQualifier, $xml_issuer->getAttribute('SPNameQualifier')); $this->assertEquals($issuer->SPProvidedID, $xml_issuer->getAttribute('SPProvidedID')); // finally, make sure we can skip the Issuer by setting it to null $response->setIssuer(null); $xml = $response->toUnsignedXML(); $this->assertEmpty(Utils::xpQuery($xml, './saml_assertion:Issuer')); }
public function validate(Response $response, Result $result) { if (!$response->isSuccess()) { $result->addError($this->buildMessage($response->getStatus())); } }