Skip to content

Commit 1455157

Browse files
committed
Merge branch 'wip-version-7' of github.com:simplesamlphp/simplesamlphp-module-oidc into wip-version-7
2 parents cca05a8 + d801de2 commit 1455157

13 files changed

Lines changed: 376 additions & 11 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"psr/container": "^2.0",
3232
"psr/log": "^3",
3333
"simplesamlphp/composer-module-installer": "^1.3",
34-
"simplesamlphp/openid": "~v0.1.1",
34+
"simplesamlphp/openid": "~0.2.0",
3535
"spomky-labs/base64url": "^2.0",
3636
"symfony/expression-language": "^7.4",
3737
"symfony/psr-http-message-bridge": "^7.4",

public/assets/css/src/default.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,31 @@ table.client-table {
104104
font-weight: bolder;
105105
}
106106

107-
.confirm-action {}
107+
108108

109109
form.pure-form-stacked .full-width {
110110
width: 100%;
111111
}
112+
113+
/* Form loading state */
114+
.form-loading-spinner {
115+
display: inline-block;
116+
width: 0.85em;
117+
height: 0.85em;
118+
border: 2px solid currentColor;
119+
border-top-color: transparent;
120+
border-radius: 50%;
121+
animation: form-spinner-rotate 0.7s linear infinite;
122+
vertical-align: middle;
123+
margin-right: 0.4em;
124+
opacity: 0.8;
125+
}
126+
127+
@keyframes form-spinner-rotate {
128+
to { transform: rotate(360deg); }
129+
}
130+
131+
button[disabled].loading {
132+
opacity: 0.7;
133+
cursor: not-allowed;
134+
}

public/assets/js/src/default.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
(function() {
32

43
// Attach `confirm-action` click event to all elements with the `confirm-action` class.
@@ -19,4 +18,17 @@
1918
}
2019
});
2120
});
21+
22+
// Handle forms with loading state
23+
document.querySelectorAll('form.form-with-loading-state').forEach(form => {
24+
form.addEventListener('submit', function (event) {
25+
const submitter = event.submitter || this.querySelector('button[type="submit"]');
26+
if (submitter) {
27+
const loadingText = submitter.getAttribute('data-loading-text') || 'Processing...';
28+
submitter.disabled = true;
29+
submitter.classList.add('loading');
30+
submitter.innerHTML = `<span class="form-loading-spinner"></span> ${loadingText}`;
31+
}
32+
});
33+
});
2234
})();

