Skip to content

Commit

Permalink
Enable testing trust chain resolution in admin UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Marko Ivančić committed Dec 14, 2024
1 parent 20c6cbf commit 6edc115
Show file tree
Hide file tree
Showing 22 changed files with 711 additions and 18 deletions.
27 changes: 27 additions & 0 deletions config-templates/module_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,33 @@
// 'eyJ...GHg',
],

// (optional) Federation participation limit by Trust Marks. This is an array with the following format:
// [
// 'trust-anchor-id' => [
// 'limit-id' => [
// 'trust-mark-id',
// 'trust-mark-id-2',
// ],
// ],
// ],
// Check example below on how this can be used. If federation participation limit is configured for particular
// Trust Anchor ID, at least one combination of "limit ID" => "trust mark list" should be defined.
ModuleConfig::OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS => [
// We are limiting federation participation using Trust Marks for 'https://ta.example.org/'.
'https://ta.example.org/' => [
// Entities must have (at least) one Trust Mark from the list below.
\SimpleSAML\Module\oidc\Codebooks\LimitsEnum::OneOf->value => [
'trust-mark-id',
'trust-mark-id-2',
],
// Entities must have all Trust Marks from the list below.
\SimpleSAML\Module\oidc\Codebooks\LimitsEnum::AllOf->value => [
'trust-mark-id-3',
'trust-mark-id-4',
],
],
],

// (optional) Dedicated federation cache adapter, used to cache federation artifacts like trust chains, entity
// statements, etc. It will also be used for token reuse check in federation context. Setting this option is
// recommended in production environments. If set to null, no caching will be used. Can be set to any
Expand Down
7 changes: 7 additions & 0 deletions routing/routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use SimpleSAML\Module\oidc\Controllers\AccessTokenController;
use SimpleSAML\Module\oidc\Controllers\Admin\ClientController;
use SimpleSAML\Module\oidc\Controllers\Admin\ConfigController;
use SimpleSAML\Module\oidc\Controllers\Admin\TestController;
use SimpleSAML\Module\oidc\Controllers\AuthorizationController;
use SimpleSAML\Module\oidc\Controllers\ConfigurationDiscoveryController;
use SimpleSAML\Module\oidc\Controllers\EndSessionController;
Expand Down Expand Up @@ -57,6 +58,12 @@
->controller([ClientController::class, 'delete'])
->methods([HttpMethodsEnum::POST->value]);

// Testing

$routes->add(RoutesEnum::AdminTestTrustChainResolution->name, RoutesEnum::AdminTestTrustChainResolution->value)
->controller([TestController::class, 'trustChainResolution'])
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);

/*****************************************************************************************************************
* OpenID Connect
****************************************************************************************************************/
Expand Down
2 changes: 2 additions & 0 deletions routing/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ services:
factory: ['@SimpleSAML\Module\oidc\Factories\ResourceServerFactory', 'build']

# Utils
SimpleSAML\Module\oidc\Utils\Debug\ArrayLogger: ~
SimpleSAML\Module\oidc\Utils\FederationParticipationValidator: ~
SimpleSAML\Module\oidc\Utils\Routes: ~
SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
Expand Down
11 changes: 11 additions & 0 deletions src/Codebooks/LimitsEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Codebooks;

enum LimitsEnum: string
{
case OneOf = 'one_of';
case AllOf = 'all_of';
}
4 changes: 4 additions & 0 deletions src/Codebooks/RoutesEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ enum RoutesEnum: string
case AdminClientsResetSecret = 'admin/clients/reset-secret';
case AdminClientsDelete = 'admin/clients/delete';

// Testing
case AdminTestTrustChainResolution = 'admin/test/trust-chain-resolution';


/*****************************************************************************************************************
* OpenID Connect
****************************************************************************************************************/
Expand Down
111 changes: 111 additions & 0 deletions src/Controllers/Admin/TestController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Controllers\Admin;

