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..b238525f7 --- /dev/null +++ b/src/XML/aslo/SupportsAsynchronousTrait.php @@ -0,0 +1,40 @@ +supportsAsynchronous; + } + + + /** + * @param array<\SimpleSAML\XML\Attribute> $namespacedAttributes + */ + private function setSupportsAsynchronous(array $namespacedAttributes = []): void + { + foreach ($namespacedAttributes as $attr) { + if ($attr->getNamespaceURI() === C::NS_ASLO && $attr->getAttrName() === 'supportsAsynchronous') { + $this->supportsAsynchronous = BooleanValue::fromString($attr->getAttrValue()->getValue()); + return; + } + } + } +} diff --git a/src/XML/md/SingleLogoutService.php b/src/XML/md/SingleLogoutService.php index 6cfcf1842..eb4e0cd4f 100644 --- a/src/XML/md/SingleLogoutService.php +++ b/src/XML/md/SingleLogoutService.php @@ -4,11 +4,42 @@ namespace SimpleSAML\SAML2\XML\md; +use SimpleSAML\SAML2\Type\SAMLAnyURIValue; +use SimpleSAML\SAML2\XML\aslo\SupportsAsynchronousTrait; +use SimpleSAML\XML\SchemaValidatableElementInterface; +use SimpleSAML\XML\SchemaValidatableElementTrait; + /** * SingleLogoutService element of type EndpointType * * @package simplesamlphp/saml2 */ -final class SingleLogoutService extends AbstractEndpointType +final class SingleLogoutService extends AbstractEndpointType implements SchemaValidatableElementInterface { + use SchemaValidatableElementTrait; + 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\XML\ElementInterface[] $children + * @param array<\SimpleSAML\XML\Attribute> $attributes + * + * @throws \SimpleSAML\Assert\AssertionFailedException + */ + public function __construct( + protected SAMLAnyURIValue $binding, + protected SAMLAnyURIValue $location, + protected ?SAMLAnyURIValue $responseLocation = null, + array $children = [], + array $attributes = [], + ) { + $this->setSupportsAsynchronous($attributes); + + parent::__construct($binding, $location, $responseLocation, $children, $attributes); + } } diff --git a/src/XML/samlp/Extensions.php b/src/XML/samlp/Extensions.php index 5f7884e5d..d6534d1d7 100644 --- a/src/XML/samlp/Extensions.php +++ b/src/XML/samlp/Extensions.php @@ -9,6 +9,7 @@ use SimpleSAML\SAML2\Utils\XPath; use SimpleSAML\SAML2\XML\ExtensionsTrait; use SimpleSAML\XML\Chunk; +use SimpleSAML\XML\Registry\ElementRegistry; use SimpleSAML\XML\SchemaValidatableElementInterface; use SimpleSAML\XML\SchemaValidatableElementTrait; use SimpleSAML\XMLSchema\Exception\InvalidDOMElementException; @@ -63,11 +64,18 @@ public static function fromXML(Dom\Element $xml): static 'Invalid Extensions element \'' . $xml->localName . '\'', InvalidDOMElementException::class, ); + + $registry = ElementRegistry::getInstance(); $ret = []; /** @var \Dom\Element $node */ foreach (XPath::xpQuery($xml, './*', XPath::getXPath($xml)) as $node) { - $ret[] = new Chunk($node); + $result = $registry->getElementHandler($node->namespaceURI, $node->localName); + if ($result !== null) { + $ret[] = $result::fromXML($node); + } else { + $ret[] = new Chunk($node); + } } return new static($ret); 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/SAML2/XML/md/SingleLogoutServiceTest.php b/tests/SAML2/XML/md/SingleLogoutServiceTest.php new file mode 100644 index 000000000..95245d0da --- /dev/null +++ b/tests/SAML2/XML/md/SingleLogoutServiceTest.php @@ -0,0 +1,78 @@ +saveXml(self::$xmlRepresentation->documentElement); + $this->assertNotFalse($expectedXml); + $actualXml = strval($sloep); + + $this->assertXmlStringEqualsXmlString($expectedXml, $actualXml); + } +} diff --git a/tests/SAML2/XML/samlp/AuthnRequestTest.php b/tests/SAML2/XML/samlp/AuthnRequestTest.php index 08fcb58c4..4a4b679be 100644 --- a/tests/SAML2/XML/samlp/AuthnRequestTest.php +++ b/tests/SAML2/XML/samlp/AuthnRequestTest.php @@ -17,6 +17,7 @@ use SimpleSAML\SAML2\Type\SAMLStringValue; use SimpleSAML\SAML2\Utils; use SimpleSAML\SAML2\Utils\XPath; +use SimpleSAML\SAML2\XML\aslo\Asynchronous; use SimpleSAML\SAML2\XML\saml\Audience; use SimpleSAML\SAML2\XML\saml\AudienceRestriction; use SimpleSAML\SAML2\XML\saml\AuthnContextClassRef; @@ -31,6 +32,7 @@ use SimpleSAML\SAML2\XML\samlp\AbstractSamlpElement; use SimpleSAML\SAML2\XML\samlp\AuthnContextComparisonTypeEnum; use SimpleSAML\SAML2\XML\samlp\AuthnRequest; +use SimpleSAML\SAML2\XML\samlp\Extensions; use SimpleSAML\SAML2\XML\samlp\GetComplete; use SimpleSAML\SAML2\XML\samlp\IDPEntry; use SimpleSAML\SAML2\XML\samlp\IDPList; @@ -101,6 +103,10 @@ public function testMarshalling(): void ), ); + $extensions = new Extensions([ + new Asynchronous(), + ]); + $authnRequest = new AuthnRequest( subject: $subject, issuer: new Issuer( @@ -110,6 +116,7 @@ public function testMarshalling(): void id: IDValue::fromString('_2b0226190ca1c22de6f66e85f5c95158'), issueInstant: SAMLDateTimeValue::fromString('2014-09-22T13:42:00Z'), destination: SAMLAnyURIValue::fromString('https://tiqr.stepup.org/idp/profile/saml2/Redirect/SSO'), + extensions: $extensions, ); $expectedXml = self::$xmlRepresentation->saveXml(self::$xmlRepresentation->documentElement); @@ -181,6 +188,11 @@ public function testMarshallingElementOrdering(): void [$requesterId], ); + // Create Extensions + $extensions = new Extensions([ + new Asynchronous(), + ]); + $authnRequest = new AuthnRequest( id: IDValue::fromString('SomeIDValue'), requestedAuthnContext: $rac, @@ -192,27 +204,30 @@ public function testMarshallingElementOrdering(): void SAMLStringValue::fromString('https://gateway.stepup.org/saml20/sp/metadata'), ), scoping: $scoping, + extensions: $extensions, ); $authnRequestElement = $authnRequest->toXML(); // Test for a Subject $xpCache = XPath::getXPath($authnRequestElement); - $authnRequestElements = XPath::xpQuery($authnRequestElement, './saml_assertion:Subject', $xpCache); + $authnRequestElements = XPath::xpQuery($authnRequestElement, './saml_assertion:Issuer', $xpCache); $this->assertCount(1, $authnRequestElements); // Test ordering of AuthnRequest contents /** @var \Dom\Element[] $authnRequestElements */ $authnRequestElements = XPath::xpQuery( $authnRequestElement, - './saml_assertion:Subject/following-sibling::*', + './saml_assertion:Issuer/following-sibling::*', $xpCache, ); - $this->assertCount(4, $authnRequestElements); - $this->assertEquals('samlp:NameIDPolicy', $authnRequestElements[0]->tagName); - $this->assertEquals('saml:Conditions', $authnRequestElements[1]->tagName); - $this->assertEquals('samlp:RequestedAuthnContext', $authnRequestElements[2]->tagName); - $this->assertEquals('samlp:Scoping', $authnRequestElements[3]->tagName); + $this->assertCount(6, $authnRequestElements); + $this->assertEquals('samlp:Extensions', $authnRequestElements[0]->tagName); + $this->assertEquals('saml:Subject', $authnRequestElements[1]->tagName); + $this->assertEquals('samlp:NameIDPolicy', $authnRequestElements[2]->tagName); + $this->assertEquals('saml:Conditions', $authnRequestElements[3]->tagName); + $this->assertEquals('samlp:RequestedAuthnContext', $authnRequestElements[4]->tagName); + $this->assertEquals('samlp:Scoping', $authnRequestElements[5]->tagName); } 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 @@ + diff --git a/tests/resources/xml/md_SingleLogoutService.xml b/tests/resources/xml/md_SingleLogoutService.xml new file mode 100644 index 000000000..871e8766d --- /dev/null +++ b/tests/resources/xml/md_SingleLogoutService.xml @@ -0,0 +1 @@ + diff --git a/tests/resources/xml/samlp_AuthnRequest.xml b/tests/resources/xml/samlp_AuthnRequest.xml index 5eea6f099..6d8970f4c 100644 --- a/tests/resources/xml/samlp_AuthnRequest.xml +++ b/tests/resources/xml/samlp_AuthnRequest.xml @@ -1,5 +1,8 @@ - + https://gateway.stepup.org/saml20/sp/metadata + + + user@example.org