Skip to content

Commit dc8766f

Browse files
committed
WIP
1 parent 819bc62 commit dc8766f

File tree

10 files changed

+281
-18
lines changed

10 files changed

+281
-18
lines changed

config/module_oidc.php.dist

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ declare(strict_types=1);
2020
* For the full copyright and license information, please view the LICENSE
2121
* file that was distributed with this source code.
2222
*/
23+
2324
use SimpleSAML\Module\oidc\ModuleConfig;
2425
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2526
use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum;
@@ -788,4 +789,24 @@ $config = [
788789
['eduPersonScopedAffiliation' => ['eduPersonScopedAffiliation']],
789790
],
790791
],
792+
793+
/**
794+
* (optional) API-related options.
795+
*/
796+
797+
// (optional) Enable or disable API capabilities. Default is disabled (false).
798+
ModuleConfig::OPTION_API_ENABLED => false,
799+
800+
/**
801+
* List of API tokens which can be used to access API endpoints based on given scopes.
802+
*
803+
* The format is: ['token' => [ApiScopesEnum]]
804+
*/
805+
ModuleConfig::OPTION_API_TOKENS => [
806+
// 'strong-random-token-string' => [
807+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
808+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciAll, // Gives access to all VCI-related endpoints.
809+
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciCredentialOffer, // Gives access to the credential offer endpoint.
810+
// ],
811+
],
791812
];

routing/routes/routes.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use SimpleSAML\Module\oidc\Controllers\Admin\ConfigController;
1313
use SimpleSAML\Module\oidc\Controllers\Admin\FederationTestController;
1414
use SimpleSAML\Module\oidc\Controllers\Admin\VerifiableCredentailsTestController;
15+
use SimpleSAML\Module\oidc\Controllers\Api\VciCredentialOfferController;
1516
use SimpleSAML\Module\oidc\Controllers\AuthorizationController;
1617
use SimpleSAML\Module\oidc\Controllers\ConfigurationDiscoveryController;
1718
use SimpleSAML\Module\oidc\Controllers\EndSessionController;
@@ -140,4 +141,12 @@
140141
$routes->add(RoutesEnum::JwtVcIssuerConfiguration->name, RoutesEnum::JwtVcIssuerConfiguration->value)
141142
->controller([JwtVcIssuerConfigurationController::class, 'configuration'])
142143
->methods([HttpMethodsEnum::GET->value]);
144+
145+
/*****************************************************************************************************************
146+
* API
147+
****************************************************************************************************************/
148+
149+
$routes->add(RoutesEnum::ApiVciCredentialOffer->name, RoutesEnum::ApiVciCredentialOffer->value)
150+
->controller([VciCredentialOfferController::class, 'credentialOffer'])
151+
->methods([HttpMethodsEnum::POST->value]);
143152
};

src/Codebooks/ApiScopesEnum.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Codebooks;
6+
7+
enum ApiScopesEnum: string
8+
{
9+
case All = 'all'; // Gives access to the whole API.
10+
11+
// Verifiable Credential Issuance related scopes.
12+
case VciAll = 'vci_all'; // Gives access to all VCI-related endpoints.
13+
case VciCredentialOffer = 'vci_credential_offer'; // Gives access to the credential offer endpoint.
14+
}

src/Codebooks/RoutesEnum.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,10 @@ enum RoutesEnum: string
6969
****************************************************************************************************************/
7070

7171
case JwtVcIssuerConfiguration = '.well-known/jwt-vc-issuer';
72+
73+
/*****************************************************************************************************************
74+
* API
75+
****************************************************************************************************************/
76+
77+
case ApiVciCredentialOffer = 'api/vci/credential-offer';
7278
}

src/Controllers/Admin/VerifiableCredentailsTestController.php

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,25 +175,10 @@ public function verifiableCredentialIssuance(Request $request): Response
175175
// TODO mivanci Wallet (client) credential_offer_endpoint metadata
176176
// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#client-metadata
177177

178-
// TODO mivanci Implement API and API clients hanlding, together with API client to OIDC client binding.
179-
$clientId = '1234567890';
180178
$clientSecret = '1234567890';
181179

182-
$client = $this->clientEntityFactory->fromData(
183-
id: $clientId,
184-
secret: $clientSecret,
185-
name: 'VCI Pre-authorized Code Test Client',
186-
description: 'Test client for VCI Pre-authorized Code',
187-
redirectUri: ['https://example.com/oidc/callback'],
188-
scopes: ['openid', ...$credentialConfigurationIdsSupported], // Test Client so will have
189-
isEnabled: true,
190-
);
191180

192-
if ($this->clientRepository->findById($clientId) === null) {
193-
$this->clientRepository->add($client);
194-
} else {
195-
$this->clientRepository->update($client);
196-
}
181+
$client = $this->clientEntityFactory->getGenericForVciPreAuthZFlow();
197182

