Skip to content

Commit 3fcc690

Browse files
tvdijenthijskh
andauthored
Backport support for IDP Discovery protocol (#2402)
* Backport support for IDP Discovery protocol * Add default DiscoveryResponse endpoints to generated SP metadata --------- Co-authored-by: Thijs Kinkhorst <thijs@kinkhorst.com>
1 parent b849ced commit 3fcc690

9 files changed

Lines changed: 140 additions & 16 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"simplesamlphp/assert": "^1.1",
6565
"simplesamlphp/composer-module-installer": "^1.3",
6666
"simplesamlphp/saml2": "^5@dev",
67-
"simplesamlphp/saml2-legacy": "^4.17",
67+
"simplesamlphp/saml2-legacy": "^4.18.1",
6868
"simplesamlphp/simplesamlphp-assets-base": "~2.3.0",
6969
"simplesamlphp/xml-security": "^1.7",
7070
"symfony/cache": "^6.4",

composer.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
SAML V2.0 Metadata Extensions for Identity Provider Discovery Service Protocol and Profile
2+
=============================
3+
4+
[TOC]
5+
6+
This is a reference for the SimpleSAMLphp implementation of the [SAML
7+
V2.0 Metadata Extensions for Identity Provider Discovery Service Protocol and Profile](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-idp-discovery.pdf)
8+
defined by OASIS.
9+
10+
The metadata extension is available to SP usage of SimpleSAMLphp. The entries are placed inside the relevant
11+
entry in `authsources.php`.
12+
13+
An example:
14+
15+
<?php
16+
$config = [
17+
18+
'default-sp' => [
19+
'saml:SP',
20+
21+
'DiscoveryResponse' => [
22+
[
23+
'index' => 1,
24+
'Binding' => 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol',
25+
'Location' => 'https://simplesamlphp.org/some/endpoint',
26+
'isDefault' => true,
27+
],
28+
],
29+
/* ... */
30+
],
31+
];
32+
33+
Generated XML Metadata Examples
34+
----------------
35+
36+
The example given above will generate the following XML metadata:
37+
38+
<?xml version="1.0"?>
39+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://example.com/saml-idp">
40+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
41+
<md:Extensions>
42+
<idpdisc:DiscoveryResponse xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://simplesamlphp.org/some/endpoint" index="1" isDefault="true" />
43+
</md:Extensions>
44+
<md:KeyDescriptor use="signing">
45+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
46+
<ds:X509Data>
47+
...

modules/saml/docs/sp.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and with entity attributes. See the documentation for those extensions for more
1212
* [MDUI extension](../simplesamlphp-metadata-extensions-ui)
1313
* [MDRPI extension](../simplesamlphp-metadata-extensions-rpi)
1414
* [Attributes extension](../simplesamlphp-metadata-extensions-attributes)
15+
* [DiscoveryResponse extension](../simplesamlphp-metadata-extensions-idpdisc)
1516

1617
**Parameters**:
1718

modules/saml/src/Auth/Source/SP.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public function getHostedMetadata(): array
164164
'metadata-set' => 'saml20-sp-remote',
165165
'SingleLogoutService' => $this->getSLOEndpoints(),
166166
'AssertionConsumerService' => $this->getACSEndpoints(),
167+
'DiscoveryResponse' => $this->getDiscoveryResponseEndpoints(),
167168
];
168169

169170
// add NameIDPolicy
@@ -441,6 +442,19 @@ private function getSLOEndpoints(): array
441442
return $endpoints;
442443
}
443444

445+
/**
446+
* Get the DiscoveryResponse endpoint available for a given local SP.
447+
*/
448+
private function getDiscoveryResponseEndpoints(): array
449+
{
450+
$location = Module::getModuleURL('saml/sp/discoResponse/' . $this->getAuthId());
451+
452+
return [ 0 => [
453+
'Binding' => Constants::NS_IDPDISC,
454+
'Location' => $location,
455+
] ];
456+
}
457+
444458
/**
445459
* Determine if the Request Initiator Protocol is enabled
446460
*

modules/saml/src/IdP/SAML2.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,6 @@ public static function getHostedMetadata(string $entityid, MetaDataStorageHandle
949949
$metadata['saml:Extensions'] = $config->getArray('saml:Extensions');
950950
}
951951

952-
953952
if ($config->hasValue('UIInfo')) {
954953
$metadata['UIInfo'] = $config->getArray('UIInfo');
955954
}

src/SimpleSAML/Metadata/SAMLBuilder.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use DOMElement;
88
use SAML2\Constants;
9+
use SAML2\XML\idpdisc\DiscoveryResponse;
910
use SAML2\XML\md\AttributeAuthorityDescriptor;
1011
use SAML2\XML\md\AttributeConsumingService;
1112
use SAML2\XML\md\ContactPerson;
@@ -202,6 +203,14 @@ private function addExtensions(Configuration $metadata, RoleDescriptor $e): void
202203
);
203204
}
204205

206+
if ($metadata->hasValue('DiscoveryResponse')) {
207+
$discoResponse = self::createEndpoints($metadata->getArray('DiscoveryResponse'), true);
208+
209+
$e->setExtensions(
210+
array_merge($e->getExtensions(), $discoResponse),
211+
);
212+
}
213+
205214
if ($metadata->hasValue('UIInfo')) {
206215
$ui = new UIInfo();
207216
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
@@ -323,7 +332,12 @@ private static function createEndpoints(array $endpoints, bool $indexed): array
323332

324333
foreach ($endpoints as &$ep) {
325334
if ($indexed) {
326-
$t = new IndexedEndpointType();
335+
if ($ep['Binding'] === Constants::NS_IDPDISC) {
336+
$t = new DiscoveryResponse();
337+
} else {
338+
$t = new IndexedEndpointType();
339+
}
340+
327341
if (!isset($ep['index'])) {
328342
// Find the maximum index
329343
$maxIndex = -1;

src/SimpleSAML/Metadata/SAMLParser.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use SAML2\SignedElementHelper;
1414
use SAML2\XML\ds\X509Certificate;
1515
use SAML2\XML\ds\X509Data;
16+
use SAML2\XML\idpdisc\DiscoveryResponse;
1617
use SAML2\XML\md\AttributeAuthorityDescriptor;
1718
use SAML2\XML\md\AttributeConsumingService;
1819
use SAML2\XML\md\ContactPerson;
@@ -507,6 +508,10 @@ private function addExtensions(array &$metadata, array $roleDescriptor): void
507508
}
508509
}
509510

511+
if (!empty($roleDescriptor['DiscoveryResponse'])) {
512+
$metadata['DiscoveryResponse'] = $roleDescriptor['DiscoveryResponse'];
513+
}
514+
510515
if (!empty($roleDescriptor['UIInfo'])) {
511516
$metadata['UIInfo'] = $roleDescriptor['UIInfo'];
512517
}
@@ -737,6 +742,7 @@ private static function parseRoleDescriptorType(RoleDescriptor $element, ?int $e
737742
$ext = self::processExtensions($element);
738743
$ret['scope'] = $ext['scope'];
739744
$ret['EntityAttributes'] = $ext['EntityAttributes'];
745+
$ret['DiscoveryResponse'] = $ext['DiscoveryResponse'];
740746
$ret['UIInfo'] = $ext['UIInfo'];
741747
$ret['DiscoHints'] = $ext['DiscoHints'];
742748

@@ -770,7 +776,6 @@ private static function parseSSODescriptor(SSODescriptorType $element, ?int $exp
770776
// find all ArtifactResolutionService elements
771777
$sd['ArtifactResolutionService'] = self::extractEndpoints($element->getArtifactResolutionService());
772778

773-
774779
// process NameIDFormat elements
775780
$sd['nameIDFormats'] = $element->getNameIDFormat();
776781

@@ -873,11 +878,12 @@ private function processAttributeAuthorityDescriptor(
873878
private static function processExtensions(mixed $element, array $parentExtensions = []): array
874879
{
875880
$ret = [
876-
'scope' => [],
877-
'EntityAttributes' => [],
878-
'RegistrationInfo' => [],
879-
'UIInfo' => [],
880-
'DiscoHints' => [],
881+
'scope' => [],
882+
'EntityAttributes' => [],
883+
'RegistrationInfo' => [],
884+
'DiscoveryResponse' => [],
885+
'UIInfo' => [],
886+
'DiscoHints' => [],
881887
];
882888

883889
// Some extensions may get inherited from a parent element
@@ -955,6 +961,13 @@ private static function processExtensions(mixed $element, array $parentExtension
955961
}
956962
}
957963

964+
// DiscoveryResponse elements only make sense at SPSSODescriptor level extensions
965+
if ($element instanceof SPSSODescriptor) {
966+
if ($e instanceof DiscoveryResponse) {
967+
$ret['DiscoveryResponse'] = array_merge($ret['DiscoveryResponse'], self::extractEndpoints([$e]));
968+
}
969+
}
970+
958971
// UIInfo elements are only allowed at RoleDescriptor level extensions
959972
if ($element instanceof RoleDescriptor) {
960973
if ($e instanceof UIInfo) {

tests/src/SimpleSAML/Metadata/SAMLParserTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use DOMDocument;
88
use PHPUnit\Framework\Attributes\CoversClass;
9+
use SAML2\Constants;
910
use SAML2\DOMDocumentFactory;
1011
use SimpleSAML\Metadata\SAMLParser;
1112
use SimpleSAML\Test\SigningTestCase;
@@ -17,6 +18,41 @@
1718
#[CoversClass(SAMLParser::class)]
1819
class SAMLParserTest extends SigningTestCase
1920
{
21+
/**
22+
* Test that DiscoveryResponse is parsed
23+
*/
24+
public function testDiscoveryResponse(): void
25+
{
26+
$expected = [
27+
0 => [
28+
'index' => 43,
29+
'Binding' => Constants::NS_IDPDISC,
30+
'Location' => 'https://simplesamlphp.org/some/endpoint',
31+
'isDefault' => false,
32+
],
33+
];
34+
35+
$document = DOMDocumentFactory::fromString(
36+
<<<XML
37+
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="theEntityID">
38+
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
39+
<Extensions>
40+
<idpdisc:DiscoveryResponse xmlns:idpdisc="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Binding="urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol" Location="https://simplesamlphp.org/some/endpoint" index="43" isDefault="false" />
41+
</Extensions>
42+
</SPSSODescriptor>
43+
</EntityDescriptor>
44+
XML,
45+
);
46+
47+
$entities = SAMLParser::parseDescriptorsElement($document->documentElement);
48+
$this->assertArrayHasKey('theEntityID', $entities);
49+
// DiscoveryResponse is accessible in the SP metadata accessors
50+
/** @var array $metadata */
51+
$metadata = $entities['theEntityID']->getMetadata20SP();
52+
$this->assertEquals($expected, $metadata['DiscoveryResponse']);
53+
}
54+
55+
2056
/**
2157
* Test Registration Info is parsed
2258
*/

0 commit comments

Comments
 (0)