Skip to content

Commit 6342151

Browse files
committed
Include AtContext in credential metadata
1 parent 9ef3ef8 commit 6342151

6 files changed

Lines changed: 107 additions & 31 deletions

File tree

routing/services/services.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ services:
9797
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
9898
SimpleSAML\Module\oidc\Utils\JwksResolver: ~
9999
SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver: ~
100+
SimpleSAML\Module\oidc\Utils\VciContextResolver: ~
100101
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
101102
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
102103
SimpleSAML\Module\oidc\Utils\FederationCache:

src/Controllers/VerifiableCredentials/CredentialIssuerConfigurationController.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
1818
use SimpleSAML\Module\oidc\Services\LoggerService;
1919
use SimpleSAML\Module\oidc\Utils\Routes;
20+
use SimpleSAML\Module\oidc\Utils\VciContextResolver;
2021
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
22+
use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum;
2123
use Symfony\Component\HttpFoundation\Response;
2224

2325
class CredentialIssuerConfigurationController
@@ -29,6 +31,7 @@ public function __construct(
2931
protected readonly ModuleConfig $moduleConfig,
3032
protected readonly Routes $routes,
3133
protected readonly LoggerService $loggerService,
34+
protected readonly VciContextResolver $vciContextResolver,
3235
) {
3336
if (!$this->moduleConfig->getVciEnabled()) {
3437
$this->loggerService->warning('Verifiable Credential capabilities not enabled.');
@@ -47,6 +50,7 @@ public function configuration(): Response
4750
// For now, we only support one credential signing algorithm.
4851
/** @psalm-suppress MixedAssignment */
4952
foreach ($credentialConfigurationsSupported as $credentialConfigurationId => $credentialConfiguration) {
53+
$credentialConfigurationId = (string) $credentialConfigurationId;
5054
if (is_array($credentialConfiguration)) {
5155
$credentialConfiguration[ClaimsEnum::CredentialSigningAlgValuesSupported->value] = [
5256
$signatureKeyPair->getSignatureAlgorithm()->value,
@@ -63,6 +67,25 @@ public function configuration(): Response
6367
->getAllNamesUnique(),
6468
],
6569
];
70+
71+
$credentialFormatId = $credentialConfiguration[ClaimsEnum::Format->value] ?? null;
72+
73+
if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) {
74+
$atContext = $this->vciContextResolver->resolve(
75+
$credentialConfigurationId,
76+
$credentialConfiguration,
77+
);
78+
79+
/** @psalm-suppress MixedArrayAccess */
80+
if (isset($credentialConfiguration[ClaimsEnum::CredentialDefinition->value])) {
81+
/** @psalm-suppress MixedArrayAssignment */
82+
$credentialConfiguration[ClaimsEnum::CredentialDefinition->value][ClaimsEnum::AtContext->value]
83+
= $atContext;
84+
} else {
85+
$credentialConfiguration[ClaimsEnum::AtContext->value] = $atContext;
86+
}
87+
}
88+
6689
$credentialConfigurationsSupported[$credentialConfigurationId] = $credentialConfiguration;
6790
}
6891
}

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use SimpleSAML\Module\oidc\Services\NonceService;
1818
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
1919
use SimpleSAML\Module\oidc\Utils\Routes;
20+
use SimpleSAML\Module\oidc\Utils\VciContextResolver;
2021
use SimpleSAML\OpenID\Codebooks\AtContextsEnum;
2122
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2223
use SimpleSAML\OpenID\Codebooks\CredentialFormatIdentifiersEnum;
@@ -53,6 +54,7 @@ public function __construct(
5354
protected readonly Did $did,
5455
protected readonly IssuerStateRepository $issuerStateRepository,
5556
protected readonly NonceService $nonceService,
57+
protected readonly VciContextResolver $vciContextResolver,
5658
) {
5759
if (!$this->moduleConfig->getVciEnabled()) {
5860
$this->loggerService->warning('Verifiable Credential capabilities not enabled.');
@@ -762,37 +764,10 @@ public function credential(Request $request): Response
762764
}
763765

764766
if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) {
765-
// Always start with the VCDM 2.0 base context URL (mandatory).
766-
$atContext = [AtContextsEnum::W3OrgNsCredentialsV2->value];
767-
768-
// If a JSON-LD context document is configured for this credential,
769-
// append the module-hosted context URL so that verifiers can
770-
// resolve the custom credential subject terms.
771-
if ($this->moduleConfig->getVciCredentialJsonLdContextFor($resolvedCredentialIdentifier) !== null) {
772-
$atContext[] = $this->routes->urlCredentialJsonLdContext($resolvedCredentialIdentifier);
773-
}
774-
775-
// Append any additional context URLs declared in the credential
776-
// configuration's @context field (skipping the base W3C URL,
777-
// which is already first in the list).
778-
/**
779-
* @psalm-suppress MixedArrayAccess
780-
* @psalm-suppress MixedAssignment
781-
*/
782-
$configuredContexts = $resolvedCredentialConfiguration[ClaimsEnum::CredentialDefinition->value]
783-
[ClaimsEnum::AtContext->value] ?? $resolvedCredentialConfiguration[ClaimsEnum::AtContext->value] ?? [];
784-
if (is_array($configuredContexts)) {
785-
/** @psalm-suppress MixedAssignment */
786-
foreach ($configuredContexts as $configuredContext) {
787-
if (
788-
is_string($configuredContext) &&
789-
$configuredContext !== AtContextsEnum::W3OrgNsCredentialsV2->value &&
790-
!in_array($configuredContext, $atContext, true)
791-
) {
792-
$atContext[] = $configuredContext;
793-
}
794-
}
795-
}
767+
$atContext = $this->vciContextResolver->resolve(
768+
$resolvedCredentialIdentifier,
769+
$resolvedCredentialConfiguration,
770+
);
796771

797772
$sdJwtPayload = [
798773
ClaimsEnum::AtContext->value => $atContext,
@@ -839,6 +814,7 @@ public function credential(Request $request): Response
839814
$this->loggerService->debug(
840815
'Verifiable credential issued successfully.',
841816
['token' => substr($token, 0, 20) . '...'],
817+
//['token' => $token],
842818
);
843819
}
844820

src/Services/Container.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
use SimpleSAML\Module\oidc\Utils\ProtocolCache;
114114
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
115115
use SimpleSAML\Module\oidc\Utils\Routes;
116+
use SimpleSAML\Module\oidc\Utils\VciContextResolver;
116117
use SimpleSAML\OpenID\Core;
117118
use SimpleSAML\OpenID\Federation;
118119
use SimpleSAML\OpenID\Jwks;
@@ -183,6 +184,9 @@ public function __construct()
183184
);
184185
$this->services[Routes::class] = $routes;
185186

