Skip to content

Commit 06bebe8

Browse files
committed
Start with JSON-LD
1 parent b80da17 commit 06bebe8

7 files changed

Lines changed: 222 additions & 3 deletions

File tree

config/module_oidc.php.dist

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,62 @@ $config = [
11061106
],
11071107
],
11081108

1109+
/**
1110+
* (optional) JSON-LD context documents for vc+sd-jwt credential
1111+
* configurations. Providing context documents allows verifiers to resolve
1112+
* the meaning of custom terms (claims) used in the credentialSubject of
1113+
* issued vc+sd-jwt credentials, which is required for VCDM 2.0 JSON-LD
1114+
* compliance.
1115+
*
1116+
* When a context document is configured for a credential configuration ID,
1117+
* the module:
1118+
* 1. Serves the document at a stable HTTP endpoint on the credential issuer:
1119+
* <issuer-base-url>/credential-issuer/context/{credentialConfigurationId}
1120+
* with the Content-Type: application/ld+json header.
1121+
* 2. Automatically appends the URL of that endpoint after the mandatory
1122+
* https://www.w3.org/ns/credentials/v2 entry in the @context array of
1123+
* every vc+sd-jwt credential issued for the matching configuration ID.
1124+
*
1125+
* The format is a map of credential configuration IDs to JSON-LD context
1126+
* documents. Each context document is a plain PHP array that will be
1127+
* JSON-encoded and served as-is. You can use any valid JSON-LD context
1128+
* structure; the example below shows a simple term-mapping context.
1129+
*
1130+
* Notes:
1131+
* - Avoid @base or @vocab in the context document to maintain
1132+
* interoperability with lightweight (type-specific) credential
1133+
* processors, per the VCDM 2.0 specification.
1134+
* - If some custom claims are NOT defined in any context, you should add
1135+
* 'https://www.w3.org/ns/credentials/undefined-terms/v2' as the last
1136+
* entry in the @context array of the credential configuration
1137+
* (OPTION_VCI_CREDENTIAL_CONFIGURATIONS_SUPPORTED).
1138+
*
1139+
* @see https://www.w3.org/TR/vc-data-model-2.0/#contexts
1140+
* @see https://www.w3.org/TR/json-ld11/#the-context
1141+
*
1142+
* Format: array<string, array<mixed>>
1143+
*/
1144+
// ModuleConfig::OPTION_VCI_CREDENTIAL_JSON_LD_CONTEXT => [
1145+
// 'ResearchAndScholarshipCredentialVcSdJwt' => [
1146+
// '@context' => [
1147+
// 'eduPersonPrincipalName' =>
1148+
// 'https://vocabulary.example.org/terms#eduPersonPrincipalName',
1149+
// 'eduPersonTargetedID' =>
1150+
// 'https://vocabulary.example.org/terms#eduPersonTargetedID',
1151+
// 'displayName' =>
1152+
// 'https://vocabulary.example.org/terms#displayName',
1153+
// 'givenName' =>
1154+
// 'https://vocabulary.example.org/terms#givenName',
1155+
// 'sn' =>
1156+
// 'https://vocabulary.example.org/terms#sn',
1157+
// 'mail' =>
1158+
// 'https://vocabulary.example.org/terms#mail',
1159+
// 'eduPersonScopedAffiliation' =>
1160+
// 'https://vocabulary.example.org/terms#eduPersonScopedAffiliation',
1161+
// ],
1162+
// ],
1163+
// ],
1164+
11091165
/**
11101166
* (optional) Issuer State TTL (validity duration), with the given example.
11111167
* If not set, falls back to Authorization Code TTL. For duration format

routing/routes/routes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use SimpleSAML\Module\oidc\Controllers\UserInfoController;
2424
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerConfigurationController;
2525
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerCredentialController;
26+
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialJsonLdContextController;
2627
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\JwtVcIssuerConfigurationController;
2728
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\NonceController;
2829
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
@@ -131,6 +132,10 @@
131132
->controller([NonceController::class, 'nonce'])
132133
->methods([HttpMethodsEnum::POST->value]);
133134

135+
$routes->add(RoutesEnum::CredentialJsonLdContext->name, RoutesEnum::CredentialJsonLdContext->value)
136+
->controller([CredentialJsonLdContextController::class, 'context'])
137+
->methods([HttpMethodsEnum::GET->value]);
138+
134139
/*****************************************************************************************************************
135140
* SD-JWT-based Verifiable Credentials (SD-JWT VC)
136141
****************************************************************************************************************/

