Skip to content

Commit 9e7e094

Browse files
committed
WIP
1 parent 6594667 commit 9e7e094

File tree

4 files changed

+221
-29
lines changed

4 files changed

+221
-29
lines changed

src/Controllers/Admin/VerifiableCredentailsTestController.php

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace SimpleSAML\Module\oidc\Controllers\Admin;
66

7+
use DateTimeImmutable;
78
use SimpleSAML\Auth\Simple;
89
use SimpleSAML\Module\oidc\Admin\Authorization;
910
use SimpleSAML\Module\oidc\Bridges\SspBridge;
@@ -183,25 +184,7 @@ public function verifiableCredentialIssuance(Request $request): Response
183184
$this->clientRepository->update($client);
184185
}
185186

186-
$authCodeId = $this->sspBridge->utils()->random()->generateID();
187187

188-
// TODO mivanci Add indication of preAuthZ code to the auth code table.
189-
190-
if (($authCode = $this->authCodeRepository->findById($authCodeId)) === null) {
191-
$authCode = $this->authCodeEntityFactory->fromData(
192-
id: $authCodeId,
193-
client: $client,
194-
scopes: [
195-
new ScopeEntity('openid'),
196-
new ScopeEntity($selectedCredentialConfigurationId),
197-
],
198-
expiryDateTime: new \DateTimeImmutable('+10 minutes'),
199-
userIdentifier: $userId,
200-
redirectUri: 'openid-credential-offer://',
201-
);
202-
203-
$this->authCodeRepository->persistNewAuthCode($authCode);
204-
}
205188