187+
$vciContextResolver = new VciContextResolver($moduleConfig, $routes);
188+
$this->services[VciContextResolver::class] = $vciContextResolver;
189+
186190
$templateFactory = new TemplateFactory(
187191
$simpleSAMLConfiguration,
188192
$moduleConfig,

src/Utils/VciContextResolver.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Utils;
6+
7+
use SimpleSAML\Module\oidc\ModuleConfig;
8+
use SimpleSAML\OpenID\Codebooks\AtContextsEnum;
9+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
10+
11+
class VciContextResolver
12+
{
13+
/**
14+
* VciContextResolver constructor.
15+
* @param ModuleConfig $moduleConfig
16+
* @param Routes $routes
17+
*/
18+
public function __construct(
19+
protected readonly ModuleConfig $moduleConfig,
20+
protected readonly Routes $routes,
21+
) {
22+
}
23+
24+
/**
25+
* Resolve the @context array for a given credential configuration.
26+
*
27+
* @param string $credentialConfigurationId
28+
* @param array $credentialConfiguration
29+
* @return array<string>
30+
*/
31+
public function resolve(string $credentialConfigurationId, array $credentialConfiguration): array
32+
{
33+
// Always start with the VCDM 2.0 base context URL (mandatory).
34+
$atContext = [AtContextsEnum::W3OrgNsCredentialsV2->value];
35+
36+
// If a JSON-LD context document is configured for this credential,
37+
// append the module-hosted context URL so that verifiers can
38+
// resolve the custom credential subject terms.
39+
if ($this->moduleConfig->getVciCredentialJsonLdContextFor($credentialConfigurationId) !== null) {
40+
$atContext[] = $this->routes->urlCredentialJsonLdContext($credentialConfigurationId);
41+
}
42+
43+
// Append any additional context URLs declared in the credential
44+
// configuration's @context field (skipping the base W3C URL,
45+
// which is already first in the list).
46+
/**
47+
* @psalm-suppress MixedArrayAccess
48+
* @psalm-suppress MixedAssignment
49+
*/
50+
$configuredContexts = $credentialConfiguration[ClaimsEnum::CredentialDefinition->value]
51+
[ClaimsEnum::AtContext->value] ?? $credentialConfiguration[ClaimsEnum::AtContext->value] ?? [];
52+
53+
if (is_array($configuredContexts)) {
54+
/** @psalm-suppress MixedAssignment */
55+
foreach ($configuredContexts as $configuredContext) {
56+
if (
57+
is_string($configuredContext) &&
58+
$configuredContext !== AtContextsEnum::W3OrgNsCredentialsV2->value &&
59+
!in_array($configuredContext, $atContext, true)
60+
) {
61+
$atContext[] = $configuredContext;
62+
}
63+
}
64+
}
65+
66+
return $atContext;
67+
}
68+
}

tests/unit/src/Controllers/VerifiableCredentials/CredentialIssuerCredentialControllerTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use SimpleSAML\Module\oidc\Services\NonceService;
2323
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
2424
use SimpleSAML\Module\oidc\Utils\Routes;
25+
use SimpleSAML\Module\oidc\Utils\VciContextResolver;
2526
use SimpleSAML\OpenID\Algorithms\SignatureAlgorithmEnum;
2627
use SimpleSAML\OpenID\Did;
2728
use SimpleSAML\OpenID\Did\DidJwkResolver;
@@ -54,6 +55,7 @@ class CredentialIssuerCredentialControllerTest extends TestCase
5455
protected MockObject $didMock;
5556
protected MockObject $issuerStateRepositoryMock;
5657
protected MockObject $nonceServiceMock;
58+
protected MockObject $vciContextResolverMock;
5759

5860
public function setUp(): void
5961
{
@@ -69,6 +71,7 @@ public function setUp(): void
6971
$this->didMock = $this->createMock(Did::class);
7072
$this->issuerStateRepositoryMock = $this->createMock(IssuerStateRepository::class);
7173
$this->nonceServiceMock = $this->createMock(NonceService::class);
74+
$this->vciContextResolverMock = $this->createMock(VciContextResolver::class);
7275

7376
// VCI must be enabled in constructor
7477
$this->moduleConfigMock->method('getVciEnabled')->willReturn(true);
@@ -186,6 +189,7 @@ public function testCredentialWithMultipleProofs(): void
186189
$this->didMock,
187190
$this->issuerStateRepositoryMock,
188191
$this->nonceServiceMock,
192+
$this->vciContextResolverMock,
189193
);
190194

191195
$sut->credential($request);

0 commit comments

Comments
 (0)