routing/routes/routes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
$routes->add(RoutesEnum::AdminTestTrustMarkValidation->name, RoutesEnum::AdminTestTrustMarkValidation->value)
7878
->controller([FederationTestController::class, 'trustMarkValidation'])
7979
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
80+
$routes->add(RoutesEnum::AdminTestFederationDiscovery->name, RoutesEnum::AdminTestFederationDiscovery->value)
81+
->controller([FederationTestController::class, 'federationDiscovery'])
82+
->methods([HttpMethodsEnum::GET->value, HttpMethodsEnum::POST->value]);
8083
$routes->add(
8184
RoutesEnum::AdminTestVerifiableCredentialIssuance->name,
8285
RoutesEnum::AdminTestVerifiableCredentialIssuance->value,

src/Codebooks/RoutesEnum.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ enum RoutesEnum: string
2828
// Testing
2929
case AdminTestTrustChainResolution = 'admin/test/trust-chain-resolution';
3030
case AdminTestTrustMarkValidation = 'admin/test/trust-mark-validation';
31+
case AdminTestFederationDiscovery = 'admin/test/federation-discovery';
3132
case AdminTestVerifiableCredentialIssuance = 'admin/test/verifiable-credential-issuance';
3233

3334

src/Controllers/Admin/FederationTestController.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,135 @@ public function trustMarkValidation(Request $request): Response
169169
RoutesEnum::AdminTestTrustMarkValidation->value,
170170
);
171171
}
172+
173+
174+
/**
175+
* @throws \SimpleSAML\Error\ConfigurationError
176+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
177+
* @throws \SimpleSAML\Module\oidc\Exceptions\OidcException
178+
*/
179+
public function federationDiscovery(Request $request): Response
180+
{
181+
$trustAnchorId = null;
182+
$isFormSubmitted = false;
183+
$entities = [];
184+
$forceRefresh = false;
185+
$filterEntityTypes = [];
186+
$filterTrustMarkTypes = '';
187+
$filterQuery = '';
188+
$sortBy = 'entity_id';
189+
$sortOrder = 'asc';
190+
$pageLimit = 50;
191+
$pageFrom = null;
192+
$nextPageToken = null;
193+
$totalCount = 0;
194+
195+
if ($request->isMethod(Request::METHOD_POST)) {
196+
$isFormSubmitted = true;
197+
198+
!empty($trustAnchorId = $request->request->getString('trustAnchorId')) ||
199+
throw new OidcException('Empty Trust Anchor ID.');
200+
201+
$forceRefresh = $request->request->getBoolean('forceRefresh');
202+
/** @var string[] $filterEntityTypes */
203+
$filterEntityTypes = $request->request->all('filterEntityTypes');
204+
$filterTrustMarkTypes = $request->request->getString('filterTrustMarkTypes');
205+
$filterQuery = $request->request->getString('filterQuery');
206+
$sortBy = $request->request->getString('sortBy', 'entity_id');
207+
$sortOrder = $request->request->getString('sortOrder', 'asc');
208+
/** @var 'asc'|'desc' $sortOrder */
209+
$sortOrder = in_array($sortOrder, ['asc', 'desc']) ? $sortOrder : 'asc';
210+
$pageLimit = $request->request->getInt('pageLimit', 50);
211+
$pageFrom = $request->request->get('pageFrom');
212+
$pageFrom = is_string($pageFrom) ? $pageFrom : null;
213+
214+
try {
215+
$entityCollection = $this->federationWithArrayLogger->federationDiscovery()->discover(
216+
trustAnchorId: $trustAnchorId,
217+
forceRefresh: $forceRefresh,
218+
);
219+
220+
// 1. Filtering
221+
$criteria = array_filter([
222+
'entity_type' => $filterEntityTypes,
223+
'trust_mark_type' => $this->helpers->str()->convertTextToArray($filterTrustMarkTypes),
224+
'query' => $filterQuery,
225+
]);
226+
if (!empty($criteria)) {
227+
$entityCollection->filter($criteria);
228+
}
229+
230+
$totalCount = count($entityCollection->getEntities());
231+
232+
// 2. Sorting
233+
$claimPaths = match ($sortBy) {
234+
'display_name' => [
235+
['metadata', EntityTypesEnum::OpenIdProvider->value, 'display_name'],
236+
['metadata', EntityTypesEnum::FederationEntity->value, 'display_name'],
237+
['metadata', EntityTypesEnum::OpenIdRelyingParty->value, 'display_name'],
238+
],
239+
'organization_name' => [
240+
['metadata', EntityTypesEnum::OpenIdProvider->value, 'organization_name'],
241+
['metadata', EntityTypesEnum::FederationEntity->value, 'organization_name'],
242+
['metadata', EntityTypesEnum::OpenIdRelyingParty->value, 'organization_name'],
243+
],
244+
default => [['sub']],
245+
};
246+
$entityCollection->sort($claimPaths, $sortOrder);
247+
248+
// 3. Pagination
249+
/** @var positive-int $pageLimit */
250+
$entityCollection->paginate($pageLimit, $pageFrom);
251+
252+
$nextPageToken = $entityCollection->getNextPageToken();
253+
254+
foreach ($entityCollection->getEntities() as $id => $payload) {
255+
$entities[] = [
256+
'id' => $id,
257+
'payload' => $payload,
258+
];
259+
}
260+
} catch (\Throwable $exception) {
261+
$this->arrayLogger->error(sprintf(
262+
'Error during entity discovery under Trust Anchor %s. Error was %s',
263+
$trustAnchorId,
264+
$exception->getMessage(),
265+
));
266+
}
267+
}
268+
269+
$logMessages = $this->arrayLogger->getEntries();
270+
271+
try {
272+
$trustAnchorIds = $this->moduleConfig->getFederationTrustAnchorIds();
273+
} catch (\Throwable $exception) {
274+
$this->arrayLogger->error('Module config error: ' . $exception->getMessage());
275+
$trustAnchorIds = [];
276+
}
277+
278+
$entityTypeOptions = array_map(fn (EntityTypesEnum $enum) => $enum->value, EntityTypesEnum::cases());
279+
280+
return $this->templateFactory->build(
281+
'oidc:tests/federation-discovery.twig',
282+
compact(
283+
'trustAnchorId',
284+
'logMessages',
285+
'isFormSubmitted',
286+
'entities',
287+
'trustAnchorIds',
288+
'forceRefresh',
289+
'filterEntityTypes',
290+
'filterTrustMarkTypes',
291+
'filterQuery',
292+
'sortBy',
293+
'sortOrder',
294+
'pageLimit',
295+
'pageFrom',
296+
'nextPageToken',
297+
'totalCount',
298+
'entityTypeOptions',
299+
),
300+
RoutesEnum::AdminTestFederationDiscovery->value,
301+
);
302+
}
172303
}

src/Controllers/Federation/EntityStatementController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ public function configuration(): Response
9595
ClaimsEnum::OrganizationUri->value => $this->moduleConfig->getOrganizationUri(),
9696
],
9797
)),
98-
ClaimsEnum::FederationFetchEndpoint->value => $this->routes->urlFederationFetch(),
99-
ClaimsEnum::FederationListEndpoint->value => $this->routes->urlFederationList(),
10098
// TODO v7 mivanci Add when ready. Use ClaimsEnum for keys.
10199
// https://openid.net/specs/openid-federation-1_0.html#name-federation-entity
102100
//'federation_resolve_endpoint',

src/Factories/TemplateFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ protected function includeDefaultMenuItems(): void
142142
),
143143
);
144144

145+
$this->oidcMenu->addItem(
146+
$this->oidcMenu->buildItem(
147+
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminTestFederationDiscovery->value),
148+
Translate::noop('Test Federation Discovery'),
149+
),
150+
);
151+
145152
$this->oidcMenu->addItem(
146153
$this->oidcMenu->buildItem(
147154
$this->moduleConfig->getModuleUrl(RoutesEnum::AdminConfigVerifiableCredential->value),

src/Utils/Routes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ public function urlAdminTestTrustMarkValidation(array $parameters = []): string
146146
return $this->getModuleUrl(RoutesEnum::AdminTestTrustMarkValidation->value, $parameters);
147147
}
148148

149+
public function urlAdminTestFederationDiscovery(array $parameters = []): string
150+
{
151+
return $this->getModuleUrl(RoutesEnum::AdminTestFederationDiscovery->value, $parameters);
152+
}
153+
149154
public function urlAdminTestVerifiableCredentialIssuance(array $parameters = []): string
150155
{
151156
return $this->getModuleUrl(RoutesEnum::AdminTestVerifiableCredentialIssuance->value, $parameters);

0 commit comments

Comments
 (0)