198183
// TODO mivanci Randomly generate auth code.
199184
$authCodeId = '1234567890';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Controllers\Api;
6+
7+
use SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum;
8+
use SimpleSAML\Module\oidc\Exceptions\AuthorizationException;
9+
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
10+
use SimpleSAML\Module\oidc\ModuleConfig;
11+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
12+
use SimpleSAML\Module\oidc\Services\Api\Authorization;
13+
use SimpleSAML\OpenID\VerifiableCredentials;
14+
use Symfony\Component\HttpFoundation\JsonResponse;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
18+
class VciCredentialOfferController
19+
{
20+
/**
21+
* @throws OidcServerException
22+
*/
23+
public function __construct(
24+
protected readonly ModuleConfig $moduleConfig,
25+
protected readonly Authorization $authorization,
26+
protected readonly VerifiableCredentials $verifiableCredentials,
27+
protected readonly ClientEntityFactory $clientEntityFactory,
28+
) {
29+
if (!$this->moduleConfig->getApiEnabled()) {
30+
throw OidcServerException::forbidden('API capabilities not enabled.');
31+
}
32+
}
33+
34+
/**
35+
* @throws AuthorizationException
36+
*/
37+
public function credentialOffer(Request $request): Response
38+
{
39+
$this->authorization->requireTokenForAnyOfScope(
40+
$request,
41+
[ApiScopesEnum::VciCredentialOffer, ApiScopesEnum::VciAll, ApiScopesEnum::All],
42+
);
43+
44+
$input = $request->getPayload()->all();
45+
46+
// Currently, we need a dedicated client for which the PreAuthZed code will be bound to.
47+
// TODO mivanci: Remove requirement for dedicated client for authorization codes.
48+
$client = $this->clientEntityFactory->getGenericForVciPreAuthZFlow();
49+
50+
dd($this->verifiableCredentials->helpers()->arr()->hybridSort($input['user_attributes']));
51+
52+
return new JsonResponse(['ok']);
53+
}
54+
}

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ public function credential(Request $request): Response
260260
// Normalize to string for single array values.
261261
$attributeValue = is_array($userAttributes[$userAttributeName]) &&
262262
count($userAttributes[$userAttributeName]) === 1 ?
263-
reset($userAttributes[$userAttributeName]) :
264-
$userAttributes[$userAttributeName];
263+
reset($userAttributes[$userAttributeName]) :
264+
$userAttributes[$userAttributeName];
265265

266266
if ($credentialFormatId === CredentialFormatIdentifiersEnum::JwtVcJson->value) {
267267
$this->verifiableCredentials->helpers()->arr()->setNestedValue(

src/Factories/Entities/ClientEntityFactory.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use SimpleSAML\Module\oidc\Entities\ClientEntity;
1212
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
1313
use SimpleSAML\Module\oidc\Helpers;
14+
use SimpleSAML\Module\oidc\ModuleConfig;
15+
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
1416
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1517
use SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor;
1618
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
@@ -29,6 +31,8 @@ public function __construct(
2931
private readonly Helpers $helpers,
3032
private readonly ClaimTranslatorExtractor $claimTranslatorExtractor,
3133
private readonly RequestParamsResolver $requestParamsResolver,
34+
private readonly ModuleConfig $moduleConfig,
35+
private readonly ClientRepository $clientRepository,
3236
) {
3337
}
3438

@@ -380,4 +384,41 @@ public function fromState(array $state): ClientEntityInterface
380384
$isFederated,
381385
);
382386
}
387+
388+
public function getGenericForVciPreAuthZFlow(): ClientEntityInterface
389+
{
390+
$clientId = 'vci_' .
391+
hash('sha256', 'vci_' . $this->moduleConfig->sspConfig()->getString('secretsalt'));
392+
393+
$clientSecret = $this->helpers->random()->getIdentifier();
394+
395+
$credentialConfigurationIdsSupported = $this->moduleConfig->getCredentialConfigurationIdsSupported();
396+
397+
$oldClient = $this->clientRepository->findById($clientId);
398+
$createdAt = $this->helpers->dateTime()->getUtc();
399+
400+
if ($oldClient instanceof ClientEntityInterface) {
401+
$createdAt = $oldClient->getCreatedAt();
402+
}
403+
404+
$client = $this->fromData(
405+
id: $clientId,
406+
secret: $clientSecret,
407+
name: 'VCI Pre-authorized Code Generic Client',
408+
description: 'Generic client for VCI Pre-authorized Code',
409+
redirectUri: ['openid-credential-offer://'],
410+
scopes: ['openid', ...$credentialConfigurationIdsSupported],
411+
isEnabled: true,
412+
updatedAt: $this->helpers->dateTime()->getUtc(),
413+
createdAt: $createdAt,
414+
);
415+
416+
if ($oldClient === null) {
417+
$this->clientRepository->add($client);
418+
} else {
419+
$this->clientRepository->update($client);
420+
}
421+
422+
return $client;
423+
}
383424
}

