Skip to content

Commit 2dd5315

Browse files
committed
WIP
1 parent 3579417 commit 2dd5315

File tree

11 files changed

+275
-43
lines changed

11 files changed

+275
-43
lines changed

config/module_oidc.php.dist

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,20 @@ $config = [
790790
],
791791
],
792792

793+
// Map of authentication sources and user's email attribute names. This enables you to define a specific attribute
794+
// name which contains the user's email address, per authentication source. This is used, for example, to send
795+
// Transaction Code in the case of pre-authorized codes for verifiable credential issuance. If not set, the
796+
// default user's email attribute name will be used (see the option below).
797+
//
798+
// Format is: 'authentication-source-id' => 'email-attribute-name'.
799+
ModuleConfig::OPTION_AUTH_SOURCES_TO_USERS_EMAIL_ATTRIBUTE_NAME_MAP => [
800+
'example-auth-source-id' => 'mail',
801+
],
802+
803+
// The default name of the attribute which contains the user's email address. If not set, it will
804+
// fall back to 'mail'.
805+
ModuleConfig::OPTION_DEFAULT_USERS_EMAIL_ATTRIBUTE_NAME => 'mail',
806+
793807
/**
794808
* (optional) API-related options.
795809
*/

src/Controllers/Admin/VerifiableCredentailsTestController.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,11 @@
1111
use SimpleSAML\Module\oidc\Factories\AuthSimpleFactory;
1212
use SimpleSAML\Module\oidc\Factories\CredentialOfferUriFactory;
1313
use SimpleSAML\Module\oidc\Factories\EmailFactory;
14-
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
15-
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
16-
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
1714
use SimpleSAML\Module\oidc\Factories\TemplateFactory;
1815
use SimpleSAML\Module\oidc\ModuleConfig;
19-
use SimpleSAML\Module\oidc\Repositories\AuthCodeRepository;
20-
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
21-
use SimpleSAML\Module\oidc\Repositories\UserRepository;
2216
use SimpleSAML\Module\oidc\Services\LoggerService;
23-
use SimpleSAML\Module\oidc\Services\SessionMessagesService;
2417
use SimpleSAML\Module\oidc\Services\SessionService;
25-
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
2618
use SimpleSAML\Module\oidc\Utils\Routes;
27-
use SimpleSAML\OpenID\VerifiableCredentials;
2819
use Symfony\Component\HttpFoundation\Request;
2920
use Symfony\Component\HttpFoundation\Response;
3021

@@ -123,16 +114,26 @@ public function verifiableCredentialIssuance(Request $request): Response
123114

124115
$credentialOfferQrUri = null;
125116
$credentialOfferUri = null;
117+
$useTxCode = (bool) $request->get('useTxCode');
118+
$usersEmailAttributeName = $request->get('usersEmailAttributeName');
119+
$usersEmailAttributeName = is_string($usersEmailAttributeName) && (trim($usersEmailAttributeName) !== '') ?
120+
trim($usersEmailAttributeName) :
121+
null;
126122

127123
if (
128124
$authSource instanceof Simple &&
129125
$authSource->isAuthenticated()
130126
) {
131127
$userAttributes = $authSource->getAttributes();
128+
$usersEmailAttributeName ??= $this->moduleConfig->getUsersEmailAttributeNameForAuthSourceId(
129+
$authSource->getAuthSource()->getAuthId(),
130+
);
132131

133132
$credentialOfferUri = $this->credentialOfferUriFactory->buildPreAuthorized(
134133
[$selectedCredentialConfigurationId],
135134
$userAttributes,
135+
$useTxCode,
136+
$usersEmailAttributeName,
136137
);
137138

138139
// TODO mivanci Local QR code generator
@@ -142,6 +143,8 @@ public function verifiableCredentialIssuance(Request $request): Response
142143

143144
$authSourceActionRoute = $this->routes->urlAdminTestVerifiableCredentialIssuance();
144145

146+
$defaultUsersEmailAttributeName = $this->moduleConfig->getDefaultUsersEmailAttributeName();
147+
145148
return $this->templateFactory->build(
146149
'oidc:tests/verifiable-credential-issuance.twig',
147150
compact(
@@ -153,6 +156,8 @@ public function verifiableCredentialIssuance(Request $request): Response
153156
'authSource',
154157
'credentialConfigurationIdsSupported',
155158
'selectedCredentialConfigurationId',
159+
'defaultUsersEmailAttributeName',
160+
'usersEmailAttributeName',
156161
),
157162
RoutesEnum::AdminTestVerifiableCredentialIssuance->value,
158163
);

src/Controllers/Api/VciCredentialOfferController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,23 @@ public function credentialOffer(Request $request): Response
7878
);
7979
}
8080

