Skip to content

Commit

Permalink
feat: add credentialSchema property to VerifiableCredential
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jan 29, 2025
1 parent 347e228 commit 6f8bee4
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust.transform.to;

import jakarta.json.JsonObject;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema;
import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer;
import org.eclipse.edc.transform.spi.TransformerContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.net.URI;
import java.net.URISyntaxException;

import static org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema.CREDENTIAL_SCHEMA_ID_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema.CREDENTIAL_SCHEMA_TYPE_PROPERTY;


public class JsonObjectToCredentialSchemaTransformer extends AbstractJsonLdTransformer<JsonObject, CredentialSchema> {
public JsonObjectToCredentialSchemaTransformer() {
super(JsonObject.class, CredentialSchema.class);
}

@Override
public @Nullable CredentialSchema transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) {

var id = nodeId(jsonObject);

try {
new URI(id);
} catch (URISyntaxException ignored) {
context.reportProblem("The '%s' property must be in URI format but was not: '%s'".formatted(CREDENTIAL_SCHEMA_ID_PROPERTY, id));
}

var type = transformString(jsonObject.get(CREDENTIAL_SCHEMA_TYPE_PROPERTY), context);
if (type == null) {
context.reportProblem("The '%s' property is mandatory on credentialSchema objects".formatted(CREDENTIAL_SCHEMA_TYPE_PROPERTY));
}
return new CredentialSchema(id, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
Expand All @@ -36,6 +37,7 @@
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_ISSUER_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_NAME_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_PROOF_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_SCHEMA_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_STATUS_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_SUBJECT_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential.VERIFIABLE_CREDENTIAL_VALIDFROM_PROPERTY;
Expand Down Expand Up @@ -79,6 +81,8 @@ private void transformProperties(String key, JsonValue jsonValue, Builder vcBuil
case VERIFIABLE_CREDENTIAL_PROOF_PROPERTY -> {
//noop
}
case VERIFIABLE_CREDENTIAL_SCHEMA_PROPERTY ->
vcBuilder.credentialSchemas(transformArray(jsonValue, CredentialSchema.class, context));
default ->
context.reportProblem("Unknown property: %s type: %s".formatted(key, jsonValue.getValueType().name()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.eclipse.edc.iam.identitytrust.transform.to;

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.DataModelVersion;
Expand All @@ -40,6 +41,7 @@ public class JwtToVerifiableCredentialTransformer extends AbstractJwtTransformer
private static final String ID_PROPERTY = "id";
private static final String VC_CLAIM = "vc";
private static final String CREDENTIAL_SUBJECT_PROPERTY = "credentialSubject";
private static final String CREDENTIAL_SCHEMA_PROPERTY = "credentialSchema";
private static final String CREDENTIAL_STATUS_PROPERTY = "credentialStatus";
private static final String EXPIRATION_DATE_PROPERTY = "expirationDate";
private static final String ISSUANCE_DATE_PROPERTY = "issuanceDate";
Expand All @@ -54,6 +56,7 @@ public JwtToVerifiableCredentialTransformer(Monitor monitor) {
this.monitor = monitor;
}

@SuppressWarnings("unchecked")
@Override
public @Nullable VerifiableCredential transform(@NotNull String serializedJwt, @NotNull TransformerContext context) {
try {
Expand Down Expand Up @@ -86,6 +89,9 @@ public JwtToVerifiableCredentialTransformer(Monitor monitor) {
// credential status
listOrReturn(vc.get(CREDENTIAL_STATUS_PROPERTY), o -> extractStatus((Map<String, Object>) o)).forEach(builder::credentialStatus);

//credential schema
listOrReturn(vc.get(CREDENTIAL_SCHEMA_PROPERTY), o -> extractSchema((Map<String, Object>) o)).forEach(builder::credentialSchema);

// expiration date
extractDate(vc.get(EXPIRATION_DATE_PROPERTY), claims.getExpirationTime()).or(() -> extractDate(vc.get(VALID_UNTIL_PROPERTY), claims.getExpirationTime())).ifPresent(builder::expirationDate);

Expand Down Expand Up @@ -122,6 +128,16 @@ private CredentialStatus extractStatus(Map<String, Object> status) {
return new CredentialStatus(id, type, status);
}

private CredentialSchema extractSchema(Map<String, Object> schema) {
if (schema == null || schema.isEmpty()) {
return null;
}
var id = schema.remove(ID_PROPERTY).toString();
var type = schema.remove(TYPE_PROPERTY).toString();

return new CredentialSchema(id, type);
}

private CredentialSubject extractSubject(Map<String, ?> subject, String fallback) {
var builder = CredentialSubject.Builder.newInstance();
var id = Objects.requireNonNullElse(subject.remove(ID_PROPERTY), fallback);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,47 @@ public interface TestData {
}
""";

String EXAMPLE_VC_JSONLD_WITH_SCHEMA = """
{
"@context": [
"https://www.w3.org/2018/credentials/v2"
],
"id": "http://university.example/credentials/3732",
"type": ["VerifiableCredential", "ExampleDegreeCredential"],
"issuer": {
"id": "https://university.example/issuers/565049",
"name": "Example University",
"description": "A public university focusing on teaching examples."
},
"validFrom": "2015-05-10T12:30:00Z",
"validUntil":"2023-05-12T23:00:00Z",
"name": "Example University Degree",
"description": "2015 Bachelor of Science and Arts Degree",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"degreetype": "ExampleBachelorDegree",
"subtype": "Bachelor of Science and Arts"
}
},
"credentialStatus": {
"id": "https://university.example/credentials/status/3#94567",
"type": "StatusList2021Entry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://university.example/credentials/status/3"
},
"credentialSchema": [{
"id": "https://example.org/examples/degree.json",
"type": "JsonSchema"
},
{
"id": "https://example.org/examples/alumni.json",
"type": "JsonSchema"
}]
}
""";

String EXAMPLE_VC_SUB_IS_ARRAY_JSONLD = """
{
"@context": [
Expand Down Expand Up @@ -283,20 +324,18 @@ public interface TestData {
""";

String EXAMPLE_JWT_VC = """
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3
d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L
2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRl
bnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eUR
lZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLz
E0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3Via
mVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRl
Z3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5
jZSBhbmQgQXJ0cyJ9fSwiY3JlZGVudGlhbFN0YXR1cyI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS
5lZHUvc3RhdHVzLzI0IiwidHlwZSI6IkNyZWRlbnRpYWxTdGF0dXNMaXN0MjAxNyJ9fSwiaXNzI
joiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzE0IiwibmJmIjoxMjYyMzczODA0LCJqdGki
OiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGx
lOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.YQKQUu_zreDs69AZ8YqpMGHLl9V_tWH4N
S9P9l67J1wWHf0QCyt5hyuA8ckM4seV-1TRbeiHwdJ3VRkDMcwFcg
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWF
scy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR
1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSw
iaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzE0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJ
jcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRlZ3JlZSI6eyJ0eXB
lIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fSwiY3JlZGVudGlhbFN0YXR1cyI6eyJ
pZCI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvc3RhdHVzLzI0IiwidHlwZSI6IkNyZWRlbnRpYWxTdGF0dXNMaXN0MjAxNyJ9LCJjcmVkZW50aWF
sU2NoZW1hIjpbeyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvZXhhbXBsZXMvZGVncmVlLmpzb24iLCJ0eXBlIjoiSnNvblNjaGVtYSJ9LHs
iaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2FsdW1uaS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifV19LCJpc3MiOiJodHR
wczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvMTQiLCJuYmYiOjEyNjIzNzM4MDQsImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWF
scy8zNzMyIiwic3ViIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIn0.IjVESDTm094UZor3AWJY-wC7a9DBWF_
fzm4q9M-H6F7F8YVe3YF_gmzKNblR3l8VeaASD4R0YwR1rawVA2mfNQ
""";

String EXAMPLE_JWT_VC_NO_DATES = """
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust.transform.to;

import jakarta.json.Json;
import org.eclipse.edc.transform.spi.TransformerContext;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema.CREDENTIAL_SCHEMA_ID_PROPERTY;
import static org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema.CREDENTIAL_SCHEMA_TYPE_PROPERTY;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

class JsonObjectToCredentialSchemaTransformerTest {

private final JsonObjectToCredentialSchemaTransformer transformer = new JsonObjectToCredentialSchemaTransformer();
private final @NotNull TransformerContext context = mock();

@Test
void transform() {
var jo = Json.createObjectBuilder()
.add(CREDENTIAL_SCHEMA_ID_PROPERTY, "http://foo.bar/id")
.add(CREDENTIAL_SCHEMA_TYPE_PROPERTY, "JsonSchemaValidator2018")
.build();
var result = transformer.transform(jo, context);
assertThat(result).isNotNull();
assertThat(result.id()).isEqualTo("http://foo.bar/id");
assertThat(result.type()).isEqualTo("JsonSchemaValidator2018");
verify(context, never()).reportProblem(any());
}

@Test
void transform_typeMissing() {
var jo = Json.createObjectBuilder()
.add(CREDENTIAL_SCHEMA_ID_PROPERTY, "http://foo.bar/id")
.build();
var result = transformer.transform(jo, context);
assertThat(result).isNotNull();
assertThat(result.id()).isEqualTo("http://foo.bar/id");
verify(context).reportProblem(anyString());
}

@Test
void transform_idNotUri() {
var jo = Json.createObjectBuilder()
.add(CREDENTIAL_SCHEMA_ID_PROPERTY, "not a uri")
.add(CREDENTIAL_SCHEMA_TYPE_PROPERTY, "JsonSchemaValidator2018")
.build();
var result = transformer.transform(jo, context);
assertThat(result).isNotNull();
assertThat(result.type()).isEqualTo("JsonSchemaValidator2018");
verify(context).reportProblem(anyString());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_JSONLD;
import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_JSONLD_ISSUER_IS_URL;
import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_JSONLD_WITH_SCHEMA;
import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_VC_SUB_IS_ARRAY_JSONLD;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
Expand All @@ -57,6 +58,7 @@ void setUp() throws URISyntaxException {
registry.register(new JsonObjectToCredentialStatusTransformer());
registry.register(new JsonValueToGenericTypeTransformer(typeManager, "test"));
registry.register(new JsonObjectToIssuerTransformer());
registry.register(new JsonObjectToCredentialSchemaTransformer());
registry.register(transformer);

context = spy(new TransformerContextImpl(registry));
Expand Down Expand Up @@ -113,4 +115,14 @@ void transform_issuerIsUrl() throws JsonProcessingException {
assertThat(vc.getIssuer()).isNotNull().extracting(Issuer::id).isEqualTo("https://university.example/issuers/565049");
verify(context, never()).reportProblem(anyString());
}

@Test
void transform_withCredentialSchema() throws JsonProcessingException {
var jsonObj = OBJECT_MAPPER.readValue(EXAMPLE_VC_JSONLD_WITH_SCHEMA, JsonObject.class);
var vc = transformer.transform(jsonLdService.expand(jsonObj).getContent(), context);

assertThat(vc).isNotNull();
assertThat(vc.getCredentialSchema()).isNotNull().hasSize(2);
verify(context, never()).reportProblem(anyString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ void transform_success() {
assertThat(vc.getCredentialSubject()).doesNotContainNull().isNotEmpty();
assertThat(vc.getCredentialSubject().stream().findFirst().orElseThrow().getId()).isNotNull();
assertThat(vc.getIssuanceDate()).isNotNull();
assertThat(vc.getCredentialSchema()).isNotNull()
.hasSize(2)
.anyMatch(schema -> schema.type().equals("JsonSchema") &&
schema.id().equals("https://example.org/examples/degree.json"))
.anyMatch(schema -> schema.type().equals("JsonSchema") &&
schema.id().equals("https://example.org/examples/alumni.json"));

verifyNoInteractions(context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.verifiablecredentials.spi.model;

public record CredentialSchema(String id, String type) {
public static final String CREDENTIAL_SCHEMA_ID_PROPERTY = "@id";
public static final String CREDENTIAL_SCHEMA_TYPE_PROPERTY = "@type";

}
Loading

0 comments on commit 6f8bee4

Please sign in to comment.