diff --git a/classes/element.registry.php b/classes/element.registry.php index 04b8d47de..c33e56524 100644 --- a/classes/element.registry.php +++ b/classes/element.registry.php @@ -3,6 +3,9 @@ declare(strict_types=1); return [ + 'urn:oasis:names:tc:SAML:2.0:protocol:ext:async-slo' => [ + 'Asynchronous' => '\SimpleSAML\SAML2\XML\aslo\Asynchronous', + ], 'urn:oasis:names:tc:SAML:metadata:algsupport' => [ 'DigestMethod' => '\SimpleSAML\SAML2\XML\alg\DigestMethod', 'SigningMethod' => '\SimpleSAML\SAML2\XML\alg\SigningMethod', diff --git a/resources/schemas/saml-async-slo-v1.0.xsd b/resources/schemas/saml-async-slo-v1.0.xsd new file mode 100644 index 000000000..2a64b8917 --- /dev/null +++ b/resources/schemas/saml-async-slo-v1.0.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/Constants.php b/src/Constants.php index 968fd7817..ac884ff07 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -226,6 +226,11 @@ class Constants extends \SimpleSAML\XMLSecurity\Constants */ public const string NS_ALG = 'urn:oasis:names:tc:SAML:metadata:algsupport'; + /** + * The namespace for the SAML 2 Asynchronous Single Logout Profile Extension + */ + public const string NS_ASLO = 'urn:oasis:names:tc:SAML:2.0:protocol:ext:async-slo'; + /** * The namespace for the ECP protocol. */ diff --git a/src/XML/aslo/AbstractAsloElement.php b/src/XML/aslo/AbstractAsloElement.php new file mode 100644 index 000000000..02fdd7938 --- /dev/null +++ b/src/XML/aslo/AbstractAsloElement.php @@ -0,0 +1,23 @@ +localName, 'Asynchronous', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, Asynchronous::NS, InvalidDOMElementException::class); + + return new static(); + } + + + /** + * Convert this Asynchronous to XML. + */ + public function toXML(?Dom\Element $parent = null): Dom\Element + { + return $this->instantiateParentElement($parent); + } +} diff --git a/src/XML/aslo/SupportsAsynchronousTrait.php b/src/XML/aslo/SupportsAsynchronousTrait.php new file mode 100644 index 000000000..e928a97a3 --- /dev/null +++ b/src/XML/aslo/SupportsAsynchronousTrait.php @@ -0,0 +1,34 @@ +supportsAsynchronous; + } + + + /** + * @param \SimpleSAML\XMLSchema\Type\BooleanValue $supportsAsynchronous|null + */ + private function setSupportsAsynchronous(?BooleanValue $supportsAsynchronous): void + { + $this->supportsAsynchronous = $supportsAsynchronous; + } +} diff --git a/src/XML/md/SingleLogoutService.php b/src/XML/md/SingleLogoutService.php index 6cfcf1842..414a94a76 100644 --- a/src/XML/md/SingleLogoutService.php +++ b/src/XML/md/SingleLogoutService.php @@ -4,6 +4,13 @@ namespace SimpleSAML\SAML2\XML\md; +use Dom; +use SimpleSAML\SAML2\Assert\Assert; +use SimpleSAML\SAML2\Type\SAMLAnyURIValue; +use SimpleSAML\SAML2\XML\aslo\SupportsAsynchronousTrait; +use SimpleSAML\XMLSchema\Exception\InvalidDOMElementException; +use SimpleSAML\XMLSchema\Type\BooleanValue; + /** * SingleLogoutService element of type EndpointType * @@ -11,4 +18,79 @@ */ final class SingleLogoutService extends AbstractEndpointType { + use SupportsAsynchronousTrait; + + + /** + * SingleLogoutService constructor. + * + * @param \SimpleSAML\SAML2\Type\SAMLAnyURIValue $binding + * @param \SimpleSAML\SAML2\Type\SAMLAnyURIValue $location + * @param \SimpleSAML\SAML2\Type\SAMLAnyURIValue|null $responseLocation + * @param \SimpleSAML\XMLSchema\Type\BooleanValue $supportsAsynchronous + * @param \SimpleSAML\XML\ElementInterface[] $children + * @param array<\SimpleSAML\XML\Attribute> $attributes + * + * @throws \SimpleSAML\Assert\AssertionFailedException + */ + public function __construct( + SAMLAnyURIValue $binding, + SAMLAnyURIValue $location, + ?SAMLAnyURIValue $responseLocation = null, + protected ?BooleanValue $supportsAsynchronous = null, + array $children = [], + array $attributes = [], + ) { + $this->setSupportsAsynchronous($supportsAsynchronous); + + parent::__construct($binding, $location, $responseLocation, $children, $attributes); + } + + + /** + * Initialize a SingleLogoutService. + * + * @param \Dom\Element $xml The XML element we should load. + * + * @throws \SimpleSAML\XMLSchema\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XMLSchema\Exception\MissingAttributeException + * if the supplied element is missing any of the mandatory attributes + */ + public static function fromXML(Dom\Element $xml): static + { + $qualifiedName = static::getClassName(static::class); + Assert::eq( + $xml->localName, + $qualifiedName, + 'Unexpected name for endpoint: ' . $xml->localName . '. Expected: ' . $qualifiedName . '.', + InvalidDOMElementException::class, + ); + + return new static( + self::getAttribute($xml, 'Binding', SAMLAnyURIValue::class), + self::getAttribute($xml, 'Location', SAMLAnyURIValue::class), + self::getOptionalAttribute($xml, 'ResponseLocation', SAMLAnyURIValue::class, null), + self::getOptionalAttribute($xml, 'supportsAsynchronous', BooleanValue::class, null), + self::getChildElementsFromXML($xml), + self::getAttributesNSFromXML($xml), + ); + } + + + /** + * Add this endpoint to an XML element. + * + * @param \Dom\Element $parent The element we should append this endpoint to. + */ + public function toXML(?Dom\Element $parent = null): Dom\Element + { + $e = parent::toXML($parent); + + if ($this->getSupportsAsynchronous() !== null) { + $e->setAttribute('supportsAsynchronous', $this->getSupportsAsynchronous()->getValue()); + } + + return $e; + } } diff --git a/tests/SAML2/XML/aslo/AsynchronousTest.php b/tests/SAML2/XML/aslo/AsynchronousTest.php new file mode 100644 index 000000000..09e7609b8 --- /dev/null +++ b/tests/SAML2/XML/aslo/AsynchronousTest.php @@ -0,0 +1,57 @@ +saveXml(self::$xmlRepresentation->documentElement); + $this->assertNotFalse($expectedXml); + $actualXml = strval($asynchronous); + + $this->assertXmlStringEqualsXmlString($expectedXml, $actualXml); + } +} diff --git a/tests/resources/xml/aslo_Asynchronous.xml b/tests/resources/xml/aslo_Asynchronous.xml new file mode 100644 index 000000000..d4dcb6243 --- /dev/null +++ b/tests/resources/xml/aslo_Asynchronous.xml @@ -0,0 +1 @@ +