src/Codebooks/RoutesEnum.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ enum RoutesEnum: string
6464
case CredentialIssuerConfiguration = '.well-known/openid-credential-issuer';
6565
case CredentialIssuerCredential = 'credential-issuer/credential';
6666
case CredentialIssuerNonce = 'credential-issuer/nonce';
67+
case CredentialJsonLdContext = 'credential-issuer/context/{credentialConfigurationId}';
6768

6869
/*****************************************************************************************************************
6970
* SD-JWT-based Verifiable Credentials (SD-JWT VC)

src/Controllers/VerifiableCredentials/CredentialIssuerCredentialController.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,34 @@ public function credential(Request $request): Response
719719
}
720720

721721
if ($credentialFormatId === CredentialFormatIdentifiersEnum::VcSdJwt->value) {
722+
// Always start with the VCDM 2.0 base context URL (mandatory).
723+
$atContext = [AtContextsEnum::W3OrgNsCredentialsV2->value];
724+
725+
// If a JSON-LD context document is configured for this credential, append the module-hosted
726+
// context URL so that verifiers can resolve the custom credential subject terms.
727+
if ($this->moduleConfig->getVciCredentialJsonLdContextFor($resolvedCredentialIdentifier) !== null) {
728+
$atContext[] = $this->routes->urlCredentialJsonLdContext($resolvedCredentialIdentifier);
729+
}
730+
731+
// Append any additional context URLs declared in the credential configuration's @context field
732+
// (skipping the base W3C URL, which is already first in the list).
733+
/** @psalm-suppress MixedAssignment */
734+
$configuredContexts = $resolvedCredentialConfiguration[ClaimsEnum::AtContext->value] ?? [];
735+
if (is_array($configuredContexts)) {
736+
/** @psalm-suppress MixedAssignment */
737+
foreach ($configuredContexts as $configuredContext) {
738+
if (
739+
is_string($configuredContext) &&
740+
$configuredContext !== AtContextsEnum::W3OrgNsCredentialsV2->value &&
741+
!in_array($configuredContext, $atContext, true)
742+
) {
743+
$atContext[] = $configuredContext;
744+
}
745+
}
746+
}
747+
722748
$sdJwtPayload = [
723-
ClaimsEnum::AtContext->value => [
724-
AtContextsEnum::W3OrgNsCredentialsV2->value,
725-
],
749+
ClaimsEnum::AtContext->value => $atContext,
726750
ClaimsEnum::Id->value => $vcId,
727751
ClaimsEnum::Type->value => [
728752
CredentialTypesEnum::VerifiableCredential->value,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* |
7+
* \ ___ / _________
8+
* _ / \ _ GÉANT | * * | Co-Funded by
9+
* | ~ | Trust & Identity | * * | the European
10+
* \_/ Incubator |__*_*__| Union
11+
* =
12+
*
13+
* This file is part of the simplesamlphp-module-oidc.
14+
*
15+
* Copyright (C) 2018 by the Spanish Research and Academic Network.
16+
*
17+
* This code was developed by Universidad de Córdoba (UCO https://www.uco.es)
18+
* for the RedIRIS SIR service (SIR: http://www.rediris.es/sir)
19+
*
20+
* For the full copyright and license information, please view the LICENSE
21+
* file that was distributed with this source code.
22+
*/
23+
24+
namespace SimpleSAML\Module\oidc\Controllers\VerifiableCredentials;
25+
26+
use SimpleSAML\Module\oidc\ModuleConfig;
27+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
28+
use SimpleSAML\Module\oidc\Services\LoggerService;
29+
use SimpleSAML\Module\oidc\Utils\Routes;
30+
use Symfony\Component\HttpFoundation\JsonResponse;
31+
use Symfony\Component\HttpFoundation\Response;
32+
33+
/**
34+
* Serves the JSON-LD context document for a specific vc+sd-jwt credential configuration, allowing
35+
* verifiers to resolve the custom terms used in credential subjects.
36+
*
37+
* The endpoint URL is included in the @context array of issued vc+sd-jwt credentials when a
38+
* context document is configured for the matching credential configuration ID.
39+
*
40+
* @see https://www.w3.org/TR/json-ld11/#interpreting-json-as-json-ld
41+
*/
42+
class CredentialJsonLdContextController
43+
{
44+
/**
45+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
46+
*/
47+
public function __construct(
48+
protected readonly ModuleConfig $moduleConfig,
49+
protected readonly Routes $routes,
50+
protected readonly LoggerService $loggerService,
51+
) {
52+
if (!$this->moduleConfig->getVciEnabled()) {
53+
$this->loggerService->warning('Verifiable Credential capabilities not enabled.');
54+
throw OidcServerException::forbidden('Verifiable Credential capabilities not enabled.');
55+
}
56+
}
57+
58+
/**
59+
* Return the JSON-LD context document for the given credential configuration ID.
60+
*
61+
* Responds with HTTP 404 if no context document is configured for the given ID.
62+
*
63+
* @param string $credentialConfigurationId URL path parameter injected by the router.
64+
*/
65+
public function context(string $credentialConfigurationId): Response
66+
{
67+
$this->loggerService->debug(
68+
'CredentialJsonLdContextController::context',
69+
['credentialConfigurationId' => $credentialConfigurationId],
70+
);
71+
72+
$contextDocument = $this->moduleConfig->getVciCredentialJsonLdContextFor($credentialConfigurationId);
73+
74+
if ($contextDocument === null) {
75+
$this->loggerService->warning(
76+
'CredentialJsonLdContextController::context: No JSON-LD context configured for credential ' .
77+
'configuration ID.',
78+
['credentialConfigurationId' => $credentialConfigurationId],
79+
);
80+
81+
return $this->routes->newResponse(null, Response::HTTP_NOT_FOUND);
82+
}
83+
84+
return new JsonResponse(
85+
$contextDocument,
86+
Response::HTTP_OK,
87+
['Content-Type' => 'application/ld+json'],
88+
);
89+
}
90+
}

src/ModuleConfig.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ class ModuleConfig
120120
final public const string OPTION_FEDERATION_SIGNATURE_KEY_PAIRS = 'federation_signature_key_pairs';
121121
final public const string OPTION_TIMESTAMP_VALIDATION_LEEWAY = 'timestamp_validation_leeway';
122122
final public const string OPTION_VCI_SIGNATURE_KEY_PAIRS = 'vci_signature_key_pairs';
123+
final public const string OPTION_VCI_CREDENTIAL_JSON_LD_CONTEXT = 'vci_credential_json_ld_context';
123124

124125
protected static array $standardScopes = [
125126
ScopesEnum::OpenId->value => [
@@ -1037,6 +1038,37 @@ public function getVciAllowedRedirectUriPrefixesForNonRegisteredClients(): array
10371038
}
10381039

10391040

1041+
/**
1042+
* Get the full map of a credential configuration ID => JSON-LD context
1043+
* document (as a PHP array).
1044+
*
1045+
* @return mixed[]
1046+
*/
1047+
public function getVciCredentialJsonLdContext(): array
1048+
{
1049+
return $this->config()->getOptionalArray(self::OPTION_VCI_CREDENTIAL_JSON_LD_CONTEXT, []);
1050+
}
1051+
1052+
/**
1053+
* Get the JSON-LD context document (as a PHP array) configured for a
1054+
* specific credential configuration ID.
1055+
* Returns null if no context document is configured for the given ID.
1056+
*
1057+
* @return array<mixed>|null
1058+
*/
1059+
public function getVciCredentialJsonLdContextFor(string $credentialConfigurationId): ?array
1060+
{
1061+
/** @psalm-suppress MixedAssignment */
1062+
$context = $this->getVciCredentialJsonLdContext()[$credentialConfigurationId] ?? null;
1063+
1064+
if (!is_array($context)) {
1065+
return null;
1066+
}
1067+
1068+
return $context;
1069+
}
1070+
1071+
10401072
/*****************************************************************************************************************
10411073
* API-related config.
10421074
****************************************************************************************************************/

src/Utils/Routes.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,17 @@ public function urlCredentialIssuerNonce(array $parameters = []): string
232232
return $this->getModuleUrl(RoutesEnum::CredentialIssuerNonce->value, $parameters);
233233
}
234234

235+
public function urlCredentialJsonLdContext(string $credentialConfigurationId, array $parameters = []): string
236+
{
237+
$path = str_replace(
238+
'{credentialConfigurationId}',
239+
rawurlencode($credentialConfigurationId),
240+
RoutesEnum::CredentialJsonLdContext->value,
241+
);
242+
243+
return $this->getModuleUrl($path, $parameters);
244+
}
245+
235246
/*****************************************************************************************************************
236247
* SD-JWT-based Verifiable Credentials (SD-JWT VC)
237248
****************************************************************************************************************/

0 commit comments

Comments
 (0)