81+
$useTxCode = boolval($input['use_tx_code'] ?? false);
82+
$usersEmailAttributeName = $input['users_email_attribute_name'] ?? null;
83+
$usersEmailAttributeName = is_string($usersEmailAttributeName) ? $usersEmailAttributeName : null;
84+
$authenticationSourceId = $input['authentication_source_id'] ?? null;
85+
$authenticationSourceId = is_string($authenticationSourceId) ? $authenticationSourceId : null;
86+
87+
if (is_null($usersEmailAttributeName) && is_string($authenticationSourceId)) {
88+
$usersEmailAttributeName = $this->moduleConfig->getUsersEmailAttributeNameForAuthSourceId(
89+
$authenticationSourceId,
90+
);
91+
}
92+
8193
$credentialOfferUri = $this->credentialOfferUriFactory->buildPreAuthorized(
8294
[$selectedCredentialConfigurationId],
8395
$userAttributes,
96+
$useTxCode,
97+
$usersEmailAttributeName,
8498
);
8599

86100
return $this->routes->newJsonResponse(

src/Entities/AuthCodeEntity.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public function __construct(
4343
?string $redirectUri = null,
4444
?string $nonce = null,
4545
bool $isRevoked = false,
46+
protected readonly bool $isPreAuthorized = false,
47+
protected readonly ?string $txCode = null,
4648
) {
4749
$this->identifier = $id;
4850
$this->client = $client;
@@ -68,6 +70,18 @@ public function getState(): array
6870
'is_revoked' => $this->isRevoked(),
6971
'redirect_uri' => $this->getRedirectUri(),
7072
'nonce' => $this->getNonce(),
73+
'is_pre_authorized' => $this->isPreAuthorized,
74+
'tx_code' => $this->txCode,
7175
];
7276
}
77+
78+
public function isPreAuthorized(): bool
79+
{
80+
return $this->isPreAuthorized;
81+
}
82+
83+
public function getTxCode(): ?string
84+
{
85+
return $this->txCode;
86+
}
7387
}

src/Factories/CredentialOfferUriFactory.php

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
use DateTimeImmutable;
88
use RuntimeException;
9+
use SimpleSAML\Error\Exception;
910
use SimpleSAML\Module\oidc\Bridges\SspBridge;
1011
use SimpleSAML\Module\oidc\Codebooks\ParametersEnum;
1112
use SimpleSAML\Module\oidc\Entities\ScopeEntity;
1213
use SimpleSAML\Module\oidc\Entities\UserEntity;
13-
use SimpleSAML\Module\oidc\Exceptions\OidcException;
1414
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
1515
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
1616
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
@@ -21,8 +21,8 @@
2121
use SimpleSAML\Module\oidc\Services\LoggerService;
2222
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2323
use SimpleSAML\OpenID\Codebooks\GrantTypesEnum;
24-
use SimpleSAML\OpenID\Exceptions\OpenIdException;
2524
use SimpleSAML\OpenID\VerifiableCredentials;
25+
use SimpleSAML\OpenID\VerifiableCredentials\TxCode;
2626

