diff --git a/composer.json b/composer.json index 0b8c90765..3e40b1cc9 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "psr/log": "^2.0 || ^3.0", "simplesamlphp/xml-common": "^1.11.2", "simplesamlphp/xml-security": "^1.6.5", - "simplesamlphp/xml-soap": "^1.2.0", + "simplesamlphp/xml-soap": "^1.2.1", "simplesamlphp/assert": "^1.0.4" }, "require-dev": { diff --git a/src/SAML2/SOAP.php b/src/SAML2/SOAP.php index 5000b03fc..7ef5e16c5 100644 --- a/src/SAML2/SOAP.php +++ b/src/SAML2/SOAP.php @@ -47,7 +47,7 @@ public function getOutputToSend(AbstractMessage $message) // containing another message (e.g. a Response), however in the ECP // profile, this is the Response itself. if ($message instanceof SAML2_Response) { - $requestAuthenticated = new RequestAuthenticated(1); + $requestAuthenticated = new RequestAuthenticated(true); $destination = $this->destination ?: $message->getDestination(); if ($destination === null) { diff --git a/src/SAML2/XML/ecp/AbstractEcpElement.php b/src/SAML2/XML/ecp/AbstractEcpElement.php index 36cd71ada..1e38221b7 100644 --- a/src/SAML2/XML/ecp/AbstractEcpElement.php +++ b/src/SAML2/XML/ecp/AbstractEcpElement.php @@ -11,6 +11,8 @@ * Abstract class to be implemented by all the classes in this namespace * * @package simplesamlphp/saml2 + * + * @see http://docs.oasis-open.org/security/saml/Post2.0/saml-ecp/v2.0/saml-ecp-v2.0.html */ abstract class AbstractEcpElement extends AbstractElement { diff --git a/src/SAML2/XML/ecp/RelayState.php b/src/SAML2/XML/ecp/RelayState.php new file mode 100644 index 000000000..80ee6ec20 --- /dev/null +++ b/src/SAML2/XML/ecp/RelayState.php @@ -0,0 +1,105 @@ +setContent($relayState); + } + + + /** + * Convert XML into a RelayState + * + * @param \DOMElement $xml The XML element we should load + * @return static + * + * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XML\Exception\MissingAttributeException + * if the supplied element is missing any of the mandatory attributes + */ + public static function fromXML(DOMElement $xml): static + { + Assert::same($xml->localName, 'RelayState', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, RelayState::NS, InvalidDOMElementException::class); + + // Assert required attributes + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'actor'), + 'Missing env:actor attribute in .', + MissingAttributeException::class, + ); + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'), + 'Missing env:mustUnderstand attribute in .', + MissingAttributeException::class, + ); + + $mustUnderstand = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'); + Assert::same( + $mustUnderstand, + '1', + 'Invalid value of env:mustUnderstand attribute in .', + ProtocolViolationException::class, + ); + + $actor = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'actor'); + Assert::same( + $actor, + C::SOAP_ACTOR_NEXT, + 'Invalid value of env:actor attribute in .', + ProtocolViolationException::class, + ); + + return new static($xml->textContent); + } + + + /** + * Convert this ECP RelayState to XML. + * + * @param \DOMElement|null $parent The element we should append this element to. + * @return \DOMElement + */ + public function toXML(DOMElement $parent = null): DOMElement + { + $e = $this->instantiateParentElement($parent); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:mustUnderstand', '1'); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:actor', C::SOAP_ACTOR_NEXT); + $e->textContent = $this->getContent(); + + return $e; + } +} diff --git a/src/SAML2/XML/ecp/Request.php b/src/SAML2/XML/ecp/Request.php new file mode 100644 index 000000000..1ea1da605 --- /dev/null +++ b/src/SAML2/XML/ecp/Request.php @@ -0,0 +1,178 @@ +isPassive; + } + + + /** + * Collect the value of the providerName-property + * + * @return string|null + */ + public function getProviderName(): ?string + { + return $this->providerName; + } + + + /** + * Collect the value of the issuer-property + * + * @return \SimpleSAML\SAML2\XML\saml\Issuer + */ + public function getIssuer(): Issuer + { + return $this->issuer; + } + /** + * Collect the value of the idpList-property + * + * @return \SimpleSAML\SAML2\XML\samlp\IDPList|null + */ + public function getIDPList(): ?IDPList + { + return $this->idpList; + } + + + /** + * Convert XML into a Request + * + * @param \DOMElement $xml The XML element we should load + * @return static + * + * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XML\Exception\MissingAttributeException + * if the supplied element is missing any of the mandatory attributes + */ + public static function fromXML(DOMElement $xml): static + { + Assert::same($xml->localName, 'Request', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, Request::NS, InvalidDOMElementException::class); + + // Assert required attributes + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'actor'), + 'Missing env:actor attribute in .', + MissingAttributeException::class, + ); + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'), + 'Missing env:mustUnderstand attribute in .', + MissingAttributeException::class, + ); + + $mustUnderstand = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'); + Assert::same( + $mustUnderstand, + '1', + 'Invalid value of env:mustUnderstand attribute in .', + ProtocolViolationException::class, + ); + + $actor = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'actor'); + Assert::same( + $actor, + C::SOAP_ACTOR_NEXT, + 'Invalid value of env:actor attribute in .', + ProtocolViolationException::class, + ); + + $issuer = Issuer::getChildrenOfClass($xml); + Assert::count( + $issuer, + 1, + 'More than one in .', + TooManyElementsException::class, + ); + + $idpList = IDPList::getChildrenOfClass($xml); + + return new static( + array_pop($issuer), + array_pop($idpList), + self::getOptionalAttribute($xml, 'ProviderName', null), + self::getOptionalBooleanAttribute($xml, 'IsPassive', null), + ); + } + + + /** + * Convert this ECP SubjectConfirmation to XML. + * + * @param \DOMElement|null $parent The element we should append this element to. + * @return \DOMElement + */ + public function toXML(DOMElement $parent = null): DOMElement + { + $e = $this->instantiateParentElement($parent); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:mustUnderstand', '1'); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:actor', C::SOAP_ACTOR_NEXT); + + if ($this->getProviderName() !== null) { + $e->setAttribute('ProviderName', $this->getProviderName()); + } + + if ($this->getIsPassive() !== null) { + $e->setAttribute('IsPassive', strval(intval($this->getIsPassive()))); + } + + $this->getIssuer()->toXML($e); + $this->getIDPList()?->toXML($e); + + return $e; + } +} diff --git a/src/SAML2/XML/ecp/RequestAuthenticated.php b/src/SAML2/XML/ecp/RequestAuthenticated.php index 939d00983..8708228a9 100644 --- a/src/SAML2/XML/ecp/RequestAuthenticated.php +++ b/src/SAML2/XML/ecp/RequestAuthenticated.php @@ -11,6 +11,7 @@ use SimpleSAML\XML\Exception\InvalidDOMElementException; use SimpleSAML\XML\Exception\MissingAttributeException; +use function boolval; use function is_null; use function is_numeric; use function strval; @@ -25,26 +26,20 @@ final class RequestAuthenticated extends AbstractEcpElement /** * Create a ECP RequestAuthenticated element. * - * @param int|null $mustUnderstand + * @param bool $mustUnderstand */ public function __construct( - protected ?int $mustUnderstand, + protected bool $mustUnderstand ) { - Assert::oneOf( - $mustUnderstand, - [null, 0, 1], - 'Invalid value of env:mustUnderstand attribute in .', - ProtocolViolationException::class, - ); } /** * Collect the value of the mustUnderstand-property * - * @return int|null + * @return bool */ - public function getMustUnderstand(): ?int + public function getMustUnderstand(): bool { return $this->mustUnderstand; } @@ -78,20 +73,18 @@ public static function fromXML(DOMElement $xml): static Assert::oneOf( $mustUnderstand, - ['', '0', '1'], - 'Invalid value of env:mustUnderstand attribute in .', + ['0', '1'], + 'Invalid value of env:mustUnderstand attribute in .', ProtocolViolationException::class, ); Assert::same( $actor, 'http://schemas.xmlsoap.org/soap/actor/next', - 'Invalid value of env:actor attribute in .', + 'Invalid value of env:actor attribute in .', ProtocolViolationException::class, ); - $mustUnderstand = ($mustUnderstand === '') ? null : intval($mustUnderstand); - - return new static($mustUnderstand); + return new static(boolval($mustUnderstand)); } @@ -103,13 +96,10 @@ public static function fromXML(DOMElement $xml): static */ public function toXML(DOMElement $parent = null): DOMElement { - $response = $this->instantiateParentElement($parent); - - if ($this->getMustUnderstand() !== null) { - $response->setAttributeNS(C::NS_SOAP_ENV_11, 'env:mustUnderstand', strval($this->getMustUnderstand())); - } - $response->setAttributeNS(C::NS_SOAP_ENV_11, 'env:actor', 'http://schemas.xmlsoap.org/soap/actor/next'); + $e = $this->instantiateParentElement($parent); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:mustUnderstand', strval(intval($this->getMustUnderstand()))); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:actor', C::SOAP_ACTOR_NEXT); - return $response; + return $e; } } diff --git a/src/SAML2/XML/ecp/SubjectConfirmation.php b/src/SAML2/XML/ecp/SubjectConfirmation.php new file mode 100644 index 000000000..03ca304d1 --- /dev/null +++ b/src/SAML2/XML/ecp/SubjectConfirmation.php @@ -0,0 +1,142 @@ +method; + } + + + /** + * Collect the value of the subjectConfirmationData-property + * + * @return \SimpleSAML\SAML2\XML\saml\SubjectConfirmationData|null + */ + public function getSubjectConfirmationData(): ?SubjectConfirmationData + { + return $this->subjectConfirmationData; + } + + + /** + * Convert XML into a SubjectConfirmation + * + * @param \DOMElement $xml The XML element we should load + * @return static + * + * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XML\Exception\MissingAttributeException + * if the supplied element is missing any of the mandatory attributes + */ + public static function fromXML(DOMElement $xml): static + { + Assert::same($xml->localName, 'SubjectConfirmation', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, SubjectConfirmation::NS, InvalidDOMElementException::class); + + // Assert required attributes + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'actor'), + 'Missing env:actor attribute in .', + MissingAttributeException::class, + ); + Assert::true( + $xml->hasAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'), + 'Missing env:mustUnderstand attribute in .', + MissingAttributeException::class, + ); + + $mustUnderstand = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'mustUnderstand'); + Assert::same( + $mustUnderstand, + '1', + 'Invalid value of env:mustUnderstand attribute in .', + ProtocolViolationException::class, + ); + + $actor = $xml->getAttributeNS(C::NS_SOAP_ENV_11, 'actor'); + Assert::same( + $actor, + C::SOAP_ACTOR_NEXT, + 'Invalid value of env:actor attribute in .', + ProtocolViolationException::class, + ); + + $subjectConfirmationData = SubjectConfirmationData::getChildrenOfClass($xml); + Assert::maxCount( + $subjectConfirmationData, + 1, + 'More than one in .', + TooManyElementsException::class, + ); + + return new static( + self::getAttribute($xml, 'Method'), + array_pop($subjectConfirmationData), + ); + } + + + /** + * Convert this ECP SubjectConfirmation to XML. + * + * @param \DOMElement|null $parent The element we should append this element to. + * @return \DOMElement + */ + public function toXML(DOMElement $parent = null): DOMElement + { + $e = $this->instantiateParentElement($parent); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:mustUnderstand', '1'); + $e->setAttributeNS(C::NS_SOAP_ENV_11, 'env:actor', C::SOAP_ACTOR_NEXT); + $e->setAttribute('Method', $this->getMethod()); + + $this->getSubjectConfirmationData()?->toXML($e); + + return $e; + } +} diff --git a/tests/SAML2/SOAPTest.php b/tests/SAML2/SOAPTest.php index 8f8a5d312..ca5f7ee73 100644 --- a/tests/SAML2/SOAPTest.php +++ b/tests/SAML2/SOAPTest.php @@ -116,7 +116,7 @@ public function testSendResponse(): void SOAP ); - $requestAuthenticated = new RequestAuthenticated(1); + $requestAuthenticated = new RequestAuthenticated(true); $ecpResponse = new Response('https://example.org/metadata'); diff --git a/tests/SAML2/XML/ecp/RelayStateTest.php b/tests/SAML2/XML/ecp/RelayStateTest.php new file mode 100644 index 000000000..2ae7be3b9 --- /dev/null +++ b/tests/SAML2/XML/ecp/RelayStateTest.php @@ -0,0 +1,101 @@ +assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($relayState), + ); + } + + + /** + */ + public function testUnmarshalling(): void + { + $relayState = RelayState::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($relayState), + ); + } + + + /** + */ + public function testUnmarshallingWithMissingMustUnderstandThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'mustUnderstand'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:mustUnderstand attribute in .'); + + RelayState::fromXML($document); + } + + + /** + */ + public function testUnmarshallingWithMissingActorThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'actor'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:actor attribute in .'); + + RelayState::fromXML($document); + } +} diff --git a/tests/SAML2/XML/ecp/RequestAuthenticatedTest.php b/tests/SAML2/XML/ecp/RequestAuthenticatedTest.php index 80a65a63c..ec2e0db70 100644 --- a/tests/SAML2/XML/ecp/RequestAuthenticatedTest.php +++ b/tests/SAML2/XML/ecp/RequestAuthenticatedTest.php @@ -45,7 +45,7 @@ public static function setUpBeforeClass(): void */ public function testMarshalling(): void { - $ra = new RequestAuthenticated(0); + $ra = new RequestAuthenticated(false); $this->assertEquals( self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), diff --git a/tests/SAML2/XML/ecp/RequestTest.php b/tests/SAML2/XML/ecp/RequestTest.php new file mode 100644 index 000000000..16d11b789 --- /dev/null +++ b/tests/SAML2/XML/ecp/RequestTest.php @@ -0,0 +1,117 @@ +assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($request), + ); + } + + + /** + */ + public function testUnmarshalling(): void + { + $request = Request::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($request), + ); + } + + + /** + */ + public function testUnmarshallingWithMissingMustUnderstandThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'mustUnderstand'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:mustUnderstand attribute in .'); + + Request::fromXML($document); + } + + + /** + */ + public function testUnmarshallingWithMissingActorThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'actor'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:actor attribute in .'); + + Request::fromXML($document); + } +} diff --git a/tests/SAML2/XML/ecp/SubjectConfirmationTest.php b/tests/SAML2/XML/ecp/SubjectConfirmationTest.php new file mode 100644 index 000000000..5a25fb1b5 --- /dev/null +++ b/tests/SAML2/XML/ecp/SubjectConfirmationTest.php @@ -0,0 +1,138 @@ +Arbitrary Element'); + + $attr1 = new XMLAttribute('urn:test:something', 'test', 'attr1', 'testval1'); + $attr2 = new XMLAttribute('urn:test:something', 'test', 'attr2', 'testval2'); + + $subjectConfirmationData = new SubjectConfirmationData( + new DateTimeImmutable('2001-04-19T04:25:21Z'), + new DateTimeImmutable('2009-02-13T23:31:30Z'), + C::ENTITY_SP, + 'SomeRequestID', + '127.0.0.1', + [ + new KeyInfo([new KeyName('SomeKey')]), + new Chunk($arbitrary->documentElement), + ], + [$attr1, $attr2] + ); + + $subjectConfirmation = new SubjectConfirmation(C::CM_BEARER, $subjectConfirmationData); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($subjectConfirmation), + ); + } + + + /** + */ + public function testUnmarshalling(): void + { + $subjectConfirmation = SubjectConfirmation::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($subjectConfirmation), + ); + } + + + /** + */ + public function testUnmarshallingWithMissingMustUnderstandThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'mustUnderstand'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:mustUnderstand attribute in .'); + + SubjectConfirmation::fromXML($document); + } + + + /** + */ + public function testUnmarshallingWithMissingActorThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttributeNS(SOAP::NS_SOAP_ENV_11, 'actor'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing env:actor attribute in .'); + + SubjectConfirmation::fromXML($document); + } + + + /** + */ + public function testUnmarshallingWithMissingMethodThrowsException(): void + { + $document = clone self::$xmlRepresentation->documentElement; + $document->removeAttribute('Method'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage('Missing \'Method\' attribute on ecp:SubjectConfirmation.'); + + SubjectConfirmation::fromXML($document); + } +} diff --git a/tests/resources/xml/ecp_RelayState.xml b/tests/resources/xml/ecp_RelayState.xml new file mode 100644 index 000000000..d008c399a --- /dev/null +++ b/tests/resources/xml/ecp_RelayState.xml @@ -0,0 +1 @@ +AGDY854379dskssda diff --git a/tests/resources/xml/ecp_Request.xml b/tests/resources/xml/ecp_Request.xml new file mode 100644 index 000000000..819c75e28 --- /dev/null +++ b/tests/resources/xml/ecp_Request.xml @@ -0,0 +1,8 @@ + + TheIssuerValue + + + + https://some/location + + diff --git a/tests/resources/xml/ecp_SubjectConfirmation.xml b/tests/resources/xml/ecp_SubjectConfirmation.xml new file mode 100644 index 000000000..aa3465f75 --- /dev/null +++ b/tests/resources/xml/ecp_SubjectConfirmation.xml @@ -0,0 +1,8 @@ + + + + SomeKey + + Arbitrary Element + +