src/ModuleConfig.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ class ModuleConfig
103103
final public const OPTION_CREDENTIAL_CONFIGURATIONS_SUPPORTED = 'credential_configurations_supported';
104104
final public const OPTION_USER_ATTRIBUTE_TO_CREDENTIAL_CLAIM_PATH_MAP =
105105
'user_attribute_to_credential_claim_path_map';
106+
final public const OPTION_API_ENABLED = 'api_enabled';
107+
final public const OPTION_API_TOKENS = 'api_tokens';
106108

107109
protected static array $standardScopes = [
108110
ScopesEnum::OpenId->value => [
@@ -931,4 +933,38 @@ public function getUserAttributeToCredentialClaimPathMapFor(string $credentialCo
931933
{
932934
return $this->getUserAttributeToCredentialClaimPathMap()[$credentialConfigurationId] ?? [];
933935
}
936+
937+
938+
939+
/*****************************************************************************************************************
940+
* API-related config.
941+
****************************************************************************************************************/
942+
943+
public function getApiEnabled(): bool
944+
{
945+
return $this->config()->getOptionalBoolean(self::OPTION_API_ENABLED, false);
946+
}
947+
948+
/**
949+
* @return mixed[]|null
950+
*/
951+
public function getApiTokens(): ?array
952+
{
953+
return $this->config()->getOptionalArray(self::OPTION_API_TOKENS, null);
954+
}
955+
956+
/**
957+
* @param string $token
958+
* @return mixed[]
959+
*/
960+
public function getApiTokenScopes(string $token): ?array
961+
{
962+
$tokenScopes = $this->getApiTokens()[$token] ?? null;
963+
964+
if (is_array($tokenScopes)) {
965+
return $tokenScopes;
966+
}
967+
968+
return null;
969+
}
934970
}

src/Services/Api/Authorization.php

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Services\Api;
6+
7+
use SimpleSAML\Locale\Translate;
8+
use SimpleSAML\Module\oidc\Bridges\SspBridge;
9+
use SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum;
10+
use SimpleSAML\Module\oidc\Exceptions\AuthorizationException;
11+
use SimpleSAML\Module\oidc\ModuleConfig;
12+
use Symfony\Component\HttpFoundation\Request;
13+
use Throwable;
14+
15+
class Authorization
16+
{
17+
public const KEY_TOKEN = 'token';
18+
19+
public const KEY_AUTHORIZATION = 'Authorization';
20+
21+
public function __construct(
22+
protected readonly ModuleConfig $moduleConfig,
23+
protected readonly SspBridge $sspBridge,
24+
) {
25+
}
26+
27+
28+
/**
29+
* @throws AuthorizationException
30+
*/
31+
public function requireSimpleSAMLphpAdmin(bool $forceAdminAuthentication = false): void
32+
{
33+
if ($forceAdminAuthentication) {
34+
try {
35+
$this->sspBridge->utils()->auth()->requireAdmin();
36+
} catch (\Throwable $exception) {
37+
throw new AuthorizationException(
38+
Translate::noop('Unable to initiate admin authentication.'),
39+
$exception->getCode(),
40+
$exception,
41+
);
42+
}
43+
}
44+
45+
if (! $this->sspBridge->utils()->auth()->isAdmin()) {
46+
throw new AuthorizationException(Translate::noop('SimpleSAMLphp Admin access required.'));
47+
}
48+
}
49+
50+
/**
51+
* @param ApiScopesEnum[] $requiredScopes
52+
*
53+
* @throws AuthorizationException
54+
*/
55+
public function requireTokenForAnyOfScope(Request $request, array $requiredScopes): void
56+
{
57+
try {
58+
$this->requireSimpleSAMLphpAdmin();
59+
return;
60+
} catch (Throwable) {
61+
// Not admin, check for token.
62+
}
63+
64+
if (empty($token = $this->findToken($request))) {
65+
throw new AuthorizationException(Translate::noop('Token not provided.'));
66+
}
67+
68+
if (empty($tokenScopes = $this->moduleConfig->getApiTokenScopes($token))) {
69+
throw new AuthorizationException(Translate::noop('Token does not have defined scopes.'));
70+
}
71+
72+
$hasAny = !empty(array_filter($tokenScopes, fn($tokenScope) => in_array($tokenScope, $requiredScopes, true)));
73+
74+
if (!$hasAny) {
75+
throw new AuthorizationException(Translate::noop('Token is not authorized.'));
76+
}
77+
}
78+
79+
protected function findToken(Request $request): ?string
80+
{
81+
if ($token = trim((string) $request->get(self::KEY_TOKEN))) {
82+
return $token;
83+
}
84+
85+
if ($request->headers->has(self::KEY_AUTHORIZATION)) {
86+
return trim(
87+
(string) preg_replace(
88+
'/^\s*Bearer\s/',
89+
'',
90+
(string)$request->headers->get(self::KEY_AUTHORIZATION),
91+
),
92+
);
93+
}
94+
95+
return null;
96+
}
97+
}

0 commit comments

Comments
 (0)