Skip to content

Commit 5ab7708

Browse files
committed
Add initial dynamic Trust Mark fetch capabilities
1 parent 1bab69d commit 5ab7708

File tree

7 files changed

+89
-3
lines changed

7 files changed

+89
-3
lines changed

UPGRADE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ and optionally a port (as in all previous module versions).
4747
- federation caching adapter and its arguments
4848
- PKI keys - federation keys used for example to sign federation entity statements
4949
- federation participation limiting based on Trust Marks for RPs
50+
- (from v6.1) own Trust Marks to dynamically fetch
5051
- signer algorithm
5152
- entity statement duration
5253
- organization name

config/module_oidc.php.dist

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,20 @@ $config = [
369369
],
370370

371371
// (optional) Federation Trust Mark tokens. An array of tokens (signed JWTs), each representing a Trust Mark
372-
// issued to this entity.
372+
// issued to this entity. This option is primarily intended for long-lasting or non-expiring tokens, so it
373+
// is not necessary to dynamically fetch / refresh them.
373374
ModuleConfig::OPTION_FEDERATION_TRUST_MARK_TOKENS => [
374375
// 'eyJ...GHg',
375376
],
376377

378+
// (optional) Federation Trust Marks for dynamic fetching. An array of key-value pairs, where key is Trust Mark ID
379+
// and value is Trust Mark Issuer ID, each representing a Trust Mark issued to this entity. Each Trust Mark ID
380+
// in this array will be dynamically fetched from noted Trust Mark Issuer as necessary. If federation caching
381+
// is enabled (recommended), fetched Trust Marks will also be cached until their expiry.
382+
ModuleConfig::OPTION_FEDERATION_DYNAMIC_TRUST_MARK_TOKENS => [
383+
// 'trust-mark-id' => 'trust-mark-issuer-id',
384+
],
385+
377386
// (optional) Federation participation limit by Trust Marks. This is an array with the following format:
378387
// [
379388
// 'trust-anchor-id' => [

src/Controllers/Admin/ConfigController.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function protocolSettings(): Response
6868

6969
public function federationSettings(): Response
7070
{
71-
$trustMarks = null;
71+
$trustMarks = [];
7272
if (is_array($trustMarkTokens = $this->moduleConfig->getFederationTrustMarkTokens())) {
7373
$trustMarks = array_map(
7474
function (string $token): Federation\TrustMark {
@@ -78,6 +78,23 @@ function (string $token): Federation\TrustMark {
7878
);
7979
}
8080

81+
if (is_array($dynamicTrustMarks = $this->moduleConfig->getFederationDynamicTrustMarks())) {
82+
/**
83+
* @var non-empty-string $trustMarkId
84+
* @var non-empty-string $trustMarkIssuerId
85+
*/
86+
foreach ($dynamicTrustMarks as $trustMarkId => $trustMarkIssuerId) {
87+
$trustMarkIssuerConfigurationStatement = $this->federation->entityStatementFetcher()
88+
->fromCacheOrWellKnownEndpoint($trustMarkIssuerId);
89+
90+
$trustMarks[] = $this->federation->trustMarkFetcher()->fromCacheOrFederationTrustMarkEndpoint(
91+
$trustMarkId,
92+
$this->moduleConfig->getIssuer(),
93+
$trustMarkIssuerConfigurationStatement,
94+
);
95+
}
96+
}
97+
8198
return $this->templateFactory->build(
8299
'oidc:config/federation.twig',
83100
[

src/Controllers/Federation/EntityStatementController.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1212
use SimpleSAML\Module\oidc\Services\JsonWebKeySetService;
1313
use SimpleSAML\Module\oidc\Services\JsonWebTokenBuilderService;
14+
use SimpleSAML\Module\oidc\Services\LoggerService;
1415
use SimpleSAML\Module\oidc\Services\OpMetadataService;
1516
use SimpleSAML\Module\oidc\Utils\FederationCache;
1617
use SimpleSAML\Module\oidc\Utils\Routes;
@@ -42,6 +43,7 @@ public function __construct(
4243
private readonly Helpers $helpers,
4344
private readonly Routes $routes,
4445
private readonly Federation $federation,
46+
private readonly LoggerService $loggerService,
4547
private readonly ?FederationCache $federationCache,
4648
) {
4749
if (!$this->moduleConfig->getFederationEnabled()) {
@@ -126,6 +128,8 @@ public function configuration(): Response
126128
$builder = $builder->withClaim(ClaimsEnum::AuthorityHints->value, $authorityHints);
127129
}
128130

131+
$trustMarks = [];
132+
129133
if (
130134
is_array($trustMarkTokens = $this->moduleConfig->getFederationTrustMarkTokens()) &&
131135
(!empty($trustMarkTokens))
@@ -145,7 +149,45 @@ public function configuration(): Response
145149
ClaimsEnum::TrustMark->value => $token,
146150
];
147151
}, $trustMarkTokens);
152+
}
153+
154+
if (
155+
is_array($dynamicTrustMarks = $this->moduleConfig->getFederationDynamicTrustMarks()) &&
156+
(!empty($dynamicTrustMarks))
157+
) {
158+
/**
159+
* @var non-empty-string $trustMarkId
160+
* @var non-empty-string $trustMarkIssuerId
161+
*/
162+
foreach ($dynamicTrustMarks as $trustMarkId => $trustMarkIssuerId) {
163+
try {
164+
$trustMarkIssuerConfigurationStatement = $this->federation->entityStatementFetcher()
165+
->fromCacheOrWellKnownEndpoint($trustMarkIssuerId);
166+
167+
$trustMarkEntity = $this->federation->trustMarkFetcher()->fromCacheOrFederationTrustMarkEndpoint(
168+
$trustMarkId,
169+
$this->moduleConfig->getIssuer(),
170+
$trustMarkIssuerConfigurationStatement,
171+
);
172+
173+
$trustMarks[] = [
174+
ClaimsEnum::TrustMarkId->value => $trustMarkId,
175+
ClaimsEnum::TrustMark->value => $trustMarkEntity->getToken(),
176+
];
177+
} catch (\Throwable $exception) {
178+
$this->loggerService->error(
179+
'Error fetching Trust Mark: ' . $exception->getMessage(),
180+
[
181+
'trustMarkId' => $trustMarkId,
182+
'subjectId' => $this->moduleConfig->getIssuer(),
183+
'trustMarkIssuerId' => $trustMarkIssuerId,
184+
],
185+
);
186+
}
187+
}
188+
}
148189

190+
if (!empty($trustMarks)) {
149191
$builder = $builder->withClaim(ClaimsEnum::TrustMarks->value, $trustMarks);
150192
}
151193

src/ModuleConfig.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ class ModuleConfig
7676
final public const OPTION_FEDERATION_CACHE_MAX_DURATION_FOR_FETCHED = 'federation_cache_max_duration_for_fetched';
7777
final public const OPTION_FEDERATION_TRUST_ANCHORS = 'federation_trust_anchors';
7878
final public const OPTION_FEDERATION_TRUST_MARK_TOKENS = 'federation_trust_mark_tokens';
79+
final public const OPTION_FEDERATION_DYNAMIC_TRUST_MARK_TOKENS = 'federation_dynamic_trust_mark_tokens';
7980
final public const OPTION_FEDERATION_CACHE_DURATION_FOR_PRODUCED = 'federation_cache_duration_for_produced';
8081
final public const OPTION_PROTOCOL_CACHE_ADAPTER = 'protocol_cache_adapter';
8182
final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments';
@@ -632,6 +633,16 @@ public function getFederationTrustMarkTokens(): ?array
632633
return empty($trustMarks) ? null : $trustMarks;
633634
}
634635

636+
public function getFederationDynamicTrustMarks(): ?array
637+
{
638+
$dynamicTrustMarks = $this->config()->getOptionalArray(
639+
self::OPTION_FEDERATION_DYNAMIC_TRUST_MARK_TOKENS,
640+
null,
641+
);
642+
643+
return empty($dynamicTrustMarks) ? null : $dynamicTrustMarks;
644+
}
645+
635646
public function getOrganizationName(): ?string
636647
{
637648
return $this->config()->getOptionalString(

templates/config/federation.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
{% if trustMarks|default is not empty %}
9494
{% for trustMark in trustMarks %}
9595
<p>
96-
- {{ trustMark.getPayload.id }}
96+
- {{ trustMark.getPayload.trust_mark_id }}
9797
<code class="code-box code-box-content">
9898
{{- trustMark.getPayload|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES')) -}}
9999
</code>

tests/unit/src/Controllers/Federation/EntityStatementControllerTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1515
use SimpleSAML\Module\oidc\Services\JsonWebKeySetService;
1616
use SimpleSAML\Module\oidc\Services\JsonWebTokenBuilderService;
17+
use SimpleSAML\Module\oidc\Services\LoggerService;
1718
use SimpleSAML\Module\oidc\Services\OpMetadataService;
1819
use SimpleSAML\Module\oidc\Utils\FederationCache;
1920
use SimpleSAML\Module\oidc\Utils\Routes;
@@ -30,6 +31,7 @@ class EntityStatementControllerTest extends TestCase
3031
protected MockObject $helpersMock;
3132
protected MockObject $routesMock;
3233
protected MockObject $federationMock;
34+
protected MockObject $loggerServiceMock;
3335
protected MockObject $federationCacheMock;
3436

3537
protected function setUp(): void
@@ -42,6 +44,7 @@ protected function setUp(): void
4244
$this->helpersMock = $this->createMock(Helpers::class);
4345
$this->routesMock = $this->createMock(Routes::class);
4446
$this->federationMock = $this->createMock(Federation::class);
47+
$this->loggerServiceMock = $this->createMock(LoggerService::class);
4548
$this->federationCacheMock = $this->createMock(FederationCache::class);
4649
}
4750

@@ -54,6 +57,7 @@ protected function sut(
5457
?Helpers $helpers = null,
5558
?Routes $routes = null,
5659
?Federation $federation = null,
60+
?LoggerService $loggerService = null,
5761
?FederationCache $federationCache = null,
5862
): EntityStatementController {
5963
$moduleConfig ??= $this->moduleConfigMock;
@@ -64,6 +68,7 @@ protected function sut(
6468
$helpers ??= $this->helpersMock;
6569
$routes ??= $this->routesMock;
6670
$federation ??= $this->federationMock;
71+
$loggerService ??= $this->loggerServiceMock;
6772
$federationCache ??= $this->federationCacheMock;
6873

6974
return new EntityStatementController(
@@ -75,6 +80,7 @@ protected function sut(
7580
$helpers,
7681
$routes,
7782
$federation,
83+
$loggerService,
7884
$federationCache,
7985
);
8086
}

0 commit comments

Comments
 (0)