206189
$credentialOffer = $this->verifiableCredentials->credentialOfferFactory()->from(
207190
parameters: [

src/Controllers/Api/VciCredentialOfferController.php

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
use SimpleSAML\Module\oidc\Codebooks\ParametersEnum;
1010
use SimpleSAML\Module\oidc\Entities\ScopeEntity;
1111
use SimpleSAML\Module\oidc\Entities\UserEntity;
12+
use SimpleSAML\Module\oidc\Exceptions\AuthorizationException;
1213
use SimpleSAML\Module\oidc\Exceptions\OidcException;
14+
use SimpleSAML\Module\oidc\Factories\CredentialOfferUriFactory;
1315
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
1416
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
1517
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
@@ -45,6 +47,7 @@ public function __construct(
4547
protected readonly AuthCodeRepository $authCodeRepository,
4648
protected readonly AuthCodeEntityFactory $authCodeEntityFactory,
4749
protected readonly Routes $routes,
50+
protected readonly CredentialOfferUriFactory $credentialOfferUriFactory,
4851
) {
4952
if (!$this->moduleConfig->getApiEnabled()) {
5053
throw OidcServerException::forbidden('API capabilities not enabled.');
@@ -55,27 +58,52 @@ public function __construct(
5558
*/
5659
public function preAuthorizedCredentialOffer(Request $request): Response
5760
{
58-
$this->authorization->requireTokenForAnyOfScope(
59-
$request,
60-
[ApiScopesEnum::VciCredentialOffer, ApiScopesEnum::VciAll, ApiScopesEnum::All],
61+
try {
62+
$this->authorization->requireTokenForAnyOfScope(
63+
$request,
64+
[ApiScopesEnum::VciCredentialOffer, ApiScopesEnum::VciAll, ApiScopesEnum::All],
65+
);
66+
} catch (AuthorizationException $e) {
67+
return $this->routes->newJsonErrorResponse(
68+
error: 'unauthorized',
69+
description: $e->getMessage(),
70+
httpCode: Response::HTTP_UNAUTHORIZED,
71+
);
72+
}
73+
74+
$input = $request->getPayload()->all();
75+
$userAttributes = $input['user_attributes'] ?? [];
76+
77+
$selectedCredentialConfigurationId = $input['credential_configuration_id'] ?? null;
78+
79+
if (!is_string($selectedCredentialConfigurationId)) {
80+
return $this->routes->newJsonErrorResponse(
81+
error: 'invalid_request',
82+
description: 'No credential configuration ID provided.',
83+
httpCode: Response::HTTP_BAD_REQUEST,
84+
);
85+
}
86+
87+
$credentialOfferUri = $this->credentialOfferUriFactory->buildPreAuthorized(
88+
[$selectedCredentialConfigurationId],
89+
$userAttributes,
6190
);
6291

63-
// Currently, we need a dedicated client for which the PreAuthZed code will be bound to.
64-
// TODO mivanci: Remove requirement for dedicated client for (pre-)authorization codes.
92+
// TODO mivanci continue
93+
dd($credentialOfferUri);
94+
95+
96+
/////////
97+
6598
$client = $this->clientEntityFactory->getGenericForVciPreAuthZFlow();
6699
if ($this->clientRepository->findById($client->getIdentifier()) === null) {
67100
$this->clientRepository->add($client);
68101
} else {
69102
$this->clientRepository->update($client);
70103
}
71104

72-
$input = $request->getPayload()->all();
73-
$userAttributes = $input['user_attributes'] ?? [];
74105

75-
$selectedCredentialConfigurationId = $input['credential_configuration_id'] ?? null;
76-
if ($selectedCredentialConfigurationId === null) {
77-
throw new OidcException('No credential configuration ID provided.');
78-
}
106+
79107
$credentialConfigurationIdsSupported = $this->moduleConfig->getCredentialConfigurationIdsSupported();
80108

81109
if (empty($credentialConfigurationIdsSupported)) {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Factories;
6+
7+
use DateTimeImmutable;
8+
use RuntimeException;
9+
use SimpleSAML\Module\oidc\Bridges\SspBridge;
10+
use SimpleSAML\Module\oidc\Codebooks\ParametersEnum;
11+
use SimpleSAML\Module\oidc\Entities\ScopeEntity;
12+
use SimpleSAML\Module\oidc\Entities\UserEntity;
13+
use SimpleSAML\Module\oidc\Exceptions\OidcException;
14+
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
15+
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
16+
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
17+
use SimpleSAML\Module\oidc\ModuleConfig;
18+
use SimpleSAML\Module\oidc\Repositories\AuthCodeRepository;
19+
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
20+
use SimpleSAML\Module\oidc\Repositories\UserRepository;
21+
use SimpleSAML\Module\oidc\Services\LoggerService;
22+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
23+
use SimpleSAML\OpenID\Codebooks\GrantTypesEnum;
24+
use SimpleSAML\OpenID\VerifiableCredentials;
25+
26+
class CredentialOfferUriFactory
27+
{
28+
public function __construct(
29+
protected readonly VerifiableCredentials $verifiableCredentials,
30+
protected readonly ModuleConfig $moduleConfig,
31+
protected readonly SspBridge $sspBridge,
32+
protected readonly AuthCodeRepository $authCodeRepository,
33+
protected readonly AuthCodeEntityFactory $authCodeEntityFactory,
34+
protected readonly ClientEntityFactory $clientEntityFactory,
35+
protected readonly ClientRepository $clientRepository,
36+
protected readonly LoggerService $loggerService,
37+
protected readonly UserRepository $userRepository,
38+
protected readonly UserEntityFactory $userEntityFactory,
39+
)
40+
{
41+
}
42+
43+
/**
44+
* @param string[] $credentialConfigurationIds
45+
* @throws OidcException
46+
*/
47+
public function buildPreAuthorized(
48+
array $credentialConfigurationIds,
49+
array $userAttributes,
50+
): string
51+
{
52+
if (empty($credentialConfigurationIds)) {
53+
throw new RuntimeException('No credential configuration IDs provided.');
54+
}
55+
56+
$credentialConfigurationIdsSupported = $this->moduleConfig->getCredentialConfigurationIdsSupported();
57+
58+
if (empty($credentialConfigurationIdsSupported)) {
59+
throw new RuntimeException('No credential configuration IDs configured.');
60+
}
61+
62+
if (array_diff($credentialConfigurationIds, $credentialConfigurationIdsSupported)) {
63+
throw new RuntimeException('Unsupported credential configuration IDs provided.');
64+
}
65+
66+
/* TODO mivanci TX Code handling
67+
$email = $this->emailFactory->build(
68+
subject: 'VC Issuance Transaction code',
69+
70+
);
71+
72+
$email->setData(['Transaction Code' => '1234']);
73+
try {
74+
$email->send();
75+
$this->sessionMessagesService->addMessage('Email with tx code sent to: [email protected]');
76+
} catch (Exception $e) {
77+
$this->sessionMessagesService->addMessage('Error emailing tx code.');
78+
}
79+
*/
80+
81+
// TODO mivanci Wallet (client) credential_offer_endpoint metadata
82+
// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#client-metadata
83+
84+
$scopes = array_map(
85+
fn (string $scope) => new ScopeEntity($scope),
86+
['openid', ...$credentialConfigurationIds],
87+
);
88+
89+
// Currently, we need a dedicated client for which the PreAuthZed code will be bound to.
90+
// TODO mivanci: Remove requirement for dedicated client for (pre-)authorization codes.
91+
$client = $this->clientEntityFactory->getGenericForVciPreAuthZFlow();
92+
if ($this->clientRepository->findById($client->getIdentifier()) === null) {
93+
$this->clientRepository->add($client);
94+
} else {
95+
$this->clientRepository->update($client);
96+
}
97+
98+
$userId = null;
99+
try {
100+
$userId = $this->sspBridge->utils()->attributes()->getExpectedAttribute(
101+
$userAttributes,
102+
$this->moduleConfig->getUserIdentifierAttribute(),
103+
);
104+
} catch (\Throwable $e) {
105+
$this->loggerService->warning(
106+
'Could not extract user identifier from user attributes: ' . $e->getMessage(),
107+
);
108+
}
109+
110+
if ($userId === null) {
111+
$sortedAttributes = $userAttributes;
112+
$this->verifiableCredentials->helpers()->arr()->hybridSort($sortedAttributes);
113+
$userId = 'vci_preauthz_' . hash('sha256', serialize($sortedAttributes));
114+
}
115+
116+
$oldUserEntity = $this->userRepository->getUserEntityByIdentifier($userId);
117+
118+
$userEntity = $this->userEntityFactory->fromData($userId, $userAttributes);
119+
120+
if ($oldUserEntity instanceof UserEntity) {
121+
$this->userRepository->update($userEntity);
122+
} else {
123+
$this->userRepository->add($userEntity);
124+
}
125+
126+
$authCodeId = null;
127+
$authCodeIdGenerationAttempts = 3;
128+
while ($authCodeIdGenerationAttempts > 0) {
129+
$authCodeId = $this->sspBridge->utils()->random()->generateID();
130+
if ($this->authCodeRepository->findById($authCodeId) === null) {
131+
break;
132+
}
133+
$authCodeIdGenerationAttempts--;
134+
}
135+
136+
if ($authCodeId === null) {
137+
throw new RuntimeException('Failed to generate Authorization Code ID.');
138+
}
139+
140+
// TODO mivanci Add indication of preAuthZ code to the auth code table.
141+
$authCode = $this->authCodeEntityFactory->fromData(
142+
id: $authCodeId,
143+
client: $client,
144+
scopes: $scopes,
145+
expiryDateTime: (new DateTimeImmutable())->add($this->moduleConfig->getAuthCodeDuration()),
146+
userIdentifier: $userId,
147+
redirectUri: 'openid-credential-offer://',
148+
);
149+
$this->authCodeRepository->persistNewAuthCode($authCode);
150+
151+
$credentialOffer = $this->verifiableCredentials->credentialOfferFactory()->from(
152+
parameters: [
153+
ClaimsEnum::CredentialIssuer->value => $this->moduleConfig->getIssuer(),
154+
ClaimsEnum::CredentialConfigurationIds->value => [
155+
...$credentialConfigurationIds,
156+
],
157+
ClaimsEnum::Grants->value => [
158+
GrantTypesEnum::PreAuthorizedCode->value => [
159+
ClaimsEnum::PreAuthorizedCode->value => $authCode->getIdentifier(),
160+
// TODO mivanci support for TxCode
161+
// ClaimsEnum::TxCode->value => [
162+
// ClaimsEnum::InputMode->value => 'numeric',
163+
// ClaimsEnum::Length->value => 6,
164+
// ClaimsEnum::Description->value => 'Sent to user mail',
165+
// ],
166+
],
167+
],
168+
],
169+
);
170+
171+
$credentialOfferValue = $credentialOffer->jsonSerialize();
172+
$parameterName = ParametersEnum::CredentialOfferUri->value;
173+
if (is_array($credentialOfferValue)) {
174+
$parameterName = ParametersEnum::CredentialOffer->value;
175+
$credentialOfferValue = json_encode($credentialOfferValue);
176+
}
177+
178+
return "openid-credential-offer://?$parameterName=$credentialOfferValue";
179+
}
180+
}

src/Services/Container.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ public function __construct()
235235
$helpers,
236236
$claimTranslatorExtractor,
237237
$requestParamsResolver,
238+
$moduleConfig,
238239
);
239240
$this->services[ClientEntityFactory::class] = $clientEntityFactory;
240241

0 commit comments

Comments
 (0)