use SimpleSAML\Module\oidc\Admin\Authorization;
use SimpleSAML\Module\oidc\Codebooks\RoutesEnum;
use SimpleSAML\Module\oidc\Exceptions\OidcException;
use SimpleSAML\Module\oidc\Factories\TemplateFactory;
use SimpleSAML\Module\oidc\Helpers;
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Utils\Debug\ArrayLogger;
use SimpleSAML\OpenID\Codebooks\EntityTypesEnum;
use SimpleSAML\OpenID\Exceptions\TrustChainException;
use SimpleSAML\OpenID\Federation;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class TestController
{
public function __construct(
protected readonly ModuleConfig $moduleConfig,
protected readonly TemplateFactory $templateFactory,
protected readonly Authorization $authorization,
protected readonly Federation $federation,
protected readonly Helpers $helpers,
protected readonly ArrayLogger $arrayLogger,
) {
$this->authorization->requireAdmin(true);
}

/**
* @throws \SimpleSAML\Error\ConfigurationError
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
* @throws \SimpleSAML\Module\oidc\Exceptions\OidcException
*/
public function trustChainResolution(Request $request): Response
{
$this->arrayLogger->setWeight(ArrayLogger::WEIGHT_WARNING);
// Let's create new Federation instance so we can inject our debug logger and go without cache.
$federation = new Federation(
supportedAlgorithms: $this->federation->supportedAlgorithms(),
cache: null,
logger: $this->arrayLogger,
);

$leafEntityId = $this->moduleConfig->getIssuer();
$trustChainBag = null;
$resolvedMetadata = [];
$isFormSubmitted = false;

try {
$trustAnchorIds = $this->moduleConfig->getFederationTrustAnchorIds();
} catch (\Throwable $exception) {
$this->arrayLogger->error('Module config error: ' . $exception->getMessage());
$trustAnchorIds = [];
}

if ($request->isMethod(Request::METHOD_POST)) {
$isFormSubmitted = true;

!empty($leafEntityId = $request->request->getString('leafEntityId')) ||
throw new OidcException('Empty leaf entity ID.');
!empty($rawTrustAnchorIds = $request->request->getString('trustAnchorIds')) ||
throw new OidcException('Empty Trust Anchor IDs.');

/** @var non-empty-array<non-empty-string> $trustAnchorIds */
$trustAnchorIds = $this->helpers->str()->convertTextToArray($rawTrustAnchorIds);

try {
$trustChainBag = $federation->trustChainResolver()->for($leafEntityId, $trustAnchorIds);

foreach ($trustChainBag->getAll() as $index => $trustChain) {
$metadataEntries = [];
foreach (EntityTypesEnum::cases() as $entityTypeEnum) {
try {
$metadataEntries[$entityTypeEnum->value] =
$trustChain->getResolvedMetadata($entityTypeEnum);
} catch (\Throwable $exception) {
$this->arrayLogger->error(
'Metadata resolving error: ' . $exception->getMessage(),
compact('index', 'entityTypeEnum'),
);
continue;
}
}
$resolvedMetadata[$index] = array_filter($metadataEntries);
}
} catch (TrustChainException $exception) {
$this->arrayLogger->error('Trust chain error: ' . $exception->getMessage());
}
}

$trustAnchorIds = implode("\n", $trustAnchorIds);
$logMessages = $this->arrayLogger->getEntries();
//dd($this->arrayLogger->getEntries());
return $this->templateFactory->build(
'oidc:tests/trust-chain-resolution.twig',
compact(
'leafEntityId',
'trustAnchorIds',
'trustChainBag',
'resolvedMetadata',
'logMessages',
'isFormSubmitted',
),
RoutesEnum::AdminTestTrustChainResolution->value,
);
}
}
16 changes: 8 additions & 8 deletions src/Controllers/Federation/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,25 @@ public function __invoke(): Response
// $requestObject = $requestObjectFactory->fromToken($unprotectedJws);

// dd($requestObject, $requestObject->getPayload(), $requestObject->getHeader());
// $cache->clear();
$this->federationCache?->cache->clear();

$trustChain = $this->federation
->trustChainResolver()
->for(
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/',
// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ALeaf/',
// 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/oidc/rp/',
// 'https://relying-party-php.testbed.oidcfed.incubator.geant.org/',
// 'https://gorp.testbed.oidcfed.incubator.geant.org',
'https://gorp.testbed.oidcfed.incubator.geant.org',
// 'https://maiv1.incubator.geant.org',
[
// 'https://trust-anchor.testbed.oidcfed.incubator.geant.org/',
'https://trust-anchor.testbed.oidcfed.incubator.geant.org/',
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/ABTrustAnchor/',
// 'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/',
'https://08-dap.localhost.markoivancic.from.hr/openid/entities/CTrustAnchor/',
],
);

)->getAll();
dd($trustChain);
$leaf = $trustChain->getResolvedLeaf();
// dd($leaf);
dd($leaf->getPayload());
$leafFederationJwks = $leaf->getJwks();
// dd($leafFederationJwks);
// /** @psalm-suppress PossiblyNullArgument */
Expand Down
3 changes: 3 additions & 0 deletions src/Factories/RequestRulesManagerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use SimpleSAML\Module\oidc\Services\LoggerService;
use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor;
use SimpleSAML\Module\oidc\Utils\FederationCache;
use SimpleSAML\Module\oidc\Utils\FederationParticipationValidator;
use SimpleSAML\Module\oidc\Utils\JwksResolver;
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
Expand All @@ -58,6 +59,7 @@ public function __construct(
private readonly Federation $federation,
private readonly Helpers $helpers,
private readonly JwksResolver $jwksResolver,
private readonly FederationParticipationValidator $federationParticipationValidator,
private readonly ?FederationCache $federationCache = null,
private readonly ?ProtocolCache $protocolCache = null,
) {
Expand Down Expand Up @@ -88,6 +90,7 @@ private function getDefaultRules(): array
$this->federation,
$this->helpers,
$this->jwksResolver,
$this->federationParticipationValidator,
$this->federationCache,
),
new RedirectUriRule($this->requestParamsResolver),
Expand Down
11 changes: 9 additions & 2 deletions src/Factories/TemplateFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ protected function includeDefaultMenuItems(): void
),
);

