diff --git a/phpcs.xml b/phpcs.xml index 5eabf531b..1abfbc6ac 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -36,6 +36,7 @@ **/HTTPRedirectTest.php **/SOAPTest.php tests/SAML2/Assertion/Validation/AssertionValidatorTest.php + tests/SAML2/Entity/ServiceProviderTest.php tests/SAML2/XML/saml/AssertionTest.php tests/SAML2/XML/saml/AttributeValueTest.php tests/SAML2/XML/saml/AuthnContextTest.php diff --git a/src/Binding.php b/src/Binding.php index e4ca793d7..f863646ca 100644 --- a/src/Binding.php +++ b/src/Binding.php @@ -25,6 +25,20 @@ */ abstract class Binding { + /** + * The schema to be used for schema validation + * + * @var string + */ + protected static string $schemaFile = 'resources/schemas/saml-schema-protocol-2.0.xsd'; + + /** + * Whether or not to perform schema validation + * + * @var bool + */ + protected bool $schemaValidation = true; + /** * The RelayState associated with the message. * @@ -157,7 +171,20 @@ public function getDestination(): ?string /** - * Set the RelayState associated with he message. + * 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(?string $destination = null): void + { + $this->destination = $destination; + } + + + /** + * Set the RelayState associated with the message. * * @param string|null $relayState The RelayState. */ @@ -179,15 +206,24 @@ public function getRelayState(): ?string /** - * Override the destination of a message. + * Set the schema validation for the message. * - * Set to null to use the destination set in the message. + * @param bool $schemaValidation + */ + public function setSchemaValidation(bool $schemaValidation): void + { + $this->schemaValidation = $schemaValidation; + } + + + /** + * Get the schema validation setting. * - * @param string|null $destination The destination the message should be delivered to. + * @return bool */ - public function setDestination(?string $destination = null): void + public function getSchemaValidation(): bool { - $this->destination = $destination; + return $this->schemaValidation; } diff --git a/src/Binding/HTTPPost.php b/src/Binding/HTTPPost.php index acfcda229..0228f9c91 100644 --- a/src/Binding/HTTPPost.php +++ b/src/Binding/HTTPPost.php @@ -91,9 +91,11 @@ public function receive(ServerRequestInterface $request): AbstractMessage } $msgStr = base64_decode($msgStr, true); - $msgStr = DOMDocumentFactory::fromString($msgStr)->saveXML(); - $document = DOMDocumentFactory::fromString($msgStr); + $document = DOMDocumentFactory::fromString( + xml: $msgStr, + schemaFile: $this->getSchemaValidation() ? self::$schemaFile : null, + ); Utils::getContainer()->debugMessage($document->documentElement, 'in'); $msg = MessageFactory::fromXML($document->documentElement); diff --git a/src/Binding/HTTPRedirect.php b/src/Binding/HTTPRedirect.php index f0357c01b..4dba36dfb 100644 --- a/src/Binding/HTTPRedirect.php +++ b/src/Binding/HTTPRedirect.php @@ -148,7 +148,10 @@ public function receive(ServerRequestInterface $request): AbstractMessage throw new Exception('Error while inflating SAML message.'); } - $document = DOMDocumentFactory::fromString($message); + $document = DOMDocumentFactory::fromString( + xml: $message, + schemaFile: $this->getSchemaValidation() ? self::$schemaFile : null, + ); Utils::getContainer()->debugMessage($document->documentElement, 'in'); $message = MessageFactory::fromXML($document->documentElement); diff --git a/src/Binding/SOAP.php b/src/Binding/SOAP.php index 9b0b35ec1..04c35e5af 100644 --- a/src/Binding/SOAP.php +++ b/src/Binding/SOAP.php @@ -101,8 +101,12 @@ public function receive(/** @scrutinizer ignore-unused */ServerRequestInterface $xpCache = XPath::getXPath($document->documentElement); /** @var \DOMElement[] $results */ $results = XPath::xpQuery($xml, '/SOAP-ENV:Envelope/SOAP-ENV:Body/*[1]', $xpCache); + $document = DOMDocumentFactory::fromString( + xml: $results[0]->ownerDocument->saveXML($results[0]), + schemaFile: $this->getSchemaValidation() ? self::$schemaFile : null, + ); - return MessageFactory::fromXML($results[0]); + return MessageFactory::fromXML($document->documentElement); } diff --git a/src/Exception/ConstraintValidationFailedException.php b/src/Exception/ConstraintValidationFailedException.php new file mode 100644 index 000000000..3ad396f44 --- /dev/null +++ b/src/Exception/ConstraintValidationFailedException.php @@ -0,0 +1,12 @@ +signatureAlgorithmFactory = new SignatureAlgorithmFactory(); + $this->encryptionAlgorithmFactory = new EncryptionAlgorithmFactory(); + $this->keyTransportAlgorithmFactory = new KeyTransportAlgorithmFactory(); + } + + + /** + */ + public function setStateProvider(StateProviderInterface $stateProvider): void + { + $this->stateProvider = $stateProvider; + } + + + /** + */ + public function setStorageProvider(StorageProviderInterface $storageProvider): void + { + $this->storageProvider = $storageProvider; + } + + + /** + * Receive a verified, and optionally validated Response. + * + * Upon receiving the response from the binding, the signature will be validated first. + * Once the signature checks out, the assertions are decrypted, their signatures verified + * and then any encrypted NameID's and/or attributes are decrypted. + * + * @param \Psr\Http\Message\ServerRequestInterface $request + * @return \SimpleSAML\SAML2\XML\samlp\Response The validated response. + * + * @throws \SimpleSAML\SAML2\Exception\Protocol\UnsupportedBindingException + */ + public function receiveResponse(ServerRequestInterface $request): Response + { + $binding = Binding::getCurrentBinding($request); + + if ($binding instanceof HTTPArtifact) { + if ($this->storageProvider === null) { + throw new RuntimeException( + "A StorageProvider is required to use the HTTP-Artifact binding.", + ); + } + + $artifact = $binding->receiveArtifact($request); + $this->idpMetadata = $this->metadataProvider->getIdPMetadataForSha1($artifact->getSourceId()); + + if ($this->idpMetadata === null) { + throw new MetadataNotFoundException(sprintf( + 'No metadata found for remote entity with SHA1 ID: %s', + $artifact->getSourceId(), + )); + } + + $binding->setIdpMetadata($this->idpMetadata); + $binding->setSPMetadata($this->spMetadata); + } + + $binding->setSchemaValidation($this->performSchemaValidation); + $rawResponse = $binding->receive($request); + Assert::isInstanceOf($rawResponse, Response::class, ResourceNotRecognizedException::class); // Wrong type of msg + + // Will return a raw Response prior to any form of verification + if ($this->bypassResponseVerification === true) { + return $rawResponse; + } + + // Fetch the metadata for the remote entity + if (!($binding instanceof HTTPArtifact)) { + $this->idpMetadata = $this->metadataProvider->getIdPMetadata($rawResponse->getIssuer()->getContent()); + + if ($this->idpMetadata === null) { + throw new MetadataNotFoundException(sprintf( + 'No metadata found for remote entity with entityID: %s', + $rawResponse->getIssuer()->getContent(), + )); + } + } + + // Verify the signature (if any) + $this->responseWasSigned = $rawResponse->isSigned(); + $verifiedResponse = $this->responseWasSigned ? $this->verifyElementSignature($rawResponse) : $rawResponse; + + $state = null; + $stateId = $verifiedResponse->getInResponseTo(); + + if (!empty($stateId)) { + if ($this->stateProvider === null) { + throw new RuntimeException( + "A StateProvider is required to correlate responses to their initial request.", + ); + } + + // this should be a response to a request we sent earlier + try { + $state = $this->stateProvider::loadState($stateId, 'saml:sp:sso'); + } catch (RuntimeException $e) { + // something went wrong, + Utils::getContainer()->getLogger()->warning(sprintf( + 'Could not load state specified by InResponseTo: %s; processing response as unsolicited.', + $e->getMessage(), + )); + } + } + + $issuer = $verifiedResponse->getIssuer()->getContent(); + if ($state === null) { + if ($this->enableUnsolicited === false) { + throw new RequestDeniedException('Unsolicited responses are denied by configuration.'); + } + } else { + // check that the issuer is the one we are expecting + Assert::keyExists($state, 'ExpectedIssuer'); + + if ($state['ExpectedIssuer'] !== $issuer) { + throw new ResourceNotRecognizedException("Issuer doesn't match the one the AuthnRequest was sent to."); + } + } + + $this->idpMetadata = $this->metadataProvider->getIdPMetadata($issuer); + if ($this->idpMetadata === null) { + throw new MetadataNotFoundException(sprintf( + 'No metadata found for remote identity provider with entityID: %s', + $issuer, + )); + } + + $responseValidator = ResponseValidator::createResponseValidator( + $this->idpMetadata, + $this->spMetadata, + $binding, + ); + $responseValidator->validate($verifiedResponse); + + if ($this->encryptedAssertions === true) { + Assert::allIsInstanceOf($verifiedResponse->getAssertions(), EncryptedAssertion::class); + } + + // Decrypt and verify assertions, then rebuild the response. + $verifiedAssertions = $this->decryptAndVerifyAssertions($verifiedResponse->getAssertions()); + $decryptedResponse = new Response( + $verifiedResponse->getStatus(), + $verifiedResponse->getIssueInstant(), + $verifiedResponse->getIssuer(), + $verifiedResponse->getID(), + $verifiedResponse->getVersion(), + $verifiedResponse->getInResponseTo(), + $verifiedResponse->getDestination(), + $verifiedResponse->getConsent(), + $verifiedResponse->getExtensions(), + $verifiedAssertions, + ); + + + // Will return a verified and fully decrypted Response prior to any form of validation + if ($this->bypassConstraintValidation === true) { + return $decryptedResponse; + } + + // TODO: Validate assertions + return $decryptedResponse; + } + + + /** + * Process the assertions and decrypt any encrypted elements inside. + * + * @param \SimpleSAML\SAML2\XML\saml\Assertion[] $unverifiedAssertions + * @return \SimpleSAML\SAML2\XML\saml\Assertion[] + * + * @throws \SimpleSAML\SAML2\Exception\RuntimeException if none of the keys could be used to decrypt the element + */ + protected function decryptAndVerifyAssertions(array $unverifiedAssertions): array + { + $wantAssertionsSigned = $this->spMetadata->getWantAssertionsSigned(); + + /** + * See paragraph 6.2 of the SAML 2.0 core specifications for the applicable processing rules + * + * Long story short - Decrypt the assertion first, then validate it's signature + * Once the signature is verified, decrypt any BaseID, NameID or Attribute that's encrypted + */ + $verifiedAssertions = []; + foreach ($unverifiedAssertions as $i => $assertion) { + // Decrypt the assertions + $decryptedAssertion = ($assertion instanceof EncryptedAssertion) + ? $this->decryptElement($assertion) + : $assertion; + + // Verify that the request is signed, if we require this by configuration + if ($wantAssertionsSigned === true) { + Assert::true($decryptedAssertion->isSigned(), RuntimeException::class); + } + + // Verify the signature on the assertions (if any) + $verifiedAssertion = $this->verifyElementSignature($decryptedAssertion); + + // Decrypt the NameID and replace it inside the assertion's Subject + $nameID = $verifiedAssertion->getSubject()?->getIdentifier(); + + if ($nameID instanceof EncryptedID) { + $decryptedNameID = $this->decryptElement($nameID); + // Anything we can't decrypt, we leave up for the application to deal with + try { + $subject = new Subject($decryptedNameID, $verifiedAssertion->getSubjectConfirmation()); + } catch (RuntimeException) { + $subject = $verifiedAssertion->getSubject(); + } + } else { + $subject = $verifiedAssertion->getSubject(); + } + + // Decrypt any occurrences of EncryptedAttribute and replace them inside the assertion's AttributeStatement + $statements = $verifiedAssertion->getStatements(); + foreach ($verifiedAssertion->getStatements() as $j => $statement) { + if ($statement instanceof AttributeStatement) { + $attributes = $statement->getAttributes(); + if ($statement->hasEncryptedAttributes()) { + foreach ($statement->getEncryptedAttributes() as $encryptedAttribute) { + // Anything we can't decrypt, we leave up for the application to deal with + try { + $attributes[] = $this->decryptElement($encryptedAttribute); + } catch (RuntimeException) { + $attributes[] = $encryptedAttribute; + } + } + } + + $statements[$j] = new AttributeStatement($attributes); + } + } + + // Rebuild the Assertion + $verifiedAssertions[] = new Assertion( + $verifiedAssertion->getIssuer(), + $verifiedAssertion->getIssueInstant(), + $verifiedAssertion->getID(), + $subject, + $verifiedAssertion->getConditions(), + $statements, + ); + } + + return $verifiedAssertions; + } + + + /** + * Decrypt the given element using the decryption keys provided to us. + * + * @param \SimpleSAML\XMLSecurity\XML\EncryptedElementInterface $element + * @return \SimpleSAML\XMLSecurity\EncryptableElementInterface + * + * @throws \SimpleSAML\SAML2\Exception\RuntimeException if none of the keys could be used to decrypt the element + */ + protected function decryptElement(EncryptedElementInterface $element): EncryptableElementInterface + { + $factory = $this->encryptionAlgorithmFactory; + + // If the IDP has a pre-shared key, try decrypting with that + $preSharedKey = $this->idpMetadata->getPreSharedKey(); + if ($preSharedKey !== null) { + $encryptionAlgorithm = $element?->getEncryptedKey()?->getEncryptionMethod() + ?? $this->idpMetadata->getPreSharedKeyAlgorithm(); + + $decryptor = $factory->getAlgorithm($encryptionAlgorithm, $preSharedKey); + try { + return $element->decrypt($decryptor); + } catch (Exception $e) { + // Continue to try decrypting with asymmetric keys. + } + } + + $encryptionAlgorithm = $element->getEncryptedKey()->getEncryptionMethod()->getAlgorithm(); + foreach ($this->spMetadata->getDecryptionKeys() as $decryptionKey) { + $factory = $this->keyTransportAlgorithmFactory; + $decryptor = $factory->getAlgorithm($encryptionAlgorithm, $decryptionKey); + try { + return $element->decrypt($decryptor); + } catch (Exception $e) { + continue; + } + } + + throw new RuntimeException(sprintf( + 'Unable to decrypt %s with any of the available keys.', + $element::class, + )); + } + + + /** + * Verify the signature of an element using the available validation keys. + * + * @param \SimpleSAML\XMLSecurity\XML\SignedElementInterface $element + * @return \SimpleSAML\XMLSecurity\XML\SignableElementInterface The validated element. + * + * @throws \SimpleSAML\XMLSecurity\Exception\SignatureVerificationFailedException + */ + protected function verifyElementSignature(SignedElementInterface $element): SignableElementInterface + { + $signatureAlgorithm = $element->getSignature()->getSignedInfo()->getSignatureMethod()->getAlgorithm(); + + foreach ($this->idpMetadata->getValidatingKeys() as $validatingKey) { + $verifier = $this->signatureAlgorithmFactory->getAlgorithm($signatureAlgorithm, $validatingKey); + + try { + return $element->verify($verifier); + } catch (SignatureVerificationFailedException $e) { + continue; + } + } + + throw new SignatureVerificationFailedException(); + } +} diff --git a/src/SAML2/Metadata/AbstractProvider.php b/src/SAML2/Metadata/AbstractProvider.php new file mode 100644 index 000000000..7bbf61ccc --- /dev/null +++ b/src/SAML2/Metadata/AbstractProvider.php @@ -0,0 +1,137 @@ +signatureAlgorithm; + } + + + /** + * Get the private key to use for signing messages. + * + * @return \SimpleSAML\XMLSecurity\Key\PrivateKey|null + */ + public function getSigningKey(): ?PrivateKey + { + return $this->signingKey; + } + + + /** + * Get the validating keys to verify a message signature with. + * + * @return array<\SimpleSAML\XMLSecurity\Key\PublicKey> + */ + public function getValidatingKeys(): array + { + return $this->validatingKeys; + } + + + /** + * Get the public key to use for encrypting messages. + * + * @return \SimpleSAML\XMLSecurity\Key\PublicKey|null + */ + public function getEncryptionKey(): ?PublicKey + { + return $this->encryptionKey; + } + + + /** + * Get the symmetric key to use for encrypting/decrypting messages. + * + * @return \SimpleSAML\XMLSecurity\Key\SymmetricKey|null + */ + public function getPreSharedKey(): ?SymmetricKey + { + return $this->preSharedKey; + } + + + /** + * Get the symmetric encrypting/decrypting algorithm to use. + * + * @return string|null + */ + public function getPreSharedKeyAlgorithm(): ?string + { + return $this->preSharedKeyAlgorithm; + } + + + /** + * Get the decryption keys to decrypt the assertion with. + * + * @return array<\SimpleSAML\XMLSecurity\Key\PrivateKey> + */ + public function getDecryptionKeys(): array + { + return $this->decryptionKeys; + } + + + /** + * Retrieve the configured entity ID for this entity + */ + public function getEntityId(): string + { + return $this->entityId; + } + + + /** + * Retrieve the configured IDPList for this entity. + * + * @return string[] + */ + public function getIDPList(): array + { + return $this->IDPList; + } +} diff --git a/src/SAML2/Metadata/IdentityProvider.php b/src/SAML2/Metadata/IdentityProvider.php new file mode 100644 index 000000000..80fd28663 --- /dev/null +++ b/src/SAML2/Metadata/IdentityProvider.php @@ -0,0 +1,42 @@ + + */ + public function getAssertionConsumerService(): array + { + return $this->assertionConsumerService; + } + + + /** + * Retrieve the configured value for whether assertions must be signed. + * + * @return bool + */ + public function getWantAssertionsSigned(): bool + { + return $this->wantAssertionsSigned; + } +} diff --git a/src/SAML2/MetadataProviderInterface.php b/src/SAML2/MetadataProviderInterface.php new file mode 100644 index 000000000..f75b3eeb1 --- /dev/null +++ b/src/SAML2/MetadataProviderInterface.php @@ -0,0 +1,34 @@ +spMetadata->getAssertionConsumerService() as $assertionConsumerService) { + if ($assertionConsumerService->getLocation() === $response->getDestination()) { + if (Binding::getBinding($assertionConsumerService->getBinding()) instanceof $this->binding) { + return; + } + } + } + throw new ResourceNotRecognizedException(); + } +} diff --git a/src/SAML2/Process/IdentityProviderAwareInterface.php b/src/SAML2/Process/IdentityProviderAwareInterface.php new file mode 100644 index 000000000..d45021828 --- /dev/null +++ b/src/SAML2/Process/IdentityProviderAwareInterface.php @@ -0,0 +1,15 @@ +idpMetadata = $idpMetadata; + } +} diff --git a/src/SAML2/Process/ServiceProviderAwareInterface.php b/src/SAML2/Process/ServiceProviderAwareInterface.php new file mode 100644 index 000000000..7c04a265c --- /dev/null +++ b/src/SAML2/Process/ServiceProviderAwareInterface.php @@ -0,0 +1,15 @@ +spMetadata = $spMetadata; + } +} diff --git a/src/SAML2/Process/Validator/ResponseValidator.php b/src/SAML2/Process/Validator/ResponseValidator.php new file mode 100644 index 000000000..4e84af67f --- /dev/null +++ b/src/SAML2/Process/Validator/ResponseValidator.php @@ -0,0 +1,42 @@ +addConstraintValidator(new DestinationMatches($spMetadata, $binding)); +// $validator->addConstraintValidator(new IsSuccesful()); + + return $validator; + } +} diff --git a/src/SAML2/Process/Validator/ValidatorInterface.php b/src/SAML2/Process/Validator/ValidatorInterface.php new file mode 100644 index 000000000..6e9bccada --- /dev/null +++ b/src/SAML2/Process/Validator/ValidatorInterface.php @@ -0,0 +1,26 @@ + */ + protected array $validators; + + + /** + * Add a validation to the chain. + * + * @param \SimpleSAML\SAML2\Process\ConstraintValidation\ConstraintValidatorInterface $validation + */ + public function addConstraintValidator(ConstraintValidatorInterface $validator) + { + if ($validator instanceof IdentityProviderAwareInterface) { + $validator->setIdentityProvider($this->idpMetadata); + } + + if ($validator instanceof ServiceProviderAwareInterface) { + $validator->setServiceProvider($this->spMetadata); + } + + $this->validators[] = $validator; + } + + + /** + * Runs all the validations in the validation chain. + * + * If this function returns, all validations have been succesful. + * + * @throws \SimpleSAML\SAML2\Exception\ConstraintViolationFailedException when one of the conditions fail. + */ + public function validate(SerializableElementInterface $element): void + { + foreach ($this->validators as $validator) { + $validator->validate($element); + } + } +} diff --git a/src/SAML2/StateProviderInterface.php b/src/SAML2/StateProviderInterface.php new file mode 100644 index 000000000..45b11f98b --- /dev/null +++ b/src/SAML2/StateProviderInterface.php @@ -0,0 +1,24 @@ +documentElement); + /** @var \DOMElement[] $results */ + $results = XPath::xpQuery($xml, '/SOAP-ENV:Envelope/SOAP-ENV:Body/*[1]', $xpCache); + + // This is already too late to perform schema validation. + // TODO: refactor the SOAPClient and artifact binding. The SOAPClient should be a generic tool from xml-soap + $document = DOMDocumentFactory::fromString( + xml: $results[0]->ownerDocument->saveXML(), + schemaFile: $this->getSchemaValidation() ? self::$schemaFile : null, + ); + // Extract the message from the response /** @var \SimpleSAML\XML\SerializableElementInterface[] $messages */ $messages = $env->getBody()->getElements(); diff --git a/tests/SAML2/Entity/ServiceProviderTest.php b/tests/SAML2/Entity/ServiceProviderTest.php new file mode 100644 index 000000000..c6ea6bc3c --- /dev/null +++ b/tests/SAML2/Entity/ServiceProviderTest.php @@ -0,0 +1,278 @@ + C::BINDING_HTTP_POST, + 'Location' => 'https://example.org/metadata', + 'Index' => 0, + ]), + ], + decryptionKeys: [ + PEMCertificatesMock::getPrivateKey(PEMCertificatesMock::SELFSIGNED_PRIVATE_KEY), + ], + wantAssertionsSigned: true, + ); + + self::$idpMetadata = new Metadata\IdentityProvider( + entityId: 'https://simplesamlphp.org/idp/metadata', + validatingKeys: [ + PEMCertificatesMock::getPublicKey(PEMCertificatesMock::PUBLIC_KEY), + PEMCertificatesMock::getPublicKey(PEMCertificatesMock::OTHER_PUBLIC_KEY), + PEMCertificatesMock::getPublicKey(PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY), + ], + ); + + /** A valid solicited signed response with a signed assertion */ + $q = [ + 'SAMLResponse' => 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIENvbnNlbnQ9Imh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvc3AvbWV0YWRhdGEiIERlc3RpbmF0aW9uPSJodHRwczovL2V4YW1wbGUub3JnL21ldGFkYXRhIiBJRD0iYWJjMTIzIiBJblJlc3BvbnNlVG89IlBIUFVuaXQiIElzc3VlSW5zdGFudD0iMjAyNC0wNy0yNVQyMjo0NDoyMVoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvaWRwL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhNTEyIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNhYmMxMjMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTUxMiIvPjxkczpEaWdlc3RWYWx1ZT4rV2YzTXpjNFZ1dlFJUEpnbEdIeUhnR0l0clc0S3cwTFdJWXo4SitYcGZtTG5taEdwQkRUYTllTjhrYWN3Qm1wdkhXNkFsOHc5SDJNcjhrZjNPbThKZz09PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmRTY2FLUHVZUnhRbEdLSHVqWnJtZUZkb1M3Y2F1aXNvMGZ3Q2gvN0s0VDFjcHdaTUNad0R5SU1qMGlzVlJycmVZM2FqSXpRTnNSVy9uWVRFd0FFWkloM3NOWGdJcW5vWmg3dng4SUN4TEo1cGVNL215citGMGlMbTk5YWs3U0FmN2FYV25McmpxWjBVOTVUMjlrd0plcXE3MzM2eHlycm9mKzZPbzhvdkN2cz08L2RzOlNpZ25hdHVyZVZhbHVlPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkFzc2VydGlvbiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzkzYWY2NTUyMTk0NjRmYjQwM2IzNDQzNmNmYjBjNWNiMWQ5YTU1MDIiIElzc3VlSW5zdGFudD0iMTk3MC0wMS0wMVQwMTozMzozMVoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyPnVybjp4LXNpbXBsZXNhbWxwaHA6aXNzdWVyPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNfOTNhZjY1NTIxOTQ2NGZiNDAzYjM0NDM2Y2ZiMGM1Y2IxZDlhNTUwMiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPkoxWnNyOTRVU2FXaEhDVmNnamdnM3dHZFBHWTViVkNOeW16cEx4Yk1XekE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlJIcmxHQVdSN3RROEM4T3l6cVp4WmRlU2NiRjN1QnZJdU13RG5GdXdYcjA4OCthNnBJdlRqUTNtM0syUytwM2NGNWRxS2ZiRUd5UDBmbGRpZFNtdVVLU1B2V1hTVm14bFlONldOKzBtbEVYV08rNUduN0drTXFseHlrQTAvWDlZL3AvcmYydGhEQTJPN2dnQmRrRDlsMlFUbzNYYU1CZXZtUmgwcHl4d0lNZz08L2RzOlNpZ25hdHVyZVZhbHVlPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vc3AuZXhhbXBsZS5vcmcvYXV0aGVudGljYXRpb24vc3AvbWV0YWRhdGEiPlNvbWVOYW1lSURWYWx1ZTwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3NwLmV4YW1wbGUub3JnL2F1dGhlbnRpY2F0aW9uL3NwL21ldGFkYXRhIj5Tb21lT3RoZXJOYW1lSURWYWx1ZTwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfMTM2MDNhNjU2NWE2OTI5N2U5ODA5MTc1YjA1MmQxMTU5NjUxMjFjOCIgTm90T25PckFmdGVyPSIyMDExLTA4LTMxVDA4OjUxOjA1WiIgUmVjaXBpZW50PSJodHRwczovL3NwLmV4YW1wbGUub3JnL2F1dGhlbnRpY2F0aW9uL3NwL2NvbnN1bWUtYXNzZXJ0aW9uIi8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTEtMDgtMzFUMDg6NTE6MDVaIiBOb3RPbk9yQWZ0ZXI9IjIwMTEtMDgtMzFUMTA6NTE6MDVaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvc3AvbWV0YWRhdGE8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDExLTA4LTMxVDA4OjUxOjA1WiIgU2Vzc2lvbkluZGV4PSJfOTNhZjY1NTIxOTQ2NGZiNDAzYjM0NDM2Y2ZiMGM1Y2IxZDlhNTUwMiI+PHNhbWw6U3ViamVjdExvY2FsaXR5IEFkZHJlc3M9IjEyNy4wLjAuMSIvPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVybjp0ZXN0OlNlcnZpY2VJRCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmludGVnZXIiPjE8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idXJuOnRlc3Q6RW50aXR5Q29uY2VybmVkSUQiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czppbnRlZ2VyIj4xPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVybjp0ZXN0OkVudGl0eUNvbmNlcm5lZFN1YklEIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6aW50ZWdlciI+MTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==', + ]; + $request = new ServerRequest('POST', 'http://tnyholm.se'); + self::$validResponse = $request->withParsedBody($q); + + /** A valid unsolicited signed response with a signed assertion */ + $q = [ + 'SAMLResponse' => 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIENvbnNlbnQ9Imh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvc3AvbWV0YWRhdGEiIERlc3RpbmF0aW9uPSJodHRwczovL2V4YW1wbGUub3JnL21ldGFkYXRhIiBJRD0iYWJjMTIzIiBJc3N1ZUluc3RhbnQ9IjIwMjQtMDctMjVUMjI6NTE6MzRaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3VlciB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL3NpbXBsZXNhbWxwaHAub3JnL2lkcC9tZXRhZGF0YTwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTUxMiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjYWJjMTIzIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGE1MTIiLz48ZHM6RGlnZXN0VmFsdWU+Sjk4U0t2K1NDdXFzakY0N0s3VmxXKzVKZUZkZTRCL29aOU5ac2hzM1N3VWE0ZjRXSW05ZkdaK0hOMi9MTFRBemhSWVZHeHVIUFNjUUd2WUV5Unc1cXc9PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5KUnpad1FvMWZic0R0L0FWcVZEcHU5WjVXNnNLVi9YZmtyUlhLOE84MzRaSzVDYzZHTWFBbHdnYnlaWFBZbDhsNWthSGY5eHNxWXBMT25NbzdoY0c1R2hJaHJ1QzFEK1NPUzlRdUJsSDF3ckhvdmhLNVJQWUhZUVh0NUh2UjJQdGhLQ21VclJLVE8vRkptVGQvZHN3TUt6czZCNzk5VnRuSzIwTllrNTdrc2s9PC9kczpTaWduYXR1cmVWYWx1ZT48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il85M2FmNjU1MjE5NDY0ZmI0MDNiMzQ0MzZjZmIwYzVjYjFkOWE1NTAyIiBJc3N1ZUluc3RhbnQ9IjE5NzAtMDEtMDFUMDE6MzM6MzFaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3Vlcj51cm46eC1zaW1wbGVzYW1scGhwOmlzc3Vlcjwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjXzkzYWY2NTUyMTk0NjRmYjQwM2IzNDQzNmNmYjBjNWNiMWQ5YTU1MDIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxkczpEaWdlc3RWYWx1ZT5KMVpzcjk0VVNhV2hIQ1ZjZ2pnZzN3R2RQR1k1YlZDTnltenBMeGJNV3pBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5SSHJsR0FXUjd0UThDOE95enFaeFpkZVNjYkYzdUJ2SXVNd0RuRnV3WHIwODgrYTZwSXZUalEzbTNLMlMrcDNjRjVkcUtmYkVHeVAwZmxkaWRTbXVVS1NQdldYU1ZteGxZTjZXTiswbWxFWFdPKzVHbjdHa01xbHh5a0EwL1g5WS9wL3JmMnRoREEyTzdnZ0Jka0Q5bDJRVG8zWGFNQmV2bVJoMHB5eHdJTWc9PC9kczpTaWduYXR1cmVWYWx1ZT48L2RzOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnRyYW5zaWVudCIgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3NwLmV4YW1wbGUub3JnL2F1dGhlbnRpY2F0aW9uL3NwL21ldGFkYXRhIj5Tb21lTmFtZUlEVmFsdWU8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9hdXRoZW50aWNhdGlvbi9zcC9tZXRhZGF0YSI+U29tZU90aGVyTmFtZUlEVmFsdWU8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iXzEzNjAzYTY1NjVhNjkyOTdlOTgwOTE3NWIwNTJkMTE1OTY1MTIxYzgiIE5vdE9uT3JBZnRlcj0iMjAxMS0wOC0zMVQwODo1MTowNVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9hdXRoZW50aWNhdGlvbi9zcC9jb25zdW1lLWFzc2VydGlvbiIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDExLTA4LTMxVDA4OjUxOjA1WiIgTm90T25PckFmdGVyPSIyMDExLTA4LTMxVDEwOjUxOjA1WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3NpbXBsZXNhbWxwaHAub3JnL3NwL21ldGFkYXRhPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMS0wOC0zMVQwODo1MTowNVoiIFNlc3Npb25JbmRleD0iXzkzYWY2NTUyMTk0NjRmYjQwM2IzNDQzNmNmYjBjNWNiMWQ5YTU1MDIiPjxzYW1sOlN1YmplY3RMb2NhbGl0eSBBZGRyZXNzPSIxMjcuMC4wLjEiLz48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46dGVzdDpTZXJ2aWNlSUQiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czppbnRlZ2VyIj4xPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVybjp0ZXN0OkVudGl0eUNvbmNlcm5lZElEIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6aW50ZWdlciI+MTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1cm46dGVzdDpFbnRpdHlDb25jZXJuZWRTdWJJRCI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOmludGVnZXIiPjE8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=', + ]; + $request = new ServerRequest('POST', 'http://tnyholm.se'); + self::$validUnsolicitedResponse = $request->withParsedBody($q); + + /** A valid solicited signed response with an asymmetric encrypted signed assertion */ + $q = [ + 'SAMLResponse' => 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIENvbnNlbnQ9Imh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvc3AvbWV0YWRhdGEiIERlc3RpbmF0aW9uPSJodHRwczovL2V4YW1wbGUub3JnL21ldGFkYXRhIiBJRD0iYWJjMTIzIiBJblJlc3BvbnNlVG89IlBIUFVuaXQiIElzc3VlSW5zdGFudD0iMjAyNC0wNy0zMFQwOTozNToyNVoiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vc2ltcGxlc2FtbHBocC5vcmcvaWRwL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhNTEyIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNhYmMxMjMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTUxMiIvPjxkczpEaWdlc3RWYWx1ZT44OWhQWHVUQ0M4RE1jbEJMSzY0Nkh0ekhsZDlLcnJKdE05UEdoaUZjdm9vb3hlbGRDVW92bWRBcEVVOWcrOEtJaHJpbTczYU8zY2dPZVUxSHJoRkdVQT09PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmJUQTZjTkM4dlRxaElHRDJHazl3ZFFsQUMzeU1nVEdKZWVUd3BPdEhHVFptRTg5bnFEWUpLTDNtR1dEYWNkTVdDWW1kMlYxVFFxallocEpISkJrNFd5NXgzTUE1VHVwQzNmRGg0VmFmVHVESXpEOWU3aUtOc0lUVkVJVW9teTZFeFpsK1dHRGdxQnhqWURJK0RSL1hrUHhITFdqSkhmVzQ2RmZ6WlVHS1hJRT08L2RzOlNpZ25hdHVyZVZhbHVlPjwvZHM6U2lnbmF0dXJlPjxzYW1scDpTdGF0dXM+PHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbHA6U3RhdHVzPjxzYW1sOkVuY3J5cHRlZEFzc2VydGlvbiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDl4bWxlbmMxMSNhZXMyNTYtZ2NtIi8+PGRzOktleUluZm8geG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLW9hZXAtbWdmMXAiLz48eGVuYzpDaXBoZXJEYXRhPjx4ZW5jOkNpcGhlclZhbHVlPkVmdit0VWEzSHVrQksySnMxVkJxZGNHODFhYTZMTWFWL05kcTFrMlJzTzFneDRES3hTNXdlYXkweTA5M3VNblhKWVYzUGRKZkFaczh0V2xwU1d2RStkdmdtc21KOUpid1MvV2VYQTlCVTFHQzBIOWsrMWkra0R3VE9MSU1xdUQydDFCSXBGMXd4aFJ4SVdJdnRRQi8vdVlXYnBMd3Z1cUpQakVnSTR0WkJ5bz08L3hlbmM6Q2lwaGVyVmFsdWU+PC94ZW5jOkNpcGhlckRhdGE+PC94ZW5jOkVuY3J5cHRlZEtleT48L2RzOktleUluZm8+PHhlbmM6Q2lwaGVyRGF0YT48eGVuYzpDaXBoZXJWYWx1ZT4vUFF2VDdmU0pEbkFCMldvRGJZT0ErOUIwRnlSelVuQjd1STFYVnJheEVqVkhCVlE0NE83d2o5YjBrY2VGdmV5MmMrNlc0TlZpblloM0YzUG15MEhjdFV0VTl6RVlsSG02bXFGQTZvV21RVkJ6RG81VXBRcGp1WVl5TnBOMDlkOEdTcEthMzExM25TeFM0K3o4M1JWOEwwcTZ6ajZNeTBMdTkyWkZhZHJpaUJtQlJsQXlTRWdxOTlnVUlQMUsvRXdQbjI3c0E3K21PeVZkWVNTVHZDZjJmZDVLNXRqNDJIcXV4dnlWeDEzeUtlQ0FjNUhQcUhDa0JqTW15YURRNGc2NXQ5TlBlZEtWQzRZdDZmaFQranNiTVFyUlB3VkV6amJWRmIyQ3BOK2k2bDRIbjg4VWdpb1dWaExyZ0l4aUFuRFYxazVrZGVPUFRUQTJ4Ly9zZk9NSE5EaGo2YnYxbkIrNFZUOGtFSm5uZU1XUXU5d2hXanZsUCtlaFRRVTZPK0lpVjBYRTNlbi9TSWZVUTY1KzE3S0EwNFo0VStrcm9ZNENzZU1uRGxySjFVRVBNdEJrUzhyYlRtN3pxMUdFNWNuS0RMUHdmdEt2blpNV1VUcno2c3ZuZ0g0b3M1cnBudkJnWUtUZXhiVXR1bTNCRldhSE0za2V2QlMwV1F4RS9WQ3hQYUFTUy9NbXdQRWZVeVpGMkJOR3R4d2w1RWpMVGRIZjlSOEhiQTVSRjN2WmVqWEtjUWtXZStSN1N6UHJQaVhPYjI3QkZTVmtTRktOTzlyRDFGUlJyR1RQSHFCNlhod1RsTEgxNHprUDVHN2s0bDZZSlRlN3JjSFBiMWVwcngrMDQrbmpUZjFVUTFMK0R1YXJhMHRPeFEyQjVnTDk1bS9yTHBwRGkrUmp6cjRWMTdOSnBPRmszM2VKYlZNNXBjYUFzOUl5NStvOXpOMUlCMlFsWmxpZFdMd1d1WmhmUUsvZmdQb3NObTFqUmNzcXE1WncrejZMMExxcXlHdEg4MEhrdGpGekhUWHB5TU5lV1o5T0hxWGlkUnd1MGJIUlZONTI0Y3VJczZpV1p2UkFlWDZrWW5SRmNSU2owSEhlOHdDSlk2S2ZMZ3k4NE82Y1RPdCtxRjZEajNxVmFncjVsTGExQzhmT3E0NU56bjZvMElhZkt6OENYa0dQYmZrZm1OVnpmUUFMb0VzMERCQ3lWdWlNZ01WUmR5cEdrNzdGeE1nWUdpd2h6bU51RkxzS1JzVTNVNWwxSDZTcno2S0xvd2dsTHg0MG1kMURMeWZGRS9MYml3S0s2SUEwVVNJbTQ4MTJYdFQyS3YyQnlEZ1FyTU9VWXpIL251Qy9rQ1Nrd01pOENDR3k2c3N2VlRrNFRlRFJiZFd2QnJBK2d1YTNIN0NWS2FpdW5tbnNRYzVlT2FYRnNqbnErSCs3SUQxNUVxazRLa09IOVlqMGVKcFcvZWJyMllBUldRNnc3b1pSZFNpY2c4WFcrdmNVV2tLcnlGWmNHUlVSUmpaNC9zalY1REpMZUZseGZvVVBYN3ZLd1RLRDVQWHoxZFh5dDNkc25BYUhxbXpDQWxoQUNWK1lBcll0NGE4SW8zY0JDU2pjdy9HemJ5ak8zK2JZSG85WUtCR2ExS3IzYTlETjBOZW56OUhDRGhmZjU5K3B3Mlh5OEhxWEdBMnZneVRoc3kwcWlLbTlpRmtqS2JtRVFhRi90NmFHa1duOEhEUFJUTnJyZEhJUDJUU3pvaWpKVEtHcTlybmhsSGZaTXhzZktaa1M0c1VCTjhnNE9tR243UzFWNklyU2svQ3Y1Ukx4RG9ZNlAwakpXZUtrYVBTbVRSR0N2R1dGbytNb3VVOGpIZ2dEeFZXNFV3OWEzSnZJMGM0YndSeWY0UjFTR09XVlNHeGE4clVPMlN3Y09aU2RrUnJLV0h0MVJLRytvRWxyMUxJQm4vVmplN0NjUFlvbXc5amVYMmZWT2d4WUpKZkEwYUM2TVhZVEFUbUsvOG9SeXFRdWVJYlJxYjMwbDBMYUtyNWRKUFJIUU02ZFlzUFU5dEpjY3dFOGttNVFXa1RnQXU2L2dDRHBuNXdnM2c4TWwwV216S0sxdW1RZXdqNXE3V09hY3A0QldjdHk3elE3TC9ERXk3dy9qSkFScTZNaDh1MVM3b2M1Qy9iYTY2dGtYb2JJdko5OGs4NDhEY3g0dmdHTndGSm5YSS9YVG1pR0xidjJDWnhKRVZKQlVLN2pUSHVaYW5IVDJoYkdiRzUvSUtUSHpneTJRYjMxbVpPNzRORndXSTNjUjVKK0djajNnc0IyWktGVkMwTEowYlIxbE5PcGpMM3d3SXJwSzRjYnBWR0h4NmZJQzQzbE4zTlBMTUtCMlFaQlJvcGZRODJ4MFJVYzdBdnZBa0FVb0d3ejBjMmVZdW90TVZiaUp2RjY4MkZEMXNpdmNzMmt3WjBoaVM0aEwwS1QzOVBSZ044NG9VZStKZEVrb2hSSU9JT2NycTdpdzA0aXNyWm5oV2VNamxRVFozZE4yV2lIcUlFSndhbGM2anRuVWVRNXJiRmw0ZXZFMnc4aUMxUWtBck5zOVNxeGZKM1E2T1lnU0VZYzdMVlFzYUttV3hkZ1h2YXhmVDMwNDMzbENrZ1ZxbWJnbURHQWtlWUNTVnF0K2pIQVYraERJbE1Ib0llemE3RGN0K1ZTYXQyNlZ2UUlFeU9VYU9VRVhHZllDam5WZWRUS3ovTktwN285c3NyZWRJTEJtdW93RXArN1hVRWJSUHV2SGgzYW44MEp2TmxmRVlhUFB2QUFZWXpobVdXcElpYklyWHlJeEh5dVdsMnpiRzNiOVJIalV6dEVhYVNtZ2ozcHVITlZFZ3YyZ0FiL21WcGNLdFUzajh4ME9XRUJxUnVzMGFhVDlZODN4b3U2U2NVcUpNVGF2RUVaTzRuT2ZDV1NYbSt0UjNrdWxrcFBpMmsyZnVCQk9wVTZmUkFmSWMzZVpCRk5oMm9QcXhMWHkxQy81clVoRExlUHBReFkxK3Y4T3p0MVJqUGFYWmxRbzF1ZzRNRHU0aXU1ZDQ0MTIzZTRlVEhYZ1VMOGJLZk4wMktXdDYrOENMSlpCeVUySzdJUVdLVUhDOHNsNmFWOVMwN1FlcU1CRHRpYVY3NUFTS2R3L0JJMHRqc2NkUkNQNjljdUtOQUkzUzE2cW5OSmttREp2MnVJcW5aS2YvZHN0SHZ0ejRnaC9tQk5GSG0vR1cvaFpLcmQ3eXJ6Sm9KY3ZTL3pyTEJxaERHOW41aExBTEN5cHgvSFVOY2FCQXR6NmlEcEhGTU5pMGVFU0ZqNTB2WUF6aHJtZHYxRStLUGNjZGZRRWlpRjhHMHVEZzc2Zk8wU01WK2lJQzhRWXFJK3lvaVdFZmJDUzJJa1lZRUtCMUpiTXlKSU82dWR6c2hqZ1ppT0djTTliNWRpd3JJT1BTWUw0Q2lOaUF1dTNYQ2paVDBNVkdmL1FjTjJMb1plN0M1Zk9LK1hxUEtpUmtzU21LUTBHUG9PdjNyQ2JqQ2JGK3BpZm5SWDNva1drTThNNnpwMEhvaXpFaXIzb0hHU1g4eDVxQ0FvV1l0TFRJUmtiUzZMNXM3blRkTmNTdWx4RU9uL08zWWg1cXBkbURsUWRvSXkyZ1Y3WjZrdnVrb0NadXM5TEVqcjM3Y09aOEtmVGcwSy9lV3d6dlBHMm1WOVFIQjdwNzdCNEZUZDR0UU9NV29iYlpJOTdLVE0zRURadGd3ZGtGa0xRVzk4Q05CbEFDa01ZR1RVTEduREVONzlFM05RcEVrU3ZJaE44dC95ZVlrU1NFOEdHTklyQml0VmlPV1lMa1loUGVDbGdUZDhmaERWOW5uOXVhSWFvTGU1MEdUZDNPU1dCcEloNlBFTWZHeExvak9CT2o3M3BKbi9sdCszMUtnYzRMb0tkQTV1UXpmdzA4ZEU3UEpPamdXcExhNjM4bFE1VXlhNXVNYVBPMTlSd3g2UGRORUNqdzNqei9nWmFWU0NaYzh5cnpNTS84Y1hvZVNOQStianlMSmNUN3p5VmNKMkpLc3RnUVlpVUl6ajZWLzNVNG82WFdSRnFYSFpjWVlIS2J0eG5NbWpPUnNZODgwWjdwT2J4aUkvcyt2aUh1WmFiRHY1SUhwWTVoOCtybzV6V2ZpK2Y2TlArUjNQYitKNGdJSjVEVDBPTmRmbmVNVW5mMTIvcjF6ODdJeGRRcXAzZDh2YlMrVlpmV1lkR0p6dzRJd0tuRUJMMDYwTnlQQWkxZUZ1eThrOFRZZmQ2Sk5iYWtZY2ZTZElad3NPb1dPNEtHeHRGQnNBMDY3NlREY2JwcTE5ZG9tdUNaZUtmQ21vd25ja3FXRW9RNjgzUUk0VmFRWnRUOHpLSWVZMG1obHZhVkYxUGlXemtZT01SbDhNWlN0WnhlenFuR1pBSjJ2d2VlbkoxQXpwZFJUUnoxbk1wa0UvZG5GTzM1SGozeXBIUGpxR2Z5bHVXVytWUXFBQUcydjdRcFV5SmVyM2E4U3lJcFp3dXJsMmRQcXlRZVhJWkhhMEFkR3dzNUtOQS9uZEV6SE1PTTlrWENuRDVzaDNZd1FzWkZXQ0piQnBRVjc2dHFaaDMraGluZ1dCT2FTUnJJbUZPcGhWazcydVJ0emxiRStMWXpQV21JMW1wbEY1NWpHVk9PNnJCbTAyeVRoZy9XYVpPcm4rMklIUkxJQmkrTHNQa1A1Vm9IR0xxSUVqWXpUWE1DN1Zkc251aGhNN0xkbG9zbmU0UzRxMzBwN0gxL2VVS1pVVWdsSVZPZ3VQaFF4SGVlV1dnUHNRV2dNNEFKOHRPM3dNc0htS0MxTklxcVNSbnZLell5b2VnWVE0NEpxblBQTHg3aEJ2bTI5ZGgxRUNIRUF1dnNOd3RLbmtUczRRdStRa0JITjJrZmlYUHQxbzNQelR6S1pkNDJPVElGbWhaQXRLcEljWEZpMi95T2p4RTd4d28xcjlURzB6TE84UjN5MHRJdU1KQlE5WjMvWmV1SDBWSWN2dkpiSnNQZ2pQWlBSamtIRXJ3VW1jYm16eVpUdUhzaXFUenZDS2tJc1VUMlo5dXRCdllhaC9sUEpwZTNOcnJncmdNU0NNZ0tBWjZEUlZjY2R4UkFTcVU1eWdzTmlWaThrc05uZkpNb1EyQlBnS044Rjd4akczQ29YeUdvVjQ2dTFFTFJZajdvT0FMTTQvN2JlWCt3WFhFRUEwMzBNUlR2NXdZSDdkb1ZpZ2FvSEZoOHNKZzJVMkJHVHdnMG04KzVvMXROM3RsZz08L3hlbmM6Q2lwaGVyVmFsdWU+PC94ZW5jOkNpcGhlckRhdGE+PC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZEFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==', + ]; + $request = new ServerRequest('POST', 'http://tnyholm.se'); + self::$validSolicitedResponseAsymmetricEncryptedSignedResponse = $request->withParsedBody($q); + } + + + /** + * test that an unsolicited samlp:Response is refused by default + */ + public function testUnsolicitedResponseIsRefusedByDefault(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + ); + + $this->expectException(RequestDeniedException::class); + $this->expectExceptionMessage("Unsolicited responses are denied by configuration."); + $serviceProvider->receiveResponse(self::$validUnsolicitedResponse); + } + + + /** + * test that an unsolicited samlp:Response is allowed by config + */ + public function testUnsolicitedResponseIsAllowedByConfig(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + enableUnsolicited: true, + ); + + $response = $serviceProvider->receiveResponse(self::$validUnsolicitedResponse); + $this->assertInstanceOf(Response::class, $response); + } + + + /** + * test that samlp:Response can be received when verification is bypassed. + */ + public function testResponseParsingBypassVerification(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + bypassResponseVerification: true, + ); + $response = $serviceProvider->receiveResponse(self::$validResponse); + $this->assertInstanceOf(Response::class, $response); + } + + + /** + * test that samlp:Response can be received when verification is enabled, but validation is bypassed. + */ + #[Depends('testResponseParsingBypassVerification')] + public function testResponseParsingBypassValidation(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + bypassResponseVerification: false, + bypassConstraintValidation: true, + ); + + $state = [ + 'ExpectedIssuer' => 'https://simplesamlphp.org/idp/metadata', + ]; + + $stateProvider = new MockStateProvider(); + $stateProvider::saveState($state, 'saml:sp:sso'); + $serviceProvider->setStateProvider($stateProvider); + + $response = $serviceProvider->receiveResponse(self::$validResponse); + $this->assertInstanceOf(Response::class, $response); + } + + + /** + * test that samlp:Response can be received when both verification and validation are enabled. + */ + #[Depends('testResponseParsingBypassValidation')] + public function testResponseParsingFull(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + bypassResponseVerification: false, + bypassConstraintValidation: false, + ); + + $state = [ + 'ExpectedIssuer' => 'https://simplesamlphp.org/idp/metadata', + ]; + + $stateProvider = new MockStateProvider(); + $stateProvider::saveState($state, 'saml:sp:sso'); + $serviceProvider->setStateProvider($stateProvider); + + $response = $serviceProvider->receiveResponse(self::$validResponse); + $this->assertInstanceOf(Response::class, $response); + } + + + /** + * test that samlp:Response with symmetric encrypted signed assertion + * can be received when both verification and validation are enabled. + */ +// #[Depends('testResponseParsingBypassValidation')] + public function testResponseParsingFullAsymmetricEncryptedSignedAssertion(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + ); + + $state = [ + 'ExpectedIssuer' => 'https://simplesamlphp.org/idp/metadata', + ]; + + $stateProvider = new MockStateProvider(); + $stateProvider::saveState($state, 'saml:sp:sso'); + $serviceProvider->setStateProvider($stateProvider); + + $response = $serviceProvider->receiveResponse(self::$validSolicitedResponseAsymmetricEncryptedSignedResponse); + $this->assertInstanceOf(Response::class, $response); + } + + + /** + * test that a message from an unexpected issuer is refused + */ + #[Depends('testResponseParsingBypassVerification')] + public function testUnknownIssuerThrowsException(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([self::$idpMetadata]), + spMetadata: self::$spMetadata, + ); + + $state = [ + 'ExpectedIssuer' => 'urn:x-simplesamlphp:sp', + ]; + + $stateProvider = new MockStateProvider(); + $stateProvider::saveState($state, 'saml:sp:sso'); + $serviceProvider->setStateProvider($stateProvider); + + $this->expectException(ResourceNotRecognizedException::class); + $this->expectExceptionMessage("Issuer doesn't match the one the AuthnRequest was sent to."); + + $serviceProvider->receiveResponse(self::$validResponse); + } + + + /** + * test that a message from an entity is refused + */ + public function testUnknownEntityThrowsException(): void + { + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([]), + spMetadata: self::$spMetadata, + ); + + $this->expectException(MetadataNotFoundException::class); + $this->expectExceptionMessage( + "No metadata found for remote entity with entityID: https://simplesamlphp.org/idp/metadata", + ); + + $serviceProvider->receiveResponse(self::$validResponse); + } + + + /** + * test that verifying a Response with the wrong key throws a SignatureVerificationFailedException. + */ + public function testWrongKeyThrowsException(): void + { + $idpMetadata = new Metadata\IdentityProvider( + entityId: 'https://simplesamlphp.org/idp/metadata', + validatingKeys: [ + PEMCertificatesMock::getPublicKey(PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY), + ], + ); + + $serviceProvider = new Entity\ServiceProvider( + metadataProvider: new MockMetadataProvider([$idpMetadata]), + spMetadata: self::$spMetadata, + ); + + $this->expectException(SignatureVerificationFailedException::class); + $this->expectExceptionMessage('Signature verification failed.'); + + $serviceProvider->receiveResponse(self::$validResponse); + } +} diff --git a/tests/SAML2/MockMetadataProvider.php b/tests/SAML2/MockMetadataProvider.php new file mode 100644 index 000000000..68faa5740 --- /dev/null +++ b/tests/SAML2/MockMetadataProvider.php @@ -0,0 +1,86 @@ +entities = $entities; + } + + + /** + * Find IdP-metadata based on a SHA-1 hash of the entityID. Return `null` if not found. + */ + public function getIdPMetadataForSha1(string $hash): ?IdentityProvider + { + foreach ($this->entities as $entity) { + if (($entity instanceof IdentityProvider) && ($hash === sha1($entity->getEntityId()))) { + return $entity; + } + } + + return null; + } + + + /** + * Find SP-metadata based on a SHA-1 hash of the entityID. Return `null` if not found. + */ + public function getSPMetadataForSha1(string $hash): ?ServiceProvider + { + foreach ($this->entities as $entity) { + if (($entity instanceof ServiceProvider) && ($hash === sha1($entity->getEntityId()))) { + return $entity; + } + } + + return null; + } + + + /** + * Find IdP-metadata based on an entityID. Return `null` if not found. + */ + public function getIdPMetadata(string $entityId): ?IdentityProvider + { + foreach ($this->entities as $entity) { + if (($entity instanceof IdentityProvider) && ($entityId === $entity->getEntityId())) { + return $entity; + } + } + + return null; + } + + + /** + * Find SP-metadata based on an entityID. Return `null` if not found. + */ + public function getSPMetadata(string $entityId): ?ServiceProvider + { + foreach ($this->entities as $entity) { + if (($entity instanceof ServiceProvider) && ($entityId === $entity->getEntityId())) { + return $entity; + } + } + + return null; + } +} diff --git a/tests/SAML2/MockStateProvider.php b/tests/SAML2/MockStateProvider.php new file mode 100644 index 000000000..2bba0b6d2 --- /dev/null +++ b/tests/SAML2/MockStateProvider.php @@ -0,0 +1,52 @@ +setBlacklistedAlgorithms(null); +ContainerSingleton::setContainer($container); + +$assertionSigner = (new SignatureAlgorithmFactory())->getAlgorithm( + C::SIG_RSA_SHA256, + PEMCertificatesMock::getPrivateKey(PEMCertificatesMock::OTHER_PRIVATE_KEY), +); + +$document = DOMDocumentFactory::fromFile(dirname(__FILE__, 2) . '/resources/xml/saml_Assertion.xml'); +$unsignedAssertion = Assertion::fromXML($document->documentElement); +$unsignedAssertion->sign($assertionSigner); + +$signedAssertion = $unsignedAssertion->toXML(); +$signedAssertion = Assertion::fromXML($signedAssertion); + +$encryptor = (new KeyTransportAlgorithmFactory())->getAlgorithm( + C::KEY_TRANSPORT_OAEP_MGF1P, + PEMCertificatesMock::getPublicKey(PEMCertificatesMock::SELFSIGNED_PUBLIC_KEY), +); +$encryptedAssertion = new EncryptedAssertion($signedAssertion->encrypt($encryptor)); +$unsignedResponse = new Response( + status: new Status(new StatusCode(C::STATUS_SUCCESS)), + issuer: new Issuer('https://simplesamlphp.org/idp/metadata'), + issueInstant: new DateTimeImmutable('now', new DateTimeZone('Z')), + id: 'abc123', + inResponseTo: 'PHPUnit', + destination: C::ENTITY_OTHER, + consent: C::ENTITY_SP, + assertions: [$encryptedAssertion], +); + +$responseSigner = (new SignatureAlgorithmFactory())->getAlgorithm( + C::SIG_RSA_SHA512, + PEMCertificatesMock::getPrivateKey(PEMCertificatesMock::PRIVATE_KEY), +); + +$unsignedResponse->sign($responseSigner); +$signedResponse = $unsignedResponse->toXML(); + +$xmlRepresentation = $signedResponse->ownerDocument->saveXML($signedResponse); +echo $xmlRepresentation . PHP_EOL; +echo base64_encode($xmlRepresentation) . PHP_EOL; diff --git a/tests/resources/xml/saml_Assertion.min.xml b/tests/resources/xml/saml_Assertion.min.xml new file mode 100644 index 000000000..46ca831c9 --- /dev/null +++ b/tests/resources/xml/saml_Assertion.min.xml @@ -0,0 +1 @@ +urn:x-simplesamlphp:issuerSomeNameIDValueSomeOtherNameIDValuehttps://simplesamlphp.org/sp/metadataurn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport111