Skip to content

Commit 1bab69d

Browse files
authored
Enable showing supported claims in OP discovery (#299)
1 parent e48921e commit 1bab69d

File tree

9 files changed

+110
-18
lines changed

9 files changed

+110
-18
lines changed

UPGRADE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ https://openid.net/specs/openid-connect-core-1_0.html#RequestObject
3535
https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
3636

3737
## New configuration options
38-
38+
- (from v6.1) Show `claims_supported` claim in OP Discovery endpoint - you can now choose to show supported claims,
39+
as is recommended by OpenID Connect Discovery specification https://openid.net/specs/openid-connect-discovery-1_0.html.
3940
- (optional) Issuer - you can now override the issuer (OP identifier). If not set, it falls back to current scheme, host
4041
and optionally a port (as in all previous module versions).
4142
- (optional) Protocol caching adapter and its arguments

config/module_oidc.php.dist

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ $config = [
7878
// to use this attribute as the 'sub' claim since it designates unique identifier for the user).
7979
ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE => 'uid',
8080

81-
// The default translate table from SAML attributes to OIDC claims.
81+
// The default translate table from SAML attributes to OIDC claims. If you don't want to support specific default
82+
// claim, set it to an empty array.
8283
ModuleConfig::OPTION_AUTH_SAML_TO_OIDC_TRANSLATE_TABLE => [
8384
/*
8485
* The basic format is
@@ -243,6 +244,11 @@ $config = [
243244
// ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => '0',
244245
ModuleConfig::OPTION_AUTH_FORCED_ACR_VALUE_FOR_COOKIE_AUTHENTICATION => null,
245246

247+
// Choose if OP discovery document will include 'claims_supported' claim, which is recommended per OpenID Connect
248+
// Discovery specification https://openid.net/specs/openid-connect-discovery-1_0.html. The list will include all
249+
// claims for which "SAML attribute to OIDC claim translation" has been defined above.
250+
ModuleConfig::OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED => false,
251+
246252
// Settings regarding Authentication Processing Filters.
247253
// Note: OIDC authN state array will not contain all the keys which are available during SAML authN,
248254
// like Service Provider metadata, etc.

src/ModuleConfig.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class ModuleConfig
8181
final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments';
8282
final public const OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION = 'protocol_user_entity_cache_duration';
8383
final public const OPTION_PROTOCOL_CLIENT_ENTITY_CACHE_DURATION = 'protocol_client_entity_cache_duration';
84+
final public const OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED = 'protocol_discover_show_claims_supported';
8485
final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS =
8586
'federation_participation_limit_by_trust_marks';
8687

@@ -504,6 +505,14 @@ public function getProtocolClientEntityCacheDuration(): DateInterval
504505
);
505506
}
506507

508+
public function getProtocolDiscoveryShowClaimsSupported(): bool
509+
{
510+
return $this->config()->getOptionalBoolean(
511+
self::OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED,
512+
false,
513+
);
514+
}
515+
507516

508517
/*****************************************************************************************************************
509518
* OpenID Federation related config.

src/Services/Container.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,6 @@ public function __construct()
189189
);
190190
$this->services[TemplateFactory::class] = $templateFactory;
191191

192-
$opMetadataService = new OpMetadataService($moduleConfig);
193-
$this->services[OpMetadataService::class] = $opMetadataService;
194-
195-
$metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
196-
$this->services[MetaDataStorageHandler::class] = $metadataStorageHandler;
197-
198192
$claimSetEntityFactory = new ClaimSetEntityFactory();
199193
$this->services[ClaimSetEntityFactory::class] = $claimSetEntityFactory;
200194

@@ -204,6 +198,12 @@ public function __construct()
204198
))->build();
205199
$this->services[ClaimTranslatorExtractor::class] = $claimTranslatorExtractor;
206200

201+
$opMetadataService = new OpMetadataService($moduleConfig, $claimTranslatorExtractor);
202+
$this->services[OpMetadataService::class] = $opMetadataService;
203+
204+
$metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
205+
$this->services[MetaDataStorageHandler::class] = $metadataStorageHandler;
206+
207207
$processingChainFactory = new ProcessingChainFactory($moduleConfig);
208208
$this->services[ProcessingChainFactory::class] = $processingChainFactory;
209209

src/Services/OpMetadataService.php

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

77
use SimpleSAML\Module\oidc\Codebooks\RoutesEnum;
88
use SimpleSAML\Module\oidc\ModuleConfig;
9+
use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor;
910
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
1011
use SimpleSAML\OpenID\Codebooks\TokenEndpointAuthMethodsEnum;
1112

@@ -24,6 +25,7 @@ class OpMetadataService
2425
*/
2526
public function __construct(
2627
private readonly ModuleConfig $moduleConfig,
28+
private readonly ClaimTranslatorExtractor $claimTranslatorExtractor,
2729
) {
2830
$this->initMetadata();
2931
}
@@ -75,6 +77,11 @@ private function initMetadata(): void
7577
}
7678
$this->metadata[ClaimsEnum::BackChannelLogoutSupported->value] = true;
7779
$this->metadata[ClaimsEnum::BackChannelLogoutSessionSupported->value] = true;
80+
81+
if ($this->moduleConfig->getProtocolDiscoveryShowClaimsSupported()) {
82+
$claimsSupported = $this->claimTranslatorExtractor->getSupportedClaims();
83+
$this->metadata[ClaimsEnum::ClaimsSupported->value] = $claimsSupported;
84+
}
7885
}
7986

8087
/**

src/Utils/ClaimTranslatorExtractor.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ClaimTranslatorExtractor
3535
protected array $claimSets = [];
3636

3737
/** @var string[] */
38-
protected array $protectedClaims = ['openid', 'profile', 'email', 'address', 'phone'];
38+
protected array $protectedScopes = ['openid', 'profile', 'email', 'address', 'phone'];
3939

4040
protected array $translationTable = [
4141
'sub' => [
@@ -85,13 +85,13 @@ class ClaimTranslatorExtractor
8585
'preferredLanguage',
8686
],
8787
'updated_at' => [
88-
'type' => 'int',
88+
// 'type' => 'int',
8989
],
9090
'email' => [
9191
'mail',
9292
],
9393
'email_verified' => [
94-
'type' => 'bool',
94+
// 'type' => 'bool',
9595
],
9696
'address' => [
9797
'type' => 'json',
@@ -105,7 +105,7 @@ class ClaimTranslatorExtractor
105105
'homePhone',
106106
],
107107
'phone_number_verified' => [
108-
'type' => 'bool',
108+
// 'type' => 'bool',
109109
// Empty
110110
],
111111
];
@@ -198,7 +198,7 @@ public function addClaimSet(ClaimSetEntityInterface $claimSet): self
198198
{
199199
$scope = $claimSet->getScope();
200200

201-
if (in_array($scope, $this->protectedClaims, true) && isset($this->claimSets[$scope])) {
201+
if (in_array($scope, $this->protectedScopes, true) && isset($this->claimSets[$scope])) {
202202
throw OidcServerException::serverError(
203203
sprintf("%s is a protected scope and is pre-defined by the OpenID Connect specification.", $scope),
204204
);
@@ -352,4 +352,13 @@ private function extractAdditionalClaims(array $requestedClaims, array $claims):
352352
ARRAY_FILTER_USE_KEY,
353353
);
354354
}
355+
356+
/**
357+
* Get supported claims for this OP. This will return all the claims for which the "SAML attribute to OIDC claim
358+
* translation" has been defined in module config, meaning it is expected for OP to release those claims.
359+
*/
360+
public function getSupportedClaims(): array
361+
{
362+
return array_keys(array_filter($this->translationTable));
363+
}
355364
}

tests/unit/src/ModuleConfigTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,4 +372,15 @@ public function testCanGetProtocolCacheConfiguration(): void
372372
$this->assertInstanceOf(DateInterval::class, $this->sut()->getProtocolUserEntityCacheDuration());
373373
$this->assertInstanceOf(DateInterval::class, $this->sut()->getProtocolClientEntityCacheDuration());
374374
}
375+
376+
public function testCanGetProtocolDiscoveryShowClaimsSupported(): void
377+
{
378+
$this->assertFalse($this->sut()->getProtocolDiscoveryShowClaimsSupported());
379+
$this->assertTrue(
380+
$this->sut(
381+
null,
382+
[ModuleConfig::OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED => true],
383+
)->getProtocolDiscoveryShowClaimsSupported(),
384+
);
385+
}
375386
}

tests/unit/src/Services/OpMetadataServiceTest.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
use SimpleSAML\Module\oidc\Codebooks\RoutesEnum;
1111
use SimpleSAML\Module\oidc\ModuleConfig;
1212
use SimpleSAML\Module\oidc\Services\OpMetadataService;
13+
use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor;
14+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
1315

1416
/**
1517
* @covers \SimpleSAML\Module\oidc\Services\OpMetadataService
1618
*/
1719
class OpMetadataServiceTest extends TestCase
1820
{
1921
protected MockObject $moduleConfigMock;
22+
protected MockObject $claimTranslatorExtractorMock;
2023

2124
/**
2225
* @throws \Exception
@@ -46,14 +49,24 @@ public function setUp(): void
4649
$signer = $this->createMock(Rsa::class);
4750
$signer->method('algorithmId')->willReturn('RS256');
4851
$this->moduleConfigMock->method('getProtocolSigner')->willReturn($signer);
52+
53+
$this->claimTranslatorExtractorMock = $this->createMock(ClaimTranslatorExtractor::class);
4954
}
5055

5156
/**
5257
* @throws \Exception
5358
*/
54-
protected function prepareMockedInstance(): OpMetadataService
55-
{
56-
return new OpMetadataService($this->moduleConfigMock);
59+
protected function sut(
60+
?ModuleConfig $moduleConfig = null,
61+
?ClaimTranslatorExtractor $claimTranslatorExtractor = null,
62+
): OpMetadataService {
63+
$moduleConfig = $moduleConfig ?? $this->moduleConfigMock;
64+
$claimTranslatorExtractor = $claimTranslatorExtractor ?? $this->claimTranslatorExtractorMock;
65+
66+
return new OpMetadataService(
67+
$moduleConfig,
68+
$claimTranslatorExtractor,
69+
);
5770
}
5871

5972
/**
@@ -63,7 +76,7 @@ public function testItIsInitializable(): void
6376
{
6477
$this->assertInstanceOf(
6578
OpMetadataService::class,
66-
$this->prepareMockedInstance(),
79+
$this->sut(),
6780
);
6881
}
6982

@@ -102,7 +115,23 @@ public function testItReturnsExpectedMetadata(): void
102115
'backchannel_logout_supported' => true,
103116
'backchannel_logout_session_supported' => true,
104117
],
105-
$this->prepareMockedInstance()->getMetadata(),
118+
$this->sut()->getMetadata(),
119+
);
120+
}
121+
122+
public function testCanShowClaimsSupportedClaim(): void
123+
{
124+
$this->moduleConfigMock->method('getProtocolDiscoveryShowClaimsSupported')->willReturn(true);
125+
$this->claimTranslatorExtractorMock->method('getSupportedClaims')->willReturn(['sample']);
126+
127+
$sut = $this->sut();
128+
$this->assertArrayHasKey(
129+
ClaimsEnum::ClaimsSupported->value,
130+
$sut->getMetadata(),
131+
);
132+
133+
$this->assertTrue(
134+
in_array('sample', $sut->getMetadata()[ClaimsEnum::ClaimsSupported->value], true),
106135
);
107136
}
108137
}

tests/unit/src/Utils/ClaimTranslatorExtractorTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,24 @@ public function testExtractRequestClaimsIdToken(): void
272272
$claims = $claimTranslator->extractAdditionalIdTokenClaims($requestClaims, ['displayName' => ['bob']]);
273273
$this->assertEquals(['name' => 'bob'], $claims);
274274
}
275+
276+
public function testCanGetSupportedClaims(): void
277+
{
278+
$translate = [
279+
'custom' => [
280+
'type' => 'int',
281+
'custom_attr',
282+
],
283+
];
284+
285+
$this->assertTrue(in_array('custom', $this->mock([], $translate)->getSupportedClaims(), true));
286+
}
287+
288+
public function testCanUnsetClaimWhichIsSupportedByDefault(): void
289+
{
290+
$this->assertTrue(in_array('nickname', $this->mock()->getSupportedClaims(), true));
291+
292+
$translate = ['nickname' => []];
293+
$this->assertFalse(in_array('nickname', $this->mock([], $translate)->getSupportedClaims(), true));
294+
}
275295
}

0 commit comments

Comments
 (0)