$this->oidcMenu->addItem(
$this->oidcMenu->buildItem(
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminClients->value),
Translate::noop('Client Registry'),
),
);

$this->oidcMenu->addItem(
$this->oidcMenu->buildItem(
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminConfigProtocol->value),
Expand All @@ -123,8 +130,8 @@ protected function includeDefaultMenuItems(): void

$this->oidcMenu->addItem(
$this->oidcMenu->buildItem(
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminClients->value),
Translate::noop('Client Registry'),
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminTestTrustChainResolution->value),
Translate::noop('Test Trust Chain Resolution'),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Forms/ClientForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ protected function getScopes(): array
}

/**
* TODO mivanci Move to Str helper.
* @return string[]
*/
protected function convertTextToArrayWithLinesAsValues(string $text): array
Expand Down
21 changes: 21 additions & 0 deletions src/Helpers/Arr.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,25 @@ public function ensureStringValues(array $values): array
{
return array_map(fn(mixed $value): string => (string)$value, $values);
}

public function isValueOneOf(mixed $value, array $set): bool
{
$value = is_array($value) ? $value : [$value];
return !empty(array_intersect($value, $set));
}

public function isValueSubsetOf(mixed $value, array $superset): bool
{
$value = is_array($value) ? $value : [$value];

return empty(array_diff($value, $superset));
}

public function isValueSupersetOf(mixed $value, array $subset): bool
{
$value = is_array($value) ? $value : [$value];

// Opposite of subset...
return $this->isValueSubsetOf($subset, $value);
}
}
12 changes: 12 additions & 0 deletions src/Helpers/Str.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,16 @@ public function convertScopesStringToArray(string $scopes, string $delimiter = '
{
return array_filter(explode($delimiter, trim($scopes)), fn($scope) => !empty($scope));
}

/**
* @param non-empty-string $pattern
* @return string[]
*/
public function convertTextToArray(string $text, string $pattern = "/[\t\r\n]+/"): array
{
return array_filter(
preg_split($pattern, $text),
fn(string $line): bool => !empty(trim($line)),
);
}
}
33 changes: 32 additions & 1 deletion src/ModuleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class ModuleConfig
final public const OPTION_PROTOCOL_CACHE_ADAPTER = 'protocol_cache_adapter';
final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments';
final public const OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION = 'protocol_user_entity_cache_duration';
final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS =
'federation_participation_limit_by_trust_marks';

protected static array $standardScopes = [
ScopesEnum::OpenId->value => [
Expand Down Expand Up @@ -465,7 +467,7 @@ public function getProtocolUserEntityCacheDuration(): DateInterval


/*****************************************************************************************************************
* OpenID Connect related config.
* OpenID Federation related config.
****************************************************************************************************************/

public function getFederationEnabled(): bool
Expand Down Expand Up @@ -669,4 +671,33 @@ public function getTrustAnchorJwks(string $trustAnchorId): ?array
sprintf('Unexpected JWKS format for Trust Anchor %s: %s', $trustAnchorId, var_export($jwks, true)),
);
}

public function getFederationParticipationLimitByTrustMarks(): array
{
return $this->config()->getOptionalArray(
self::OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS,
[],
);
}

/**
* @throws \SimpleSAML\Error\ConfigurationError
*/
public function getTrustMarksNeededForFederationParticipationFor(string $trustAnchorId): array
{
$participationLimit = $this->getFederationParticipationLimitByTrustMarks()[$trustAnchorId] ?? [];
if (!is_array($participationLimit)) {
throw new ConfigurationError('Invalid configuration for federation participation limit.');
}

return $participationLimit;
}

/**
* @throws \SimpleSAML\Error\ConfigurationError
*/
public function isFederationParticipationLimitedByTrustMarksFor(string $trustAnchorId): bool
{
return !empty($this->getTrustMarksNeededForFederationParticipationFor($trustAnchorId));
}
}
Loading

0 comments on commit 6edc115

Please sign in to comment.