2727
class CredentialOfferUriFactory
2828
{
@@ -37,6 +37,7 @@ public function __construct(
3737
protected readonly LoggerService $loggerService,
3838
protected readonly UserRepository $userRepository,
3939
protected readonly UserEntityFactory $userEntityFactory,
40+
protected readonly EmailFactory $emailFactory,
4041
) {
4142
}
4243

@@ -47,6 +48,8 @@ public function __construct(
4748
public function buildPreAuthorized(
4849
array $credentialConfigurationIds,
4950
array $userAttributes,
51+
bool $useTxCode = false,
52+
string $userEmailAttributeName = null,
5053
): string {
5154
if (empty($credentialConfigurationIds)) {
5255
throw new RuntimeException('No credential configuration IDs provided.');
@@ -62,21 +65,6 @@ public function buildPreAuthorized(
6265
throw new RuntimeException('Unsupported credential configuration IDs provided.');
6366
}
6467

65-
/* TODO mivanci TX Code handling
66-
$email = $this->emailFactory->build(
67-
subject: 'VC Issuance Transaction code',
68-
69-
);
70-
71-
$email->setData(['Transaction Code' => '1234']);
72-
try {
73-
$email->send();
74-
$this->sessionMessagesService->addMessage('Email with tx code sent to: [email protected]');
75-
} catch (Exception $e) {
76-
$this->sessionMessagesService->addMessage('Error emailing tx code.');
77-
}
78-
*/
79-
8068
// TODO mivanci Wallet (client) credential_offer_endpoint metadata
8169
// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#client-metadata
8270

@@ -103,13 +91,19 @@ public function buildPreAuthorized(
10391
} catch (\Throwable $e) {
10492
$this->loggerService->warning(
10593
'Could not extract user identifier from user attributes: ' . $e->getMessage(),
94+
$userAttributes,
10695
);
10796
}
10897

10998
if ($userId === null) {
99+
$this->loggerService->warning('Falling back to user attributes hash for user identifier.');
110100
$sortedAttributes = $userAttributes;
111101
$this->verifiableCredentials->helpers()->arr()->hybridSort($sortedAttributes);
112-
$userId = 'vci_preauthz_' . hash('sha256', serialize($sortedAttributes));
102+
$userId = 'vci_credential_offer_preauthz_' . hash('sha256', serialize($sortedAttributes));
103+
$this->loggerService->info(
104+
'Generated user identifier based on user attributes: ' . $userId,
105+
$userAttributes,
106+
);
113107
}
114108

115109
$oldUserEntity = $this->userRepository->getUserEntityByIdentifier($userId);
@@ -136,14 +130,28 @@ public function buildPreAuthorized(
136130
throw new RuntimeException('Failed to generate Authorization Code ID.');
137131
}
138132

139-
// TODO mivanci Add indication of preAuthZ code to the auth code table.
133+
$txCode = null;
134+
$userEmail = null;
135+
$userEmailAttributeName ??= $this->moduleConfig->getDefaultUsersEmailAttributeName();
136+
if ($useTxCode) {
137+
$userEmail = $this->getUserEmail($userEmailAttributeName, $userAttributes);
138+
$txCodeDescription = 'Please provide the one-time code that was sent to e-mail ' . $userEmail;
139+
$txCode = $this->buildTxCode($txCodeDescription);
140+
$this->loggerService->debug(
141+
'Generated TxCode for sending by email: ' . $txCode->getCodeAsString(),
142+
$txCode->jsonSerialize(),
143+
);
144+
}
145+
140146
$authCode = $this->authCodeEntityFactory->fromData(
141147
id: $authCodeId,
142148
client: $client,
143149
scopes: $scopes,
144150
expiryDateTime: (new DateTimeImmutable())->add($this->moduleConfig->getAuthCodeDuration()),
145151
userIdentifier: $userId,
146152
redirectUri: 'openid-credential-offer://',
153+
isPreAuthorized: true,
154+
txCode: $txCode instanceof VerifiableCredentials\TxCode ? $txCode->getCodeAsString() : null,
147155
);
148156
$this->authCodeRepository->persistNewAuthCode($authCode);
149157

@@ -156,17 +164,22 @@ public function buildPreAuthorized(
156164
ClaimsEnum::Grants->value => [
157165
GrantTypesEnum::PreAuthorizedCode->value => [
158166
ClaimsEnum::PreAuthorizedCode->value => $authCode->getIdentifier(),
159-
// TODO mivanci support for TxCode
160-
// ClaimsEnum::TxCode->value => [
161-
// ClaimsEnum::InputMode->value => 'numeric',
162-
// ClaimsEnum::Length->value => 6,
163-
// ClaimsEnum::Description->value => 'Sent to user mail',
164-
// ],
167+
...(array_filter(
168+
[
169+
ClaimsEnum::TxCode->value => $txCode instanceof VerifiableCredentials\TxCode ?
170+
$txCode->jsonSerialize() :
171+
null,
172+
],
173+
)),
165174
],
166175
],
167176
],
168177
);
169178

179+
if ($txCode instanceof VerifiableCredentials\TxCode && $userEmail !== null) {
180+
$this->sendTxCodeByEmail($txCode, $userEmail);
181+
}
182+
170183
$credentialOfferValue = $credentialOffer->jsonSerialize();
171184
$parameterName = ParametersEnum::CredentialOfferUri->value;
172185
if (is_array($credentialOfferValue)) {
@@ -176,4 +189,60 @@ public function buildPreAuthorized(
176189

177190
return "openid-credential-offer://?$parameterName=$credentialOfferValue";
178191
}
192+
193+
/**
194+
* @param mixed[] $userAttributes
195+
* @throws RuntimeException
196+
*/
197+
public function getUserEmail(string $userEmailAttributeName, array $userAttributes): string
198+
{
199+
try {
200+
$userEmail = $this->sspBridge->utils()->attributes()->getExpectedAttribute(
201+
$userAttributes,
202+
$userEmailAttributeName,
203+
true,
204+
);
205+
} catch (Exception $e) {
206+
throw new RuntimeException('Could not extract user email from user attributes: ' . $e->getMessage());
207+
}
208+
209+
if (!is_string($userEmail)) {
210+
throw new RuntimeException('User email attribute value is not a string.');
211+
}
212+
213+
return $userEmail;
214+
}
215+
216+
public function buildTxCode(
217+
string $description,
218+
int|string $txCode = null,
219+
): TxCode {
220+
$txCode ??= rand(1000, 9999);
221+
222+
return $this->verifiableCredentials->txCodeFactory()->build(
223+
$txCode,
224+
$description,
225+
);
226+
}
227+
228+
/**
229+
* @throws OidcException
230+
*/
231+
public function sendTxCodeByEmail(TxCode $txCode, string $email, string $subject = null): void
232+
{
233+
$subject ??= 'Your one-time code';
234+
235+
$email = $this->emailFactory->build(
236+
subject: $subject,
237+
to: $email,
238+
);
239+
240+
$email->setText('Use the following code to complete the transaction.');
241+
242+
$email->setData([
243+
'Transaction Code' => $txCode->getCodeAsString(),
244+
]);
245+
246+
$email->send();
247+
}
179248
}

src/Factories/Entities/AuthCodeEntityFactory.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public function fromData(
3131
?string $redirectUri = null,
3232
?string $nonce = null,
3333
bool $isRevoked = false,
34+
bool $isPreAuthorized = false,
35+
?string $txCode = null,
3436
): AuthCodeEntity {
3537
return new AuthCodeEntity(
3638
$id,
@@ -41,6 +43,8 @@ public function fromData(
4143
$redirectUri,
4244
$nonce,
4345
$isRevoked,
46+
$isPreAuthorized,
47+
$txCode,
4448
);
4549
}
4650

@@ -81,6 +85,8 @@ public function fromState(array $state): AuthCodeEntity
8185
$redirectUri = empty($state['redirect_uri']) ? null : (string)$state['redirect_uri'];
8286
$nonce = empty($state['nonce']) ? null : (string)$state['nonce'];
8387
$isRevoked = (bool) $state['is_revoked'];
88+
$isPreAuthorized = (bool) $state['is_pre_authorized'];
89+
$txCode = empty($state['tx_code']) ? null : (string)$state['tx_code'];
8490

8591
return $this->fromData(
8692
$id,
@@ -91,6 +97,8 @@ public function fromState(array $state): AuthCodeEntity
9197
$redirectUri,
9298
$nonce,
9399
$isRevoked,
100+
$isPreAuthorized,
101+
$txCode,
94102
);
95103
}
96104
}

0 commit comments

Comments
 (0)