From 2448f9c7997b0d85fdea5ffd53412cd2ad5471ad Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 12 Jun 2024 15:55:08 +0200 Subject: [PATCH 1/5] Support Nuts v6 --- README.md | 2 +- pom.xml | 197 +++++++++++++++--- .../java/nl/reinkrul/nuts/Configuration.java | 42 ++++ .../nl/reinkrul/nuts/common/DIDDocument.java | 1 - .../VerifiableCredentialDeserializer.java | 37 ++++ .../VerifiablePresentationDeserializer.java | 39 ++++ .../VerifiableCredentialDeserializerTest.java | 12 ++ .../reinkrul/nuts/{ => v5}/AuthApiTest.java | 15 +- .../nl/reinkrul/nuts/v6/IntegrationTest.java | 43 ++++ .../{MarshalTest.java => v6/VdrTest.java} | 19 +- .../java/{ => v5}/CredentialExamples.java | 44 ++-- src/test/java/v6/CredentialExamples.java | 33 +++ 12 files changed, 420 insertions(+), 64 deletions(-) create mode 100644 src/main/java/nl/reinkrul/nuts/Configuration.java create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java create mode 100644 src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java rename src/test/java/nl/reinkrul/nuts/{ => v5}/AuthApiTest.java (79%) create mode 100644 src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java rename src/test/java/nl/reinkrul/nuts/{MarshalTest.java => v6/VdrTest.java} (66%) rename src/test/java/{ => v5}/CredentialExamples.java (77%) create mode 100644 src/test/java/v6/CredentialExamples.java diff --git a/README.md b/README.md index 82f7779..f5cbf96 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You can find in their own subpackage in `nl.reinkrul.nuts` (e.g. `nl.reinkrul.nu # Examples -See [src/test/java/CredentialExamples.java](src/test/java/CredentialExamples.java) +See [src/test/java/v6.CredentialExamples.java](src/test/java/v6.CredentialExamples.java) for how to issue `NutsOrganizationCredential`, `NutsAuthenticationCredential` and `NutsEmployeeCredential`. # Versioning diff --git a/pom.xml b/pom.xml index 33ad66a..0837b74 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ nl.reinkrul.nuts java-client jar - 5.3.0 + 6.0.0-beta.1 Java Nuts Client Library https://github.com/reinkrul/java-nuts-client @@ -34,9 +34,9 @@ - 11 - 11 - v5.3.0 + 18 + 18 + master https://raw.githubusercontent.com/nuts-foundation/nuts-node/${nuts.version}/docs/_static @@ -44,6 +44,12 @@ ${project.basedir}/target/generated 1.0-SNAPSHOT + + 3.1.1 + 2.15.2 + 2.15.2 + 0.2.6 + 2.1.1 @@ -54,28 +60,81 @@ + - org.junit.jupiter - junit-jupiter-engine - 5.9.1 - test + com.google.code.findbugs + jsr305 + 3.0.2 + + + + + org.glassfish.jersey.core + jersey-client + ${jersey-version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey-version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey-version} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey-version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson-version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind-version} + org.openapitools jackson-databind-nullable - 0.2.6 + ${jackson-databind-nullable-version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.14.2 + ${jackson-version} - com.google.code.findbugs - jsr305 - 3.0.2 + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation-version} + provided + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey-version} + + + + org.junit.jupiter + junit-jupiter-engine + 5.9.1 + test + + + io.swagger swagger-annotations @@ -86,6 +145,11 @@ did-common-java 1.4.0 + + com.danubetech + verifiable-credentials-java + 1.7.0 + javax.annotation javax.annotation-api @@ -126,7 +190,7 @@ run - + + + + dest="${openapi.spec.dir}/vcr/v2.yaml"/> + + + - + + + + + + + + remove-generated-configuration-class + process-sources + + run + + + + + @@ -169,7 +256,7 @@ org.openapitools openapi-generator-maven-plugin - 6.6.0 + 7.5.0 java false @@ -195,19 +282,21 @@ https://www.reinkrul.nl Java client library for using the Nuts Node's REST API. - native + jersey3 - DIDDocument=nl.reinkrul.nuts.common.DIDDocument + + DIDDocument=nl.reinkrul.nuts.common.DIDDocument, + VerifiableCredential=com.danubetech.verifiablecredentials.VerifiableCredential, + VerifiablePresentation=com.danubetech.verifiablecredentials.VerifiablePresentation, + DIDDocumentMetadata=nl.reinkrul.nuts.common.DIDDocumentMetadata, EmbeddedProof=nl.reinkrul.nuts.common.EmbeddedProof, Revocation=nl.reinkrul.nuts.common.Revocation, Service=nl.reinkrul.nuts.common.Service, - VerifiableCredential=nl.reinkrul.nuts.common.VerifiableCredential, - VerifiablePresentation=nl.reinkrul.nuts.common.VerifiablePresentation, VerificationMethod=nl.reinkrul.nuts.common.VerificationMethod @@ -236,7 +325,11 @@ ${openapi.spec.dir}/common/ssi_types.yaml true - DIDDocument=nl.reinkrul.nuts.common.DIDDocument + + DIDDocument=nl.reinkrul.nuts.common.DIDDocument, + VerifiableCredential=com.danubetech.verifiablecredentials.VerifiableCredential, + VerifiablePresentation=com.danubetech.verifiablecredentials.VerifiablePresentation + nl.reinkrul.nuts.common nl.reinkrul.nuts.common @@ -246,15 +339,30 @@ - generate-vdr-client + generate-vdr-v1-client generate ${openapi.spec.dir}/vdr/v1.yaml - nl.reinkrul.nuts.vdr - nl.reinkrul.nuts.vdr + nl.reinkrul.nuts.vdr.v1 + nl.reinkrul.nuts.vdr.v1 + nl.reinkrul.nuts + + + + + generate-vdr-v2-client + + generate + + + ${openapi.spec.dir}/vdr/v2.yaml + + nl.reinkrul.nuts.vdr.v2 + nl.reinkrul.nuts.vdr.v2 + nl.reinkrul.nuts @@ -265,7 +373,7 @@ generate - ${openapi.spec.dir}/vcr/vcr_v2.yaml + ${openapi.spec.dir}/vcr/v2.yaml nl.reinkrul.nuts.vcr nl.reinkrul.nuts.vcr @@ -286,6 +394,20 @@ + + + generate-discovery-client + + generate + + + ${openapi.spec.dir}/discovery/v1.yaml + + nl.reinkrul.nuts.discovery + nl.reinkrul.nuts.discovery + + + generate-network-client @@ -314,17 +436,32 @@ - + - generate-auth-client + generate-auth-v1-client generate ${openapi.spec.dir}/auth/v1.yaml - nl.reinkrul.nuts.auth - nl.reinkrul.nuts.auth + nl.reinkrul.nuts.auth.v1 + nl.reinkrul.nuts.auth.v1 + nl.reinkrul.nuts + + + + + generate-auth-v2-client + + generate + + + ${openapi.spec.dir}/auth/v2.yaml + + nl.reinkrul.nuts.auth.v2 + nl.reinkrul.nuts.auth.v2 + nl.reinkrul.nuts diff --git a/src/main/java/nl/reinkrul/nuts/Configuration.java b/src/main/java/nl/reinkrul/nuts/Configuration.java new file mode 100644 index 0000000..8568d0c --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/Configuration.java @@ -0,0 +1,42 @@ +package nl.reinkrul.nuts; + +import com.danubetech.verifiablecredentials.VerifiableCredential; +import com.danubetech.verifiablecredentials.VerifiablePresentation; +import com.fasterxml.jackson.databind.module.SimpleModule; +import nl.reinkrul.nuts.common.VerifiableCredentialDeserializer; +import nl.reinkrul.nuts.common.VerifiablePresentationDeserializer; + +public class Configuration { + + private static ApiClient defaultApiClient = create(); + + /** + * Get the default API client, which would be used when creating API + * instances without providing an API client. + * + * @return Default API client + */ + public static ApiClient getDefaultApiClient() { + return defaultApiClient; + } + + /** + * Set the default API client, which would be used when creating API + * instances without providing an API client. + * + * @param apiClient API client + */ + public static void setDefaultApiClient(ApiClient apiClient) { + defaultApiClient = apiClient; + } + + public static ApiClient create() { + var result = new ApiClient(); + result.setUserAgent("nuts-java-client"); + var module = new SimpleModule(); + module.addDeserializer(VerifiableCredential.class, new VerifiableCredentialDeserializer(VerifiableCredential.class)); + module.addDeserializer(VerifiablePresentation.class, new VerifiablePresentationDeserializer(VerifiablePresentation.class)); + result.getJSON().getMapper().registerModule(module); + return result; + } +} diff --git a/src/main/java/nl/reinkrul/nuts/common/DIDDocument.java b/src/main/java/nl/reinkrul/nuts/common/DIDDocument.java index 757fc5f..73e7f85 100644 --- a/src/main/java/nl/reinkrul/nuts/common/DIDDocument.java +++ b/src/main/java/nl/reinkrul/nuts/common/DIDDocument.java @@ -1,5 +1,4 @@ package nl.reinkrul.nuts.common; public class DIDDocument extends foundation.identity.did.DIDDocument { - } diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java new file mode 100644 index 0000000..53382d5 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java @@ -0,0 +1,37 @@ +package nl.reinkrul.nuts.common; + +import com.danubetech.verifiablecredentials.VerifiableCredential; +import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.text.ParseException; + +public class VerifiableCredentialDeserializer extends StdDeserializer { + + public VerifiableCredentialDeserializer(Class vc) { + super(vc); + } + + @Override + public VerifiableCredential deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { + if (jsonParser.getCurrentToken().isScalarValue()) { + try { + return JwtVerifiableCredential.fromCompactSerialization(jsonParser.getValueAsString()).getPayloadObject(); + } catch (ParseException e) { + throw new IOException(e); + } + } + if (jsonParser.getCurrentToken().isStructStart()) { + + } + throw new IOException("Unexpected token: " + jsonParser.getCurrentToken().toString()); + } +} diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java new file mode 100644 index 0000000..ce6e54d --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java @@ -0,0 +1,39 @@ +package nl.reinkrul.nuts.common; + +import com.danubetech.verifiablecredentials.VerifiableCredential; +import com.danubetech.verifiablecredentials.VerifiablePresentation; +import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential; +import com.danubetech.verifiablecredentials.jwt.JwtVerifiablePresentation; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.ParseException; + +public class VerifiablePresentationDeserializer extends StdDeserializer { + + public VerifiablePresentationDeserializer(Class vc) { + super(vc); + } + + @Override + public VerifiablePresentation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + var token = jsonParser.nextValue(); + if (token == JsonToken.START_OBJECT) { + // Parse as JSON-LD + return com.danubetech.verifiablecredentials.VerifiablePresentation.fromJson(new InputStreamReader(new ByteArrayInputStream(token.asByteArray()))); + } else if (token == JsonToken.VALUE_STRING) { + try { + return JwtVerifiablePresentation.fromCompactSerialization(token.asString()).getPayloadObject(); + } catch (ParseException e) { + throw new IOException(e); + } + } + throw new IOException("Unexpected token: " + token); + } +} diff --git a/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java b/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java new file mode 100644 index 0000000..a4f427c --- /dev/null +++ b/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java @@ -0,0 +1,12 @@ +package nl.reinkrul.nuts.common; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class VerifiableCredentialDeserializerTest { + + @Test + void deserialize() { + } +} \ No newline at end of file diff --git a/src/test/java/nl/reinkrul/nuts/AuthApiTest.java b/src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java similarity index 79% rename from src/test/java/nl/reinkrul/nuts/AuthApiTest.java rename to src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java index e7dfdf8..ad9e685 100644 --- a/src/test/java/nl/reinkrul/nuts/AuthApiTest.java +++ b/src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java @@ -1,15 +1,16 @@ -package nl.reinkrul.nuts; +package nl.reinkrul.nuts.v5; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpServer; -import nl.reinkrul.nuts.auth.AuthApi; +import nl.reinkrul.nuts.ApiClient; +import nl.reinkrul.nuts.ApiException; +import nl.reinkrul.nuts.Configuration; +import nl.reinkrul.nuts.auth.v1.AuthApi; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.io.InputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; @@ -33,6 +34,7 @@ public static void setup() throws IOException { server.createContext("/internal/auth/v1/accesstoken/introspect", exchange -> { actualRequestHeaders = exchange.getRequestHeaders(); actualRequestBody = exchange.getRequestBody().readAllBytes(); + exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().write("{\"sub\": \"admin\"}".getBytes()); exchange.close(); @@ -49,12 +51,11 @@ public static void tearDown() { @Test public void introspectAccessToken() throws ApiException { - var client = new ApiClient(); - client.updateBaseUri("http://" + server.getAddress().getHostName() + ":" + server.getAddress().getPort()); + var client = Configuration.getDefaultApiClient(); + client.setBasePath("http://" + server.getAddress().getHostName() + ":" + server.getAddress().getPort()); var authApi = new AuthApi(client); var response = authApi.introspectAccessToken("some-token"); - assertEquals("application/x-www-form-urlencoded; charset=UTF-8", actualRequestHeaders.getFirst("Content-Type")); assertEquals("token=some-token", new String(actualRequestBody)); assertEquals("admin", response.getSub()); } diff --git a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java new file mode 100644 index 0000000..201b92d --- /dev/null +++ b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java @@ -0,0 +1,43 @@ +package nl.reinkrul.nuts.v6; + +import nl.reinkrul.nuts.ApiException; +import nl.reinkrul.nuts.Configuration; +import nl.reinkrul.nuts.vcr.CredentialApi; +import nl.reinkrul.nuts.vcr.IssueVCRequest; +import nl.reinkrul.nuts.vcr.IssueVCRequestType; +import nl.reinkrul.nuts.vdr.v2.CreateDIDOptions; +import nl.reinkrul.nuts.vdr.v2.DidApi; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +public class IntegrationTest { + + @Test + public void createDIDDocumentIssueVC() throws ApiException { + var apiClient = Configuration.getDefaultApiClient(); + apiClient.setBasePath("http://localhost:8081"); + + var didApi = new DidApi(apiClient); + var didDocument = didApi.createDID(new CreateDIDOptions()); + + var credentialApi = new CredentialApi(apiClient); + var issuedVC = credentialApi.issueVC(new IssueVCRequest() + .type(new IssueVCRequestType("EmployeeCredential")) + .issuer(didDocument.getId().toString()) + .withStatusList2021Revocation(true) + .publishToNetwork(null) + .visibility(null) + .format(IssueVCRequest.FormatEnum.JWT_VC) + .credentialSubject( + Map.of( + "id", "did:jwk:1234", + "name", "John Doe", + "roleName", "Software Engineer", + "identifier", "1234" + ) + ) + ); + System.out.println(issuedVC.getId()); + } +} diff --git a/src/test/java/nl/reinkrul/nuts/MarshalTest.java b/src/test/java/nl/reinkrul/nuts/v6/VdrTest.java similarity index 66% rename from src/test/java/nl/reinkrul/nuts/MarshalTest.java rename to src/test/java/nl/reinkrul/nuts/v6/VdrTest.java index ebdabe3..620749a 100644 --- a/src/test/java/nl/reinkrul/nuts/MarshalTest.java +++ b/src/test/java/nl/reinkrul/nuts/v6/VdrTest.java @@ -1,7 +1,9 @@ -package nl.reinkrul.nuts; +package nl.reinkrul.nuts.v6; import com.sun.net.httpserver.HttpServer; -import nl.reinkrul.nuts.vdr.DidApi; +import nl.reinkrul.nuts.ApiException; +import nl.reinkrul.nuts.Configuration; +import nl.reinkrul.nuts.vdr.v2.DidApi; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -13,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class MarshalTest { +public class VdrTest { private static HttpServer server; @@ -25,9 +27,10 @@ public static void setup() throws IOException { } server = HttpServer.create(new InetSocketAddress("localhost", port), 10); - server.createContext("/internal/vdr/v1/did/did:nuts:abcdefghijklmnop", exchange -> { + server.createContext("/internal/vdr/v2/did/did:nuts:abcdefghijklmnop", exchange -> { + exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, 0); - try (InputStream inputStream = MarshalTest.class.getResourceAsStream("/did-resolution-result.json")) { + try (InputStream inputStream = VdrTest.class.getResourceAsStream("/did-resolution-result.json")) { exchange.getResponseBody().write(inputStream.readAllBytes()); } exchange.close(); @@ -44,11 +47,11 @@ public static void tearDown() { @Test public void didDocument() throws ApiException { - var apiClient = new ApiClient(); - apiClient.updateBaseUri("http://localhost:" + server.getAddress().getPort()); + var apiClient = Configuration.getDefaultApiClient(); + apiClient.setBasePath("http://localhost:" + server.getAddress().getPort()); var didApi = new DidApi(apiClient); - var result = didApi.getDID("did:nuts:abcdefghijklmnop", null, null); + var result = didApi.resolveDID("did:nuts:abcdefghijklmnop"); assertEquals("did:nuts:abcdefghijklmnop", result.getDocument().getId().toString()); } diff --git a/src/test/java/CredentialExamples.java b/src/test/java/v5/CredentialExamples.java similarity index 77% rename from src/test/java/CredentialExamples.java rename to src/test/java/v5/CredentialExamples.java index b857d8b..42ef871 100644 --- a/src/test/java/CredentialExamples.java +++ b/src/test/java/v5/CredentialExamples.java @@ -1,29 +1,39 @@ +package v5; + +import com.danubetech.verifiablecredentials.CredentialSubject; +import com.danubetech.verifiablecredentials.VerifiableCredential; import nl.reinkrul.nuts.ApiException; -import nl.reinkrul.nuts.common.VerifiableCredential; import nl.reinkrul.nuts.extra.*; import nl.reinkrul.nuts.vcr.*; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; +import java.util.Map; public class CredentialExamples { - public void searchNutsOrganizationCredential() throws ApiException { + public void searchNutsOrganizationCredential() throws ApiException, URISyntaxException { var client = new CredentialApi(); + var query = VerifiableCredential.builder() + .type("NutsOrganizationCredential") + .contexts(List.of(new URI("https://nuts.nl/credentials/v1"), new URI("https://www.w3.org/2018/credentials/v1"))) + .issuer(new URI("did:nuts:some-did")) // the DID of the issuer of the credential (DID of software vendor) + .credentialSubject( + CredentialSubject.builder() + .id(new URI("did:nuts:some-other-did")) // the DID of the receiver of the credential + .claims(Map.of("organization", Map.of( + "name", "Extra Careful B.V.", + "city", "Zorgdorp" + ))) + .build() + ).build(); var issuedVC = client.searchVCs(new SearchVCRequest() // Search options .searchOptions(new SearchOptions().allowUntrustedIssuer(false)) // VC to match - .query(new VerifiableCredential() - .type(List.of("VerifiableCredential", "NutsOrganizationCredential")) - .atContext(List.of("https://nuts.nl/credentials/v1", "https://www.w3.org/2018/credentials/v1")) - .issuer("did:nuts:some-did") // the DID of the issuer of the credential (DID of software vendor) - .credentialSubject( - new NutsOrganizationCredential() - .organization(new nl.reinkrul.nuts.extra.Organization() - .name("Extra Careful B.V.") - .city("Zorgdorp") - )) - )); + .query(query) + ); for (SearchVCResult result : issuedVC.getVerifiableCredentials()) { System.out.println(result.getVerifiableCredential().getId()); @@ -34,14 +44,14 @@ public void issueNutsOrganizationCredential() throws ApiException { var client = new CredentialApi(); var issuedVC = client.issueVC(new IssueVCRequest() // General VC properties - .type("NutsOrganizationCredential") + .type(new IssueVCRequestType("NutsAuthenticationCredential")) .issuer("did:nuts:some-did") // the DID of the issuer of the credential (DID of software vendor) .visibility(IssueVCRequest.VisibilityEnum.PUBLIC) // publish on network, anyone can read it // Subject of the credential .credentialSubject( new NutsOrganizationCredential() .id("did:nuts:some-other-did") // the DID of the receiver of the credential - .organization(new nl.reinkrul.nuts.extra.Organization() + .organization(new Organization() .name("Extra Careful B.V.") .city("Zorgdorp") ) @@ -55,7 +65,7 @@ public void issueNutsAuthorizationCredential() throws ApiException { var client = new CredentialApi(); var issuedVC = client.issueVC(new IssueVCRequest() // General VC properties - .type("NutsAuthenticationCredential") + .type(new IssueVCRequestType("NutsAuthenticationCredential")) .issuer("did:nuts:some-did") // the DID of the issuer of the credential .visibility(IssueVCRequest.VisibilityEnum.PRIVATE) // publish on network, but keep it private // Subject of the credential @@ -81,7 +91,7 @@ public void issueNutsEmployeeCredential() throws ApiException { var client = new CredentialApi(); var issuedVC = client.issueVC(new IssueVCRequest() // General VC properties - .type("NutsEmployeeCredential") + .type(new IssueVCRequestType("NutsAuthenticationCredential")) .issuer("did:nuts:some-did") // the DID of the issuer of the credential // Subject of the credential .credentialSubject( diff --git a/src/test/java/v6/CredentialExamples.java b/src/test/java/v6/CredentialExamples.java new file mode 100644 index 0000000..1f9f9bd --- /dev/null +++ b/src/test/java/v6/CredentialExamples.java @@ -0,0 +1,33 @@ +package v6; + +import nl.reinkrul.nuts.ApiException; +import nl.reinkrul.nuts.vcr.CredentialApi; +import nl.reinkrul.nuts.vcr.IssueVCRequest; +import nl.reinkrul.nuts.vcr.IssueVCRequestType; + +import java.util.Map; + +public class CredentialExamples { + + public void issueNutsOrganizationCredential() throws ApiException { + var client = new CredentialApi(); + var issuedVC = client.issueVC(new IssueVCRequest() + // General VC properties + .type(new IssueVCRequestType("NutsOrganizationCredential")) + .issuer("did:web:example.com:issuer") // the DID of the issuer of the credential (DID of software vendor) + .withStatusList2021Revocation(true) // enable revocation + // Subject of the credential + .credentialSubject( + Map.of( + "id", "did:web:example.com:holder", + "organization", Map.of( + "name", "Extra Careful B.V.", + "city", "Zorgdorp" + ) + ) + ) + ); + + System.out.println("VC has been issued, ID: " + issuedVC.getId()); + } +} From b2292da40390c394b9a0eea70630dd8d69bb36a4 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 14 Jun 2024 06:33:49 +0200 Subject: [PATCH 2/5] Udpate readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5cbf96..f017b85 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Find all versions on [Maven central](https://search.maven.org/artifact/nl.reinkr # Usage The example below instantiates the API client for VDR and calls `getDID` for `subjectDID`: ```java -var apiClient = new nl.reinkrul.nuts.ApiClient(); +var apiClient = new nl.reinkrul.nuts.Configuration.getDefaultApiClient(); var didApi = new nl.reinkrul.nuts.vdr.DidApi(apiClient); var didDocument = didApi.getDID(subjectDID, null, null); From 9b2fe393a1d0a48cfb7b0f0587492c3415675c83 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 27 Sep 2024 16:52:28 +0200 Subject: [PATCH 3/5] VC/VP (de)serialization, integration test --- Makefile | 2 +- pom.xml | 32 ++--- .../java/nl/reinkrul/nuts/Configuration.java | 11 +- .../nuts/common/VerifiableCredential.java | 24 ++++ .../VerifiableCredentialDeserializer.java | 23 ++-- .../VerifiableCredentialSerializer.java | 28 +++++ .../nuts/common/VerifiablePresentation.java | 12 ++ .../VerifiablePresentationDeserializer.java | 15 +-- .../VerifiablePresentationSerializer.java | 22 ++++ .../VerifiableCredentialDeserializerTest.java | 65 +++++++++- .../java/nl/reinkrul/nuts/v5/AuthApiTest.java | 62 --------- .../nl/reinkrul/nuts/v6/IntegrationTest.java | 77 +++++++++--- .../java/nl/reinkrul/nuts/v6/VdrTest.java | 58 --------- src/test/java/v5/CredentialExamples.java | 119 ------------------ 14 files changed, 246 insertions(+), 304 deletions(-) create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialSerializer.java create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java create mode 100644 src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationSerializer.java delete mode 100644 src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java delete mode 100644 src/test/java/nl/reinkrul/nuts/v6/VdrTest.java delete mode 100644 src/test/java/v5/CredentialExamples.java diff --git a/Makefile b/Makefile index bbbca63..11a4e6d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: generate generate: - mvn clean generate-sources + mvn clean process-sources install: mvn clean install diff --git a/pom.xml b/pom.xml index 0837b74..b913b39 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 18 18 - master + v6.0.0-rc.4 https://raw.githubusercontent.com/nuts-foundation/nuts-node/${nuts.version}/docs/_static @@ -203,8 +203,8 @@ - + @@ -290,8 +290,8 @@ DIDDocument=nl.reinkrul.nuts.common.DIDDocument, - VerifiableCredential=com.danubetech.verifiablecredentials.VerifiableCredential, - VerifiablePresentation=com.danubetech.verifiablecredentials.VerifiablePresentation, + VerifiableCredential=nl.reinkrul.nuts.common.VerifiableCredential, + VerifiablePresentation=nl.reinkrul.nuts.common.VerifiablePresentation, DIDDocumentMetadata=nl.reinkrul.nuts.common.DIDDocumentMetadata, EmbeddedProof=nl.reinkrul.nuts.common.EmbeddedProof, @@ -327,8 +327,8 @@ DIDDocument=nl.reinkrul.nuts.common.DIDDocument, - VerifiableCredential=com.danubetech.verifiablecredentials.VerifiableCredential, - VerifiablePresentation=com.danubetech.verifiablecredentials.VerifiablePresentation + VerifiableCredential=nl.reinkrul.nuts.common.VerifiableCredential, + VerifiablePresentation=nl.reinkrul.nuts.common.VerifiablePresentation nl.reinkrul.nuts.common @@ -338,20 +338,6 @@ - - generate-vdr-v1-client - - generate - - - ${openapi.spec.dir}/vdr/v1.yaml - - nl.reinkrul.nuts.vdr.v1 - nl.reinkrul.nuts.vdr.v1 - nl.reinkrul.nuts - - - generate-vdr-v2-client @@ -360,8 +346,8 @@ ${openapi.spec.dir}/vdr/v2.yaml - nl.reinkrul.nuts.vdr.v2 - nl.reinkrul.nuts.vdr.v2 + nl.reinkrul.nuts.vdr + nl.reinkrul.nuts.vdr nl.reinkrul.nuts diff --git a/src/main/java/nl/reinkrul/nuts/Configuration.java b/src/main/java/nl/reinkrul/nuts/Configuration.java index 8568d0c..073c012 100644 --- a/src/main/java/nl/reinkrul/nuts/Configuration.java +++ b/src/main/java/nl/reinkrul/nuts/Configuration.java @@ -1,10 +1,7 @@ package nl.reinkrul.nuts; -import com.danubetech.verifiablecredentials.VerifiableCredential; -import com.danubetech.verifiablecredentials.VerifiablePresentation; import com.fasterxml.jackson.databind.module.SimpleModule; -import nl.reinkrul.nuts.common.VerifiableCredentialDeserializer; -import nl.reinkrul.nuts.common.VerifiablePresentationDeserializer; +import nl.reinkrul.nuts.common.*; public class Configuration { @@ -34,8 +31,10 @@ public static ApiClient create() { var result = new ApiClient(); result.setUserAgent("nuts-java-client"); var module = new SimpleModule(); - module.addDeserializer(VerifiableCredential.class, new VerifiableCredentialDeserializer(VerifiableCredential.class)); - module.addDeserializer(VerifiablePresentation.class, new VerifiablePresentationDeserializer(VerifiablePresentation.class)); + module.addDeserializer(VerifiableCredential.class, new VerifiableCredentialDeserializer()); + module.addSerializer(VerifiableCredential.class, new VerifiableCredentialSerializer()); + module.addDeserializer(VerifiablePresentation.class, new VerifiablePresentationDeserializer()); + module.addSerializer(VerifiablePresentation.class, new VerifiablePresentationSerializer()); result.getJSON().getMapper().registerModule(module); return result; } diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java new file mode 100644 index 0000000..e1606f1 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java @@ -0,0 +1,24 @@ +package nl.reinkrul.nuts.common; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Map; + +public class VerifiableCredential extends com.danubetech.verifiablecredentials.VerifiableCredential { + public VerifiableCredential(Map jsonObject, String source) { + super(jsonObject); + this.source = source; + } + + public final String source; + + public VerifiableCredential(com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential) { + super(employeeCredential.getJsonObject()); + try { + source = new ObjectMapper().writeValueAsString(employeeCredential); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java index 53382d5..5470370 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializer.java @@ -1,36 +1,41 @@ package nl.reinkrul.nuts.common; -import com.danubetech.verifiablecredentials.VerifiableCredential; import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.StringReader; import java.text.ParseException; +import java.util.Map; public class VerifiableCredentialDeserializer extends StdDeserializer { - public VerifiableCredentialDeserializer(Class vc) { - super(vc); + public VerifiableCredentialDeserializer() { + super(VerifiableCredential.class); } @Override public VerifiableCredential deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { if (jsonParser.getCurrentToken().isScalarValue()) { try { - return JwtVerifiableCredential.fromCompactSerialization(jsonParser.getValueAsString()).getPayloadObject(); + final String valueAsString = jsonParser.getValueAsString(); + JwtVerifiableCredential jwtVC = JwtVerifiableCredential.fromCompactSerialization(valueAsString); + Map claims = jwtVC.getPayload().getClaims(); + VerifiableCredential vc = new VerifiableCredential(jwtVC.getPayloadObject().getJsonObject(), "\"" + valueAsString + "\""); + if (vc.getId() == null) { + // Take from jti claim + vc.setJsonObjectKeyValue("id", (String) claims.get("jti")); + } + return vc; } catch (ParseException e) { throw new IOException(e); } } if (jsonParser.getCurrentToken().isStructStart()) { - + var asString = jsonParser.readValueAsTree().toString(); + return new VerifiableCredential(VerifiableCredential.fromJson(asString).getJsonObject(), asString); } throw new IOException("Unexpected token: " + jsonParser.getCurrentToken().toString()); } diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialSerializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialSerializer.java new file mode 100644 index 0000000..5b2c675 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredentialSerializer.java @@ -0,0 +1,28 @@ +package nl.reinkrul.nuts.common; + +import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.text.ParseException; + +public class VerifiableCredentialSerializer extends StdSerializer { + + public VerifiableCredentialSerializer() { + super(VerifiableCredential.class); + } + + @Override + public void serialize(VerifiableCredential value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if ("".equals(value.source)) { + throw new IOException("Can only unmarshal VerifiableCredential which has been deserialized earlier."); + } + gen.writeRawValue(value.source); + } +} diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java new file mode 100644 index 0000000..6026ec7 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java @@ -0,0 +1,12 @@ +package nl.reinkrul.nuts.common; + +import java.util.Map; + +public class VerifiablePresentation extends com.danubetech.verifiablecredentials.VerifiablePresentation { + public VerifiablePresentation(Map jsonObject, String source) { + super(jsonObject); + this.source = source; + } + + public final String source; +} diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java index ce6e54d..448b9cf 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java @@ -1,35 +1,30 @@ package nl.reinkrul.nuts.common; -import com.danubetech.verifiablecredentials.VerifiableCredential; -import com.danubetech.verifiablecredentials.VerifiablePresentation; -import com.danubetech.verifiablecredentials.jwt.JwtVerifiableCredential; import com.danubetech.verifiablecredentials.jwt.JwtVerifiablePresentation; -import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.text.ParseException; public class VerifiablePresentationDeserializer extends StdDeserializer { - public VerifiablePresentationDeserializer(Class vc) { - super(vc); + public VerifiablePresentationDeserializer() { + super(VerifiablePresentation.class); } @Override public VerifiablePresentation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { var token = jsonParser.nextValue(); + String valueAsString = jsonParser.getValueAsString(); if (token == JsonToken.START_OBJECT) { // Parse as JSON-LD - return com.danubetech.verifiablecredentials.VerifiablePresentation.fromJson(new InputStreamReader(new ByteArrayInputStream(token.asByteArray()))); + return new VerifiablePresentation(com.danubetech.verifiablecredentials.VerifiablePresentation.fromJson(valueAsString).getJsonObject(), valueAsString); } else if (token == JsonToken.VALUE_STRING) { try { - return JwtVerifiablePresentation.fromCompactSerialization(token.asString()).getPayloadObject(); + return new VerifiablePresentation(JwtVerifiablePresentation.fromCompactSerialization(token.asString()).getPayloadObject().getJsonObject(), valueAsString); } catch (ParseException e) { throw new IOException(e); } diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationSerializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationSerializer.java new file mode 100644 index 0000000..927a5cf --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationSerializer.java @@ -0,0 +1,22 @@ +package nl.reinkrul.nuts.common; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; + +public class VerifiablePresentationSerializer extends StdSerializer { + + public VerifiablePresentationSerializer() { + super(VerifiablePresentation.class); + } + + @Override + public void serialize(VerifiablePresentation value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if ("".equals(value.source)) { + throw new IOException("Can only unmarshal VerifiablePresentation which has been deserialized earlier."); + } + gen.writeRawValue(value.source); + } +} \ No newline at end of file diff --git a/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java b/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java index a4f427c..4aebf96 100644 --- a/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java +++ b/src/test/java/nl/reinkrul/nuts/common/VerifiableCredentialDeserializerTest.java @@ -1,12 +1,75 @@ package nl.reinkrul.nuts.common; +import com.danubetech.verifiablecredentials.VerifiableCredential; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; import org.junit.jupiter.api.Test; +import java.io.IOException; + import static org.junit.jupiter.api.Assertions.*; class VerifiableCredentialDeserializerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + public VerifiableCredentialDeserializerTest() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(nl.reinkrul.nuts.common.VerifiableCredential.class, new VerifiableCredentialDeserializer()); + mapper.registerModule(module); + } + @Test - void deserialize() { + void deserializeJWT() throws IOException { + var jwt = "\"eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6bnV0cy5ubDppYW06YmRhMDI0ZTMtYjk0My00ZDRkLTk0MjQtYTEwNjM3NmE1YmM3IzY4ODA2ZjU1LWVkMzEtNGQ3Yy1hNmMxLWJkYjc1YzIxMTc0NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6d2ViOm51dHMubmw6aWFtOmJkYTAyNGUzLWI5NDMtNGQ0ZC05NDI0LWExMDYzNzZhNWJjNyIsImp0aSI6ImRpZDp3ZWI6bnV0cy5ubDppYW06YmRhMDI0ZTMtYjk0My00ZDRkLTk0MjQtYTEwNjM3NmE1YmM3IzlhMDRkMWM0LWE4YTItNDg1Zi1hMWJmLWVkNGVhMTQyNTdmOSIsIm5iZiI6MTcyNzQ0Mjg0Mywic3ViIjoiZGlkOndlYjpudXRzLm5sOmlhbTpiZGEwMjRlMy1iOTQzLTRkNGQtOTQyNC1hMTA2Mzc2YTViYzciLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vbnV0cy5ubC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vdzNpZC5vcmcvdmMvc3RhdHVzLWxpc3QvMjAyMS92MSJdLCJjcmVkZW50aWFsU3RhdHVzIjpbeyJpZCI6Imh0dHBzOi8vbnV0cy5ubC9zdGF0dXNsaXN0L2RpZDp3ZWI6bnV0cy5ubDppYW06YmRhMDI0ZTMtYjk0My00ZDRkLTk0MjQtYTEwNjM3NmE1YmM3LzEjMCIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzUHVycG9zZSI6InJldm9jYXRpb24iLCJzdGF0dXNMaXN0SW5kZXgiOiIwIiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwczovL251dHMubmwvc3RhdHVzbGlzdC9kaWQ6d2ViOm51dHMubmw6aWFtOmJkYTAyNGUzLWI5NDMtNGQ0ZC05NDI0LWExMDYzNzZhNWJjNy8xIn1dLCJjcmVkZW50aWFsU3ViamVjdCI6W3siaWQiOiJkaWQ6d2ViOm51dHMubmw6aWFtOmJkYTAyNGUzLWI5NDMtNGQ0ZC05NDI0LWExMDYzNzZhNWJjNyIsImlkZW50aWZpZXIiOiIxMjM0IiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZU5hbWUiOiJTb2Z0d2FyZSBFbmdpbmVlciJ9XSwidHlwZSI6WyJFbXBsb3llZUNyZWRlbnRpYWwiLCJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdfX0.SglTXKcIhYFXlXyMEn8hBidoG7Ho21_t6pvl4CC-UYWuQyZZs6kjgBgtgV6dt_iwHqx3CqRQKuNJGwK48L6k8g\""; + var result = mapper.readValue(jwt, nl.reinkrul.nuts.common.VerifiableCredential.class); + assertEquals(jwt, result.source); + assertEquals("did:web:nuts.nl:iam:bda024e3-b943-4d4d-9424-a106376a5bc7#9a04d1c4-a8a2-485f-a1bf-ed4ea14257f9", result.getId().toString()); + } + + @Test + void deserializeJSONLD() { + var raw = """ +{ + "@context": [ + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json", + "https://www.w3.org/2018/credentials/v1", + "https://nuts.nl/credentials/2024", + "https://w3id.org/vc/status-list/2021/v1" + ], + "credentialStatus": { + "id": "https://zorgbijjou.test.integration.zorgbijjou.com/nuts/statuslist/did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:4f6e2a8b-fb82-4b2e-aae7-283060d05167/1#1", + "statusListCredential": "https://zorgbijjou.test.integration.zorgbijjou.com/nuts/statuslist/did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:4f6e2a8b-fb82-4b2e-aae7-283060d05167/1", + "statusListIndex": "1", + "statusPurpose": "revocation", + "type": "StatusList2021Entry" + }, + "credentialSubject": { + "id": "did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:914ab62d-9ae4-4dd0-bf76-022c7bec0f6a", + "organization": { + "city": "111", + "name": "11111", + "ura": "111" + } + }, + "expirationDate": "2025-09-27T14:27:27.023Z", + "id": "did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:4f6e2a8b-fb82-4b2e-aae7-283060d05167#9475dfac-1581-41e4-97e4-bdd81b65945e", + "issuanceDate": "2024-09-27T14:27:27.113601538Z", + "issuer": "did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:4f6e2a8b-fb82-4b2e-aae7-283060d05167", + "proof": { + "created": "2024-09-27T14:27:27.113601538Z", + "jws": "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sImtpZCI6ImRpZDp3ZWI6em9yZ2JpampvdS50ZXN0LmludGVncmF0aW9uLnpvcmdiaWpqb3UuY29tOm51dHM6aWFtOjRmNmUyYThiLWZiODItNGIyZS1hYWU3LTI4MzA2MGQwNTE2NyMxNjczMjYwNC1lYzI2LTQ4YTctODI4OC0xYTJmMTUyOGY1MjIifQ..ovZs_rJrW6ScqNlekJpelOmpf2nD9Ak2q_unTdNZVj5602VZkVik6KOrGf7JBNkRQlHGnetd2auCUQtwhJ7yEg", + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:web:zorgbijjou.test.integration.zorgbijjou.com:nuts:iam:4f6e2a8b-fb82-4b2e-aae7-283060d05167#16732604-ec26-48a7-8288-1a2f1528f522" + }, + "type": [ + "NutsUraCredential", + "VerifiableCredential" + ] +} +"""; + assertDoesNotThrow(() -> mapper.readValue(raw, nl.reinkrul.nuts.common.VerifiableCredential.class)); } } \ No newline at end of file diff --git a/src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java b/src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java deleted file mode 100644 index ad9e685..0000000 --- a/src/test/java/nl/reinkrul/nuts/v5/AuthApiTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package nl.reinkrul.nuts.v5; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpServer; -import nl.reinkrul.nuts.ApiClient; -import nl.reinkrul.nuts.ApiException; -import nl.reinkrul.nuts.Configuration; -import nl.reinkrul.nuts.auth.v1.AuthApi; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.ServerSocket; - -import static org.junit.jupiter.api.Assertions.*; - -public class AuthApiTest { - - private static HttpServer server; - - private static Headers actualRequestHeaders; - private static byte[] actualRequestBody; - - @BeforeAll - public static void setup() throws IOException { - int port; - try (ServerSocket serverSocket = new ServerSocket(0)) { - port = serverSocket.getLocalPort(); - } - - server = HttpServer.create(new InetSocketAddress("localhost", port), 10); - server.createContext("/internal/auth/v1/accesstoken/introspect", exchange -> { - actualRequestHeaders = exchange.getRequestHeaders(); - actualRequestBody = exchange.getRequestBody().readAllBytes(); - exchange.getResponseHeaders().set("Content-Type", "application/json"); - exchange.sendResponseHeaders(200, 0); - exchange.getResponseBody().write("{\"sub\": \"admin\"}".getBytes()); - exchange.close(); - } - ); - server.setExecutor(null); - server.start(); - } - - @AfterAll - public static void tearDown() { - server.stop(0); - } - - @Test - public void introspectAccessToken() throws ApiException { - var client = Configuration.getDefaultApiClient(); - client.setBasePath("http://" + server.getAddress().getHostName() + ":" + server.getAddress().getPort()); - var authApi = new AuthApi(client); - var response = authApi.introspectAccessToken("some-token"); - - assertEquals("token=some-token", new String(actualRequestBody)); - assertEquals("admin", response.getSub()); - } -} diff --git a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java index 201b92d..6013428 100644 --- a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java +++ b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java @@ -1,12 +1,21 @@ package nl.reinkrul.nuts.v6; +import com.danubetech.verifiablecredentials.CredentialSubject; import nl.reinkrul.nuts.ApiException; import nl.reinkrul.nuts.Configuration; +import nl.reinkrul.nuts.auth.v2.AuthApi; +import nl.reinkrul.nuts.auth.v2.ServiceAccessTokenRequest; +import nl.reinkrul.nuts.auth.v2.TokenIntrospectionResponse; +import nl.reinkrul.nuts.auth.v2.TokenResponse; +import nl.reinkrul.nuts.common.DIDDocument; +import nl.reinkrul.nuts.common.VerifiableCredential; import nl.reinkrul.nuts.vcr.CredentialApi; import nl.reinkrul.nuts.vcr.IssueVCRequest; +import nl.reinkrul.nuts.vcr.IssueVCRequestContext; import nl.reinkrul.nuts.vcr.IssueVCRequestType; -import nl.reinkrul.nuts.vdr.v2.CreateDIDOptions; -import nl.reinkrul.nuts.vdr.v2.DidApi; +import nl.reinkrul.nuts.vdr.CreateSubjectOptions; +import nl.reinkrul.nuts.vdr.SubjectApi; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.Map; @@ -14,30 +23,68 @@ public class IntegrationTest { @Test - public void createDIDDocumentIssueVC() throws ApiException { + public void createSubjectIssueVC() throws ApiException { var apiClient = Configuration.getDefaultApiClient(); apiClient.setBasePath("http://localhost:8081"); - var didApi = new DidApi(apiClient); - var didDocument = didApi.createDID(new CreateDIDOptions()); + var subjectApi = new SubjectApi(apiClient); + var authApi = new AuthApi(apiClient); + // Create subject + var subjectCreationResult = subjectApi.createSubject(new CreateSubjectOptions()); + System.out.println(subjectCreationResult.getSubject()); + var subjectDID = subjectCreationResult.getDocuments().get(0).getId(); var credentialApi = new CredentialApi(apiClient); - var issuedVC = credentialApi.issueVC(new IssueVCRequest() - .type(new IssueVCRequestType("EmployeeCredential")) - .issuer(didDocument.getId().toString()) - .withStatusList2021Revocation(true) + + // Issue VC + var nutsUraCredential = credentialApi.issueVC(new IssueVCRequest() + .atContext(new IssueVCRequestContext("https://nuts.nl/credentials/2024")) + .type(new IssueVCRequestType("NutsUraCredential")) + .issuer(subjectDID.toString()) + .withStatusList2021Revocation(false) .publishToNetwork(null) .visibility(null) - .format(IssueVCRequest.FormatEnum.JWT_VC) + .format(IssueVCRequest.FormatEnum.LDP_VC) .credentialSubject( Map.of( - "id", "did:jwk:1234", - "name", "John Doe", - "roleName", "Software Engineer", - "identifier", "1234" + "id", subjectDID.toString(), + "organization", Map.of( + "ura", "12345", + "name", "Extra Careful B.V.", + "city", "Zorgdorp" + ) ) ) ); - System.out.println(issuedVC.getId()); + Assertions.assertNotNull(nutsUraCredential.getId()); + + // Load it into wallet + var subjectID = subjectCreationResult.getSubject(); + credentialApi.loadVC(subjectID, nutsUraCredential); + + // List it + var vcs = credentialApi.getCredentialsInWallet(subjectID); + Assertions.assertEquals(1, vcs.size()); + Assertions.assertEquals(nutsUraCredential.source, vcs.get(0).source); + + // Request Access Token + com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential = VerifiableCredential + .builder() + .credentialSubject(CredentialSubject + .builder() + .id(subjectDID) + .build() + ) + .build(); + TokenResponse accessTokenResponse = authApi.requestServiceAccessToken(subjectID, new ServiceAccessTokenRequest() + .tokenType(ServiceAccessTokenRequest.TokenTypeEnum.BEARER) + .addCredentialsItem(new VerifiableCredential(employeeCredential)) + .scope("test") + .authorizationServer("http://localhost:8080/oauth2/" + subjectID) + ); + + // Check access token + TokenIntrospectionResponse tokenIntrospectionResponse = authApi.introspectAccessToken(accessTokenResponse.getAccessToken()); + Assertions.assertTrue(tokenIntrospectionResponse.getActive()); } } diff --git a/src/test/java/nl/reinkrul/nuts/v6/VdrTest.java b/src/test/java/nl/reinkrul/nuts/v6/VdrTest.java deleted file mode 100644 index 620749a..0000000 --- a/src/test/java/nl/reinkrul/nuts/v6/VdrTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package nl.reinkrul.nuts.v6; - -import com.sun.net.httpserver.HttpServer; -import nl.reinkrul.nuts.ApiException; -import nl.reinkrul.nuts.Configuration; -import nl.reinkrul.nuts.vdr.v2.DidApi; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.net.ServerSocket; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class VdrTest { - - private static HttpServer server; - - @BeforeAll - public static void setup() throws IOException { - int port; - try (ServerSocket serverSocket = new ServerSocket(0)) { - port = serverSocket.getLocalPort(); - } - - server = HttpServer.create(new InetSocketAddress("localhost", port), 10); - server.createContext("/internal/vdr/v2/did/did:nuts:abcdefghijklmnop", exchange -> { - exchange.getResponseHeaders().set("Content-Type", "application/json"); - exchange.sendResponseHeaders(200, 0); - try (InputStream inputStream = VdrTest.class.getResourceAsStream("/did-resolution-result.json")) { - exchange.getResponseBody().write(inputStream.readAllBytes()); - } - exchange.close(); - } - ); - server.setExecutor(null); - server.start(); - } - - @AfterAll - public static void tearDown() { - server.stop(0); - } - - @Test - public void didDocument() throws ApiException { - var apiClient = Configuration.getDefaultApiClient(); - apiClient.setBasePath("http://localhost:" + server.getAddress().getPort()); - var didApi = new DidApi(apiClient); - - var result = didApi.resolveDID("did:nuts:abcdefghijklmnop"); - - assertEquals("did:nuts:abcdefghijklmnop", result.getDocument().getId().toString()); - } -} diff --git a/src/test/java/v5/CredentialExamples.java b/src/test/java/v5/CredentialExamples.java deleted file mode 100644 index 42ef871..0000000 --- a/src/test/java/v5/CredentialExamples.java +++ /dev/null @@ -1,119 +0,0 @@ -package v5; - -import com.danubetech.verifiablecredentials.CredentialSubject; -import com.danubetech.verifiablecredentials.VerifiableCredential; -import nl.reinkrul.nuts.ApiException; -import nl.reinkrul.nuts.extra.*; -import nl.reinkrul.nuts.vcr.*; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; -import java.util.Map; - -public class CredentialExamples { - - public void searchNutsOrganizationCredential() throws ApiException, URISyntaxException { - var client = new CredentialApi(); - var query = VerifiableCredential.builder() - .type("NutsOrganizationCredential") - .contexts(List.of(new URI("https://nuts.nl/credentials/v1"), new URI("https://www.w3.org/2018/credentials/v1"))) - .issuer(new URI("did:nuts:some-did")) // the DID of the issuer of the credential (DID of software vendor) - .credentialSubject( - CredentialSubject.builder() - .id(new URI("did:nuts:some-other-did")) // the DID of the receiver of the credential - .claims(Map.of("organization", Map.of( - "name", "Extra Careful B.V.", - "city", "Zorgdorp" - ))) - .build() - ).build(); - var issuedVC = client.searchVCs(new SearchVCRequest() - // Search options - .searchOptions(new SearchOptions().allowUntrustedIssuer(false)) - // VC to match - .query(query) - ); - - for (SearchVCResult result : issuedVC.getVerifiableCredentials()) { - System.out.println(result.getVerifiableCredential().getId()); - } - } - - public void issueNutsOrganizationCredential() throws ApiException { - var client = new CredentialApi(); - var issuedVC = client.issueVC(new IssueVCRequest() - // General VC properties - .type(new IssueVCRequestType("NutsAuthenticationCredential")) - .issuer("did:nuts:some-did") // the DID of the issuer of the credential (DID of software vendor) - .visibility(IssueVCRequest.VisibilityEnum.PUBLIC) // publish on network, anyone can read it - // Subject of the credential - .credentialSubject( - new NutsOrganizationCredential() - .id("did:nuts:some-other-did") // the DID of the receiver of the credential - .organization(new Organization() - .name("Extra Careful B.V.") - .city("Zorgdorp") - ) - ) - ); - - System.out.println("VC has been issued, ID: " + issuedVC.getId()); - } - - public void issueNutsAuthorizationCredential() throws ApiException { - var client = new CredentialApi(); - var issuedVC = client.issueVC(new IssueVCRequest() - // General VC properties - .type(new IssueVCRequestType("NutsAuthenticationCredential")) - .issuer("did:nuts:some-did") // the DID of the issuer of the credential - .visibility(IssueVCRequest.VisibilityEnum.PRIVATE) // publish on network, but keep it private - // Subject of the credential - .credentialSubject( - new NutsAuthorizationCredential() - .id("did:nuts:some-other-did") // the DID of the receiver of the credential - .subject("1234567890") // social security number of the patient - .addResourcesItem( - // The FHIR resources that can be accessed using the credential - new FHIRResource() - .path("/Task/1") - .userContext(true) - .assuranceLevel(FHIRResource.AssuranceLevelEnum.LOW) - .addOperationsItem("read") - ) - ) - ); - - System.out.println("VC has been issued, ID: " + issuedVC.getId()); - } - - public void issueNutsEmployeeCredential() throws ApiException { - var client = new CredentialApi(); - var issuedVC = client.issueVC(new IssueVCRequest() - // General VC properties - .type(new IssueVCRequestType("NutsAuthenticationCredential")) - .issuer("did:nuts:some-did") // the DID of the issuer of the credential - // Subject of the credential - .credentialSubject( - new NutsEmployeeCredential() - .id("did:nuts:some-did") // the DID of the receiver of the credential, equal to the issuer for NutsEmployeeCredential - .type(NutsEmployeeCredential.TypeEnum.ORGANIZATION) // hardcoded - .member( - new OrganizationMember() - .identifier("12345678") - .type(OrganizationMember.TypeEnum.EMPLOYEEROLE) // hardcoded - .roleName("Verpleegkundige niveau 2") // optional - .member( - new OrganizationMemberMember() - .type(OrganizationMemberMember.TypeEnum.PERSON) // hardcoded - .initials("A.B.") - .familyName("van der Zorg") - ) - - ) - ) - ); - - System.out.println("VC has been issued, ID: " + issuedVC.getId()); - } -} From 1b32620a911eaa80a8f4afac9f802014d00c6aa8 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Sat, 28 Sep 2024 07:10:50 +0200 Subject: [PATCH 4/5] more working --- nutsnode/discovery/test.json | 57 ++++++++++++++ nutsnode/docker-compose.yaml | 18 +++++ nutsnode/policy/test.json | 78 +++++++++++++++++++ pom.xml | 2 +- .../nuts/common/VerifiableCredential.java | 11 ++- .../nuts/common/VerifiablePresentation.java | 3 + .../nl/reinkrul/nuts/v6/IntegrationTest.java | 21 ++++- 7 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 nutsnode/discovery/test.json create mode 100644 nutsnode/docker-compose.yaml create mode 100644 nutsnode/policy/test.json diff --git a/nutsnode/discovery/test.json b/nutsnode/discovery/test.json new file mode 100644 index 0000000..01f4ed3 --- /dev/null +++ b/nutsnode/discovery/test.json @@ -0,0 +1,57 @@ +{ + "id": "test", + "endpoint": "http://localhost:8080/discovery/test", + "presentation_max_validity": 2764800, + "presentation_definition": { + "id": "dev:eOverdracht2023", + "format": { + "ldp_vc": { + "proof_type": [ + "JsonWebSignature2020" + ] + }, + "jwt_vp": { + "alg": ["ES256"] + }, + "jwt_vc": { + "alg": ["ES256"] + } + }, + "input_descriptors": [ + { + "id": "SelfIssued_NutsUraCredential", + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "const": "NutsUraCredential" + } + }, + { + "id": "organization.name", + "path": [ + "$.credentialSubject.organization.name" + ], + "filter": { + "type": "string" + } + }, + { + "id": "organization.ura", + "path": [ + "$.credentialSubject.organization.ura" + ], + "filter": { + "type": "string" + } + } + ] + } + } + ] + } +} diff --git a/nutsnode/docker-compose.yaml b/nutsnode/docker-compose.yaml new file mode 100644 index 0000000..3381637 --- /dev/null +++ b/nutsnode/docker-compose.yaml @@ -0,0 +1,18 @@ +services: + nuts-node: + image: nutsfoundation/nuts-node:master + ports: + - "8081:8081" + - "8080:8080" + volumes: + - ./discovery:/opt/nuts/discovery:ro + - ./policy:/opt/nuts/policy:ro + environment: + NUTS_CRYPTO_STORAGE: fs + NUTS_HTTP_INTERNAL_ADDRESS: :8081 + NUTS_URL: http://localhost:8080 + NUTS_STRICTMODE: false + NUTS_DISCOVERY_DEFINITIONS_DIRECTORY: /opt/nuts/discovery + NUTS_DISCOVERY_SERVER_IDS: test + NUTS_POLICY_DIRECTORY: /opt/nuts/policy + NUTS_AUTH_CONTRACTVALIDATORS: dummy diff --git a/nutsnode/policy/test.json b/nutsnode/policy/test.json new file mode 100644 index 0000000..4c5e988 --- /dev/null +++ b/nutsnode/policy/test.json @@ -0,0 +1,78 @@ +{ + "test": { + "organization": { + "format": { + "ldp_vc": { + "proof_type": [ + "JsonWebSignature2020" + ] + }, + "jwt_vc": { + "alg": [ + "ES256" + ] + }, + "ldp_vp": { + "proof_type": [ + "JsonWebSignature2020" + ] + }, + "jwt_vp": { + "alg": [ + "ES256" + ] + } + }, + "id": "pd_any_care_organization_with_employee", + "name": "Care organization with employee", + "purpose": "Finding a care organization with logged in user for authorizing access to medical metadata", + "input_descriptors": [ + { + "id": "id_nuts_ura_credential", + "name": "Care organization", + "purpose": "Finding a care organization for authorizing access to medical metadata.", + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "const": "NutsUraCredential" + } + }, + { + "id": "organization_name", + "path": [ + "$.credentialSubject.organization.name" + ], + "filter": { + "type": "string" + } + }, + { + "id": "organization_ura", + "path": [ + "$.credentialSubject.organization.ura" + ], + "filter": { + "type": "string" + } + }, + { + "id": "organization_city", + "path": [ + "$.credentialSubject.organization.city" + ], + "filter": { + "type": "string" + } + } + ] + } + } + ] + } + } +} diff --git a/pom.xml b/pom.xml index b913b39..0f6ffe5 100644 --- a/pom.xml +++ b/pom.xml @@ -218,7 +218,7 @@ dest="${openapi.spec.dir}/didman/v1.yaml"/> - diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java index e1606f1..8e92a57 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java @@ -11,8 +11,10 @@ public VerifiableCredential(Map jsonObject, String source) { this.source = source; } - public final String source; - + /** + * Constructor for VerifiableCredential for an unsigned credential; it will marshal to JSON-LD. + * @param employeeCredential + */ public VerifiableCredential(com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential) { super(employeeCredential.getJsonObject()); try { @@ -21,4 +23,9 @@ public VerifiableCredential(com.danubetech.verifiablecredentials.VerifiableCrede throw new RuntimeException(e); } } + + /** + * The raw source of the credential, as it was deserialized. + */ + public final String source; } diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java index 6026ec7..c7a280f 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentation.java @@ -8,5 +8,8 @@ public VerifiablePresentation(Map jsonObject, String source) { this.source = source; } + /** + * The raw source of the presentation, as it was deserialized. + */ public final String source; } diff --git a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java index 6013428..7026f10 100644 --- a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java +++ b/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java @@ -9,6 +9,8 @@ import nl.reinkrul.nuts.auth.v2.TokenResponse; import nl.reinkrul.nuts.common.DIDDocument; import nl.reinkrul.nuts.common.VerifiableCredential; +import nl.reinkrul.nuts.discovery.DiscoveryApi; +import nl.reinkrul.nuts.discovery.ServiceActivationRequest; import nl.reinkrul.nuts.vcr.CredentialApi; import nl.reinkrul.nuts.vcr.IssueVCRequest; import nl.reinkrul.nuts.vcr.IssueVCRequestContext; @@ -20,6 +22,7 @@ import java.util.Map; +// To run this integration test, start the Docker Compose file in the nutsnode directory. public class IntegrationTest { @Test @@ -29,14 +32,15 @@ public void createSubjectIssueVC() throws ApiException { var subjectApi = new SubjectApi(apiClient); var authApi = new AuthApi(apiClient); + var discoveryApi = new DiscoveryApi(apiClient); - // Create subject + // Admin: Create subject var subjectCreationResult = subjectApi.createSubject(new CreateSubjectOptions()); System.out.println(subjectCreationResult.getSubject()); var subjectDID = subjectCreationResult.getDocuments().get(0).getId(); var credentialApi = new CredentialApi(apiClient); - // Issue VC + // Admin: Issue VC var nutsUraCredential = credentialApi.issueVC(new IssueVCRequest() .atContext(new IssueVCRequestContext("https://nuts.nl/credentials/2024")) .type(new IssueVCRequestType("NutsUraCredential")) @@ -67,7 +71,7 @@ public void createSubjectIssueVC() throws ApiException { Assertions.assertEquals(1, vcs.size()); Assertions.assertEquals(nutsUraCredential.source, vcs.get(0).source); - // Request Access Token + // Application: Request Access Token com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential = VerifiableCredential .builder() .credentialSubject(CredentialSubject @@ -83,8 +87,17 @@ public void createSubjectIssueVC() throws ApiException { .authorizationServer("http://localhost:8080/oauth2/" + subjectID) ); - // Check access token + // PEP: Check access token TokenIntrospectionResponse tokenIntrospectionResponse = authApi.introspectAccessToken(accessTokenResponse.getAccessToken()); Assertions.assertTrue(tokenIntrospectionResponse.getActive()); + + // Application: Register on Discovery Service + discoveryApi.activateServiceForSubject("test", subjectID, new ServiceActivationRequest() + .registrationParameters(Map.of("fhir-url", "https://example.com/fhir")) + ); + + // Application: Search on Discovery Service + var services = discoveryApi.searchPresentations("test", Map.of("credentialSubject.organization.ura", "12345")); + Assertions.assertEquals(1, services.size()); } } From e912ebfb9232ee84de6e83c295b06867e59b379c Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Wed, 2 Oct 2024 08:52:41 +0200 Subject: [PATCH 5/5] Update readme --- README.md | 17 +-- nutsnode/docker-compose.yaml | 1 + nutsnode/policy/test.json | 65 +++++++- pom.xml | 34 +++-- specs/credentials.yaml | 143 ------------------ .../nuts/common/VerifiableCredential.java | 13 -- .../VerifiablePresentationDeserializer.java | 18 ++- .../nl/reinkrul/nuts/credentials/Context.java | 9 ++ .../credentials/NutsEmployeeCredential.java | 51 +++++++ .../reinkrul/nuts/discovery/DiscoveryApi.java | 47 ++++++ .../nuts/{v6 => }/IntegrationTest.java | 37 +++-- .../nuts/NutsEmployeeCredentialTest.java | 37 ----- ...erifiablePresentationDeserializerTest.java | 93 ++++++++++++ .../NutsEmployeeCredentialTest.java | 27 ++++ 14 files changed, 339 insertions(+), 253 deletions(-) delete mode 100644 specs/credentials.yaml create mode 100644 src/main/java/nl/reinkrul/nuts/credentials/Context.java create mode 100644 src/main/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredential.java create mode 100644 src/main/java/nl/reinkrul/nuts/discovery/DiscoveryApi.java rename src/test/java/nl/reinkrul/nuts/{v6 => }/IntegrationTest.java (77%) delete mode 100644 src/test/java/nl/reinkrul/nuts/NutsEmployeeCredentialTest.java create mode 100644 src/test/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializerTest.java create mode 100644 src/test/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredentialTest.java diff --git a/README.md b/README.md index f017b85..eb166d8 100644 --- a/README.md +++ b/README.md @@ -20,24 +20,11 @@ It is generated from the latest version of the OpenAPI specifications of the Nut Find all versions on [Maven central](https://search.maven.org/artifact/nl.reinkrul.nuts/java-client). -# Usage -The example below instantiates the API client for VDR and calls `getDID` for `subjectDID`: -```java -var apiClient = new nl.reinkrul.nuts.Configuration.getDefaultApiClient(); - -var didApi = new nl.reinkrul.nuts.vdr.DidApi(apiClient); -var didDocument = didApi.getDID(subjectDID, null, null); - -// do something with the resolved DID Document -``` - +# Usage and examples Since each module in the Nuts Node has its own OpenAPI specification, there is a client API generated for each of them. You can find in their own subpackage in `nl.reinkrul.nuts` (e.g. `nl.reinkrul.nuts.vdr`). -# Examples - -See [src/test/java/v6.CredentialExamples.java](src/test/java/v6.CredentialExamples.java) -for how to issue `NutsOrganizationCredential`, `NutsAuthenticationCredential` and `NutsEmployeeCredential`. +See [src/test/java/nl/reinkrul/nuts/IntegrationTest.java](src/test/java/nl/reinkrul/nuts/IntegrationTest.java) for an example of how to use the client. # Versioning diff --git a/nutsnode/docker-compose.yaml b/nutsnode/docker-compose.yaml index 3381637..b192bbd 100644 --- a/nutsnode/docker-compose.yaml +++ b/nutsnode/docker-compose.yaml @@ -16,3 +16,4 @@ services: NUTS_DISCOVERY_SERVER_IDS: test NUTS_POLICY_DIRECTORY: /opt/nuts/policy NUTS_AUTH_CONTRACTVALIDATORS: dummy + NUTS_VDR_DIDMETHODS: web diff --git a/nutsnode/policy/test.json b/nutsnode/policy/test.json index 4c5e988..b0edd30 100644 --- a/nutsnode/policy/test.json +++ b/nutsnode/policy/test.json @@ -45,7 +45,8 @@ { "id": "organization_name", "path": [ - "$.credentialSubject.organization.name" + "$.credentialSubject.organization.name", + "$.credentialSubject[0].organization.name" ], "filter": { "type": "string" @@ -54,7 +55,8 @@ { "id": "organization_ura", "path": [ - "$.credentialSubject.organization.ura" + "$.credentialSubject.organization.ura", + "$.credentialSubject[0].organization.ura" ], "filter": { "type": "string" @@ -63,7 +65,64 @@ { "id": "organization_city", "path": [ - "$.credentialSubject.organization.city" + "$.credentialSubject.organization.city", + "$.credentialSubject[0].organization.city" + ], + "filter": { + "type": "string" + } + } + ] + } + }, + { + "id": "id_employee_credential_cred", + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "const": "NutsEmployeeCredential" + } + }, + { + "id": "employee_identifier", + "path": [ + "$.credentialSubject.member.identifier", + "$.credentialSubject[0].member.identifier" + ], + "filter": { + "type": "string" + } + }, + { + "id": "employee_name", + "path": [ + "$.credentialSubject.member.member.familyName", + "$.credentialSubject[0].member.member.familyName" + ], + "filter": { + "type": "string" + } + }, + { + "id": "employee_initials", + "path": [ + "$.credentialSubject.member.member.initials", + "$.credentialSubject[0].member.member.initials" + ], + "filter": { + "type": "string" + } + }, + { + "id": "employee_role", + "path": [ + "$.credentialSubject.member.roleName", + "$.credentialSubject[0].member.roleName" ], "filter": { "type": "string" diff --git a/pom.xml b/pom.xml index 0f6ffe5..125f96d 100644 --- a/pom.xml +++ b/pom.xml @@ -218,7 +218,7 @@ dest="${openapi.spec.dir}/didman/v1.yaml"/> - @@ -238,6 +238,24 @@ + + + replace-discovery-tag + generate-sources + + run + + + + + + + + + remove-generated-configuration-class @@ -302,20 +320,6 @@ - - generate-additions - - generate - - - specs/credentials.yaml - false - - nl.reinkrul.nuts.extra - nl.reinkrul.nuts.extra - - - generate-common-ssi-types diff --git a/specs/credentials.yaml b/specs/credentials.yaml deleted file mode 100644 index dfd8794..0000000 --- a/specs/credentials.yaml +++ /dev/null @@ -1,143 +0,0 @@ -openapi: 3.0.0 -info: - title: Additional OpenAPI spec for interacting with the Nuts APIs. - version: 1.0.0 - -components: - schemas: - ID: - type: string - description: Identifies the subject of the credential. In other words, the ID of the entity to which the credential was issued. Generally a Nuts DID. - Organization: - type: object - properties: - "name": - description: The name of the organization. - type: string - "city": - description: The city of the organization. - type: string - NutsOrganizationCredential: - type: object - description: The subject of a NutsOrganizationCredential according to the Nuts specs. - required: - - "id" - - "organization" - properties: - "id": - $ref: '#/components/schemas/ID' - "organization": - $ref: '#/components/schemas/Organization' - NutsAuthorizationCredential: - type: object - description: The subject of a NutsAuthorizationCredential according to the Nuts specs. - required: - - id - - purposeOfUse - - subject - properties: - "id": - $ref: '#/components/schemas/ID' - "purposeOfUse": - description: Generally an access policy as defined by the Bolt. - type: string - "subject": - type: string - description: Identifier of the patient (Dutch Social Security Number). - "resources": - description: The FHIR resources that can be accessed using the credential. - type: array - items: - $ref: '#/components/schemas/FHIRResource' - FHIRResource: - type: object - required: - - path - - operations - properties: - "path": - description: The path of the resource. - type: string - example: /Task/1 - "operations": - description: The FHIR operations that are allowed on the resource. - type: array - items: - type: string - "userContext": - description: Indicates whether access to the resource requires an authenticated user. - type: boolean - "assuranceLevel": - description: The assurance level of the credential. - type: string - enum: - - "low" - - "substantial" - - "high" - NutsEmployeeCredential: - type: object - description: The subject of a NutsEmployeeCredential according to the Nuts specs. - required: - - "id" - - "type" - - "member" - properties: - "id": - $ref: '#/components/schemas/ID' - "type": - description: The type of the employee credential subject, must be "Organization". - example: Organization - enum: [Organization] - type: string - "member": - $ref: '#/components/schemas/OrganizationMember' - OrganizationMember: - type: object - description: Part of the subject of a NutsEmployeeCredential according to the Nuts specs. - required: - - "identifier" - - "type" - - "member" - properties: - identifier: - description: Organizational-wide unique identifier of the employee, e.g. a number, user name or e-mail address. - example: 12345678 - type: string - roleName: - description: The role of the employee within the organization. - type: string - example: "Verpleegkundige niveau 2" - type: - description: The type of the employee credential subject, must be "EmployeeRole". - example: EmployeeRole - enum: [EmployeeRole] - type: string - member: - $ref: '#/components/schemas/OrganizationMemberMember' - OrganizationMemberMember: - type: object - description: Part of the subject of a NutsEmployeeCredential according to the Nuts specs. - properties: - type: - description: The type of the employee credential subject, must be "Person". - example: Person - enum: [Person] - type: string - familyName: - description: The family name of the employee. - example: "Jansen" - type: string - initials: - description: The initials of the employee. - example: "A.B." - type: string - - -# Required to make it a valid OpenAPI spec (can be removed when migrating to OAS 3.1.0): -paths: - /bogus: - get: - operationId: bogus - responses: - "200": - description: Bogus \ No newline at end of file diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java index 8e92a57..fb800ff 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiableCredential.java @@ -11,19 +11,6 @@ public VerifiableCredential(Map jsonObject, String source) { this.source = source; } - /** - * Constructor for VerifiableCredential for an unsigned credential; it will marshal to JSON-LD. - * @param employeeCredential - */ - public VerifiableCredential(com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential) { - super(employeeCredential.getJsonObject()); - try { - source = new ObjectMapper().writeValueAsString(employeeCredential); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - /** * The raw source of the credential, as it was deserialized. */ diff --git a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java index 448b9cf..1f9b821 100644 --- a/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java +++ b/src/main/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializer.java @@ -17,18 +17,20 @@ public VerifiablePresentationDeserializer() { @Override public VerifiablePresentation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { - var token = jsonParser.nextValue(); - String valueAsString = jsonParser.getValueAsString(); - if (token == JsonToken.START_OBJECT) { - // Parse as JSON-LD - return new VerifiablePresentation(com.danubetech.verifiablecredentials.VerifiablePresentation.fromJson(valueAsString).getJsonObject(), valueAsString); - } else if (token == JsonToken.VALUE_STRING) { + if (jsonParser.getCurrentToken().isScalarValue()) { + var valueAsString = jsonParser.getValueAsString(); try { - return new VerifiablePresentation(JwtVerifiablePresentation.fromCompactSerialization(token.asString()).getPayloadObject().getJsonObject(), valueAsString); + return new VerifiablePresentation(JwtVerifiablePresentation.fromCompactSerialization(valueAsString).getPayloadObject().getJsonObject(), valueAsString); } catch (ParseException e) { throw new IOException(e); } } - throw new IOException("Unexpected token: " + token); + if (jsonParser.getCurrentToken().isStructStart()) { + // Parse as JSON-LD + var valueAsString = jsonParser.readValueAsTree().toString(); + return new VerifiablePresentation(com.danubetech.verifiablecredentials.VerifiablePresentation.fromJson(valueAsString).getJsonObject(), valueAsString); + + } + throw new IOException("Unexpected token: " + jsonParser.getCurrentToken().id()); } } diff --git a/src/main/java/nl/reinkrul/nuts/credentials/Context.java b/src/main/java/nl/reinkrul/nuts/credentials/Context.java new file mode 100644 index 0000000..7d4b04a --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/credentials/Context.java @@ -0,0 +1,9 @@ +package nl.reinkrul.nuts.credentials; + +import java.net.URI; + +public final class Context { + public static URI VerifiableCredentialV1 = URI.create("https://www.w3.org/2018/credentials/v1"); + public static URI NutsV1 = URI.create("https://nuts.nl/credentials/v1"); + public static URI Nuts2024 = URI.create("https://nuts.nl/credentials/2024"); +} diff --git a/src/main/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredential.java b/src/main/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredential.java new file mode 100644 index 0000000..553d627 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredential.java @@ -0,0 +1,51 @@ +package nl.reinkrul.nuts.credentials; + +import com.danubetech.verifiablecredentials.CredentialSubject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.reinkrul.nuts.common.VerifiableCredential; + +import java.net.URI; +import java.util.Map; + +public class NutsEmployeeCredential { + public final String identifier; + public final String initials; + public final String name; + public final String role; + + public NutsEmployeeCredential(String identifier, String initials, String name, String role) { + this.identifier = identifier; + this.initials = initials; + this.name = name; + this.role = role; + } + + public VerifiableCredential getCredential() { + var inner = com.danubetech.verifiablecredentials.VerifiableCredential.builder() + .context(Context.NutsV1) + .type("NutsEmployeeCredential") + .credentialSubject(CredentialSubject + .builder() + .type("Organization") + .claims(Map.of("member", Map.of( + "identifier", identifier, + "type", "EmployeeRole", + "roleName", role, + "member", Map.of( + "type", "Person", + "initials", initials, + "familyName", name + ) + ))) + .build()) + .build(); + final String source; + try { + source = new ObjectMapper().writeValueAsString(inner); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return new VerifiableCredential(inner.getJsonObject(), source); + } +} diff --git a/src/main/java/nl/reinkrul/nuts/discovery/DiscoveryApi.java b/src/main/java/nl/reinkrul/nuts/discovery/DiscoveryApi.java new file mode 100644 index 0000000..2533432 --- /dev/null +++ b/src/main/java/nl/reinkrul/nuts/discovery/DiscoveryApi.java @@ -0,0 +1,47 @@ +package nl.reinkrul.nuts.discovery; + +import jakarta.ws.rs.core.GenericType; +import nl.reinkrul.nuts.ApiClient; +import nl.reinkrul.nuts.ApiException; +import nl.reinkrul.nuts.ApiResponse; +import nl.reinkrul.nuts.Pair; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DiscoveryApi extends BaseDiscoveryApi { + + private final ApiClient apiClient; + + public DiscoveryApi(ApiClient apiClient) { + super(apiClient); + this.apiClient = apiClient; + } + + public ApiResponse> searchPresentationsWithHttpInfo(String serviceID, Map query) throws ApiException { + // Check required parameters + if (serviceID == null) { + throw new ApiException(400, "Missing the required parameter 'serviceID' when calling searchPresentations"); + } + + // Path parameters + String localVarPath = "/internal/discovery/v1/{serviceID}" + .replaceAll("\\{serviceID}", apiClient.escapeString(serviceID)); + + // Query parameters + // FIX: free form query parameters (style: form, explode: true) generates invalid query parameters + // when using Jersey (ends up being {key=value}). It's fixed for Okhttp (https://github.com/OpenAPITools/openapi-generator/issues/19225), + // but not yet for Jersey. + List localVarQueryParams = query.entrySet().stream().map(entry -> new Pair(entry.getKey(), entry.getValue())).collect(Collectors.toList()); + + String localVarAccept = apiClient.selectHeaderAccept("application/json", "application/problem+json"); + String localVarContentType = apiClient.selectHeaderContentType(); + String[] localVarAuthNames = new String[] {"jwtBearerAuth"}; + GenericType> localVarReturnType = new GenericType>() {}; + return apiClient.invokeAPI("DiscoveryApi.searchPresentations", localVarPath, "GET", localVarQueryParams, null, + new LinkedHashMap<>(), new LinkedHashMap<>(), new LinkedHashMap<>(), localVarAccept, localVarContentType, + localVarAuthNames, localVarReturnType, false); + } +} diff --git a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java b/src/test/java/nl/reinkrul/nuts/IntegrationTest.java similarity index 77% rename from src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java rename to src/test/java/nl/reinkrul/nuts/IntegrationTest.java index 7026f10..30d8135 100644 --- a/src/test/java/nl/reinkrul/nuts/v6/IntegrationTest.java +++ b/src/test/java/nl/reinkrul/nuts/IntegrationTest.java @@ -1,14 +1,10 @@ -package nl.reinkrul.nuts.v6; +package nl.reinkrul.nuts; -import com.danubetech.verifiablecredentials.CredentialSubject; -import nl.reinkrul.nuts.ApiException; -import nl.reinkrul.nuts.Configuration; import nl.reinkrul.nuts.auth.v2.AuthApi; import nl.reinkrul.nuts.auth.v2.ServiceAccessTokenRequest; import nl.reinkrul.nuts.auth.v2.TokenIntrospectionResponse; import nl.reinkrul.nuts.auth.v2.TokenResponse; -import nl.reinkrul.nuts.common.DIDDocument; -import nl.reinkrul.nuts.common.VerifiableCredential; +import nl.reinkrul.nuts.credentials.NutsEmployeeCredential; import nl.reinkrul.nuts.discovery.DiscoveryApi; import nl.reinkrul.nuts.discovery.ServiceActivationRequest; import nl.reinkrul.nuts.vcr.CredentialApi; @@ -25,7 +21,7 @@ // To run this integration test, start the Docker Compose file in the nutsnode directory. public class IntegrationTest { - @Test + //@Test public void createSubjectIssueVC() throws ApiException { var apiClient = Configuration.getDefaultApiClient(); apiClient.setBasePath("http://localhost:8081"); @@ -48,7 +44,7 @@ public void createSubjectIssueVC() throws ApiException { .withStatusList2021Revocation(false) .publishToNetwork(null) .visibility(null) - .format(IssueVCRequest.FormatEnum.LDP_VC) + .format(IssueVCRequest.FormatEnum.JWT_VC) .credentialSubject( Map.of( "id", subjectDID.toString(), @@ -72,17 +68,15 @@ public void createSubjectIssueVC() throws ApiException { Assertions.assertEquals(nutsUraCredential.source, vcs.get(0).source); // Application: Request Access Token - com.danubetech.verifiablecredentials.VerifiableCredential employeeCredential = VerifiableCredential - .builder() - .credentialSubject(CredentialSubject - .builder() - .id(subjectDID) - .build() - ) - .build(); + var employeeCredential = new NutsEmployeeCredential( + "12345", + "E", + "Careful", + "Caregiver" + ); TokenResponse accessTokenResponse = authApi.requestServiceAccessToken(subjectID, new ServiceAccessTokenRequest() - .tokenType(ServiceAccessTokenRequest.TokenTypeEnum.BEARER) - .addCredentialsItem(new VerifiableCredential(employeeCredential)) + .tokenType(ServiceAccessTokenRequest.TokenTypeEnum.DPOP) + .addCredentialsItem(employeeCredential.getCredential()) .scope("test") .authorizationServer("http://localhost:8080/oauth2/" + subjectID) ); @@ -90,6 +84,11 @@ public void createSubjectIssueVC() throws ApiException { // PEP: Check access token TokenIntrospectionResponse tokenIntrospectionResponse = authApi.introspectAccessToken(accessTokenResponse.getAccessToken()); Assertions.assertTrue(tokenIntrospectionResponse.getActive()); + Assertions.assertEquals("12345", tokenIntrospectionResponse.getAdditionalProperty("organization_ura")); + Assertions.assertEquals(employeeCredential.identifier, tokenIntrospectionResponse.getAdditionalProperty("employee_identifier")); + Assertions.assertEquals(employeeCredential.role, tokenIntrospectionResponse.getAdditionalProperty("employee_role")); + Assertions.assertEquals(employeeCredential.initials, tokenIntrospectionResponse.getAdditionalProperty("employee_initials")); + Assertions.assertEquals(employeeCredential.name, tokenIntrospectionResponse.getAdditionalProperty("employee_name")); // Application: Register on Discovery Service discoveryApi.activateServiceForSubject("test", subjectID, new ServiceActivationRequest() @@ -98,6 +97,6 @@ public void createSubjectIssueVC() throws ApiException { // Application: Search on Discovery Service var services = discoveryApi.searchPresentations("test", Map.of("credentialSubject.organization.ura", "12345")); - Assertions.assertEquals(1, services.size()); + Assertions.assertNotEquals(0, services.size()); } } diff --git a/src/test/java/nl/reinkrul/nuts/NutsEmployeeCredentialTest.java b/src/test/java/nl/reinkrul/nuts/NutsEmployeeCredentialTest.java deleted file mode 100644 index 53cb5e0..0000000 --- a/src/test/java/nl/reinkrul/nuts/NutsEmployeeCredentialTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package nl.reinkrul.nuts; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import nl.reinkrul.nuts.extra.NutsEmployeeCredential; -import nl.reinkrul.nuts.extra.OrganizationMember; -import nl.reinkrul.nuts.extra.OrganizationMemberMember; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class NutsEmployeeCredentialTest { - - @Test - public void testMarshalling() throws JsonProcessingException { - var credential = new NutsEmployeeCredential() - .id("did:nuts:some-did") // the DID of the receiver of the credential, equal to the issuer for NutsEmployeeCredential - .type(NutsEmployeeCredential.TypeEnum.ORGANIZATION) // hardcoded - .member( - new OrganizationMember() - .identifier("12345678") - .type(OrganizationMember.TypeEnum.EMPLOYEEROLE) // hardcoded - .roleName("Verpleegkundige niveau 2") // optional - .member( - new OrganizationMemberMember() - .type(OrganizationMemberMember.TypeEnum.PERSON) // hardcoded - .initials("A.B.") - .familyName("van der Zorg") - ) - - ); - - var expected = "{\"id\":\"did:nuts:some-did\",\"type\":\"Organization\",\"member\":{\"identifier\":\"12345678\",\"roleName\":\"Verpleegkundige niveau 2\",\"type\":\"EmployeeRole\",\"member\":{\"type\":\"Person\",\"familyName\":\"van der Zorg\",\"initials\":\"A.B.\"}}}"; - var actual = new ObjectMapper().writeValueAsString(credential); - - Assertions.assertEquals(expected, actual); - } -} diff --git a/src/test/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializerTest.java b/src/test/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializerTest.java new file mode 100644 index 0000000..532940a --- /dev/null +++ b/src/test/java/nl/reinkrul/nuts/common/VerifiablePresentationDeserializerTest.java @@ -0,0 +1,93 @@ +package nl.reinkrul.nuts.common; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class VerifiablePresentationDeserializerTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + public VerifiablePresentationDeserializerTest() { + SimpleModule module = new SimpleModule(); + module.addDeserializer(nl.reinkrul.nuts.common.VerifiablePresentation.class, new VerifiablePresentationDeserializer()); + mapper.registerModule(module); + } + + @Test + void deserializeJWT() throws JsonProcessingException { + var jwt = "\"eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBODA4MDppYW06NDI1MDA4ZTQtZWE2Zi00ZWQyLTljZDItNTUzMDlhM2E1MjA1I2U5YTIyMTQzLTQ3NjktNGI5Mi04MzQ3LWEwMGEwMzUwMzdlMSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdCJdLCJleHAiOjE3MzAyNjQxNjgsImp0aSI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBODA4MDppYW06NDI1MDA4ZTQtZWE2Zi00ZWQyLTljZDItNTUzMDlhM2E1MjA1Izc1Yjg4YmYxLWZlM2ItNDI4MC1hYjJiLWY5NDE1MjY0MWY5OCIsIm5iZiI6MTcyNzQ5OTM2OSwibm9uY2UiOiJ0eHhkSXVnQkpRa3ByQTNoNGtEWHdLbmllNzZFMENwb3gzOFdxekRrd3pzIiwic3ViIjoiZGlkOndlYjpsb2NhbGhvc3QlM0E4MDgwOmlhbTo0MjUwMDhlNC1lYTZmLTRlZDItOWNkMi01NTMwOWEzYTUyMDUiLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJob2xkZXIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTgwODA6aWFtOjQyNTAwOGU0LWVhNmYtNGVkMi05Y2QyLTU1MzA5YTNhNTIwNSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7IkBjb250ZXh0IjpbImh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vbGRzLWp3czIwMjAvY29udGV4dHMvbGRzLWp3czIwMjAtdjEuanNvbiIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9udXRzLm5sL2NyZWRlbnRpYWxzLzIwMjQiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTgwODA6aWFtOjQyNTAwOGU0LWVhNmYtNGVkMi05Y2QyLTU1MzA5YTNhNTIwNSIsIm9yZ2FuaXphdGlvbiI6eyJjaXR5IjoiWm9yZ2RvcnAiLCJuYW1lIjoiRXh0cmEgQ2FyZWZ1bCBCLlYuIiwidXJhIjoiMTIzNDUifX0sImlkIjoiZGlkOndlYjpsb2NhbGhvc3QlM0E4MDgwOmlhbTo0MjUwMDhlNC1lYTZmLTRlZDItOWNkMi01NTMwOWEzYTUyMDUjNzQ3MzVlMWItYzIyYi00ODVlLWIxNTYtZTk0MmQ4YzhkYmZmIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0yOFQwNDo1NjowOS4xOTczNzAzOFoiLCJpc3N1ZXIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTgwODA6aWFtOjQyNTAwOGU0LWVhNmYtNGVkMi05Y2QyLTU1MzA5YTNhNTIwNSIsInByb29mIjp7ImNyZWF0ZWQiOiIyMDI0LTA5LTI4VDA0OjU2OjA5LjE5NzM3MDM4WiIsImp3cyI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW1JMk5DSTZabUZzYzJVc0ltTnlhWFFpT2xzaVlqWTBJbDBzSW10cFpDSTZJbVJwWkRwM1pXSTZiRzlqWVd4b2IzTjBKVE5CT0RBNE1EcHBZVzA2TkRJMU1EQTRaVFF0WldFMlppMDBaV1F5TFRsalpESXROVFV6TURsaE0yRTFNakExSTJVNVlUSXlNVFF6TFRRM05qa3ROR0k1TWkwNE16UTNMV0V3TUdFd016VXdNemRsTVNKOS4uNVljZ3ZpUjNsU3RrUG9QdlplcjAtR0ZoYTc2UjVmUHlqZ0xlSzBSYUw0cWFOZElZVFhfN3ZNbkNjUEdTbG9YZ0xSSnpobTB6SFBPR215cllhOGFLbFEiLCJwcm9vZlB1cnBvc2UiOiJhc3NlcnRpb25NZXRob2QiLCJ0eXBlIjoiSnNvbldlYlNpZ25hdHVyZTIwMjAiLCJ2ZXJpZmljYXRpb25NZXRob2QiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTgwODA6aWFtOjQyNTAwOGU0LWVhNmYtNGVkMi05Y2QyLTU1MzA5YTNhNTIwNSNlOWEyMjE0My00NzY5LTRiOTItODM0Ny1hMDBhMDM1MDM3ZTEifSwidHlwZSI6WyJOdXRzVXJhQ3JlZGVudGlhbCIsIlZlcmlmaWFibGVDcmVkZW50aWFsIl19LHsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL251dHMubmwvY3JlZGVudGlhbHMvdjEiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiYXV0aFNlcnZlclVSTCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9vYXV0aDIvMTdhNjE2ODgtNGJiOS00ZjA4LTg1MWQtNTIxMmJiMTliN2JiIiwiZmhpci11cmwiOiJodHRwczovL2V4YW1wbGUuY29tL2ZoaXIiLCJpZCI6ImRpZDp3ZWI6bG9jYWxob3N0JTNBODA4MDppYW06NDI1MDA4ZTQtZWE2Zi00ZWQyLTljZDItNTUzMDlhM2E1MjA1In0sImlkIjoiZGMyNzk5MzktZjViMi00YWU4LWE3MmUtYjRhNzU4ODYzYmYxIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0yOFQwNDo1NjowOVoiLCJpc3N1ZXIiOiJkaWQ6d2ViOmxvY2FsaG9zdCUzQTgwODA6aWFtOjQyNTAwOGU0LWVhNmYtNGVkMi05Y2QyLTU1MzA5YTNhNTIwNSIsInByb29mIjpudWxsLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRGlzY292ZXJ5UmVnaXN0cmF0aW9uQ3JlZGVudGlhbCJdfV19fQ.m6H4eDhqaL2WQmrkdpsoEE2g7xeV5ZghR2hrd22GADfLfYyK2oky-SB7pdErMRiSkDKmPQXDHUWsCeM-hWPOyA\""; + var result = mapper.readValue(jwt, nl.reinkrul.nuts.common.VerifiablePresentation.class); + assertEquals(jwt, "\"" + result.source + "\""); + } + + @Test + void deserializeJSONLD() { + var raw = """ +{ + "@context": [ + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json", + "https://nuts.nl/credentials/v1", + "https://www.w3.org/2018/credentials/v1" + ], + "proof": { + "challenge": "EN:PractitionerLogin:v3 I hereby declare to act on behalf of CareBears located in Caretown. This declaration is valid from Wednesday, 19 April 2023 12:20:00 until Thursday, 20 April 2023 13:20:00.", + "created": "2023-04-20T09:53:03Z", + "expires": "2023-04-24T09:53:03Z", + "jws": "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sImtpZCI6ImRpZDpudXRzOjhOWXpmc25kWkpIaDZHcXpLaVNCcHlFUnJGeHVYNjR6NnRFNXJhYTduRWptI2JZY3VldDZFSG9qTWxhTXF3Tm9DM2M2ZXRLbFVIb0o5clJ2VXUzWktFRXcifQ..IqGTyxmKgQ2HQ6RuYSn2B0sFh-okj8aEYC1VGTtlm1eiLBVr2wnnp1fX9oifhWHocuEKURkuSubENeW-Z3nMHQ", + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm#bYcuet6EHojMlaMqwNoC3c6etKlUHoJ9rRvUu3ZKEEw" + }, + "type": [ + "VerifiablePresentation", + "NutsSelfSignedPresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://nuts.nl/credentials/v1", + "https://www.w3.org/2018/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" + ], + "credentialSubject": [ + { + "id": "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm", + "member": { + "identifier": "user@example.com", + "member": { + "familyName": "Tester", + "initials": "T", + "type": "Person" + }, + "roleName": "Verpleegkundige niveau 2", + "type": "EmployeeRole" + }, + "type": "Organization" + } + ], + "id": "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm#dde77e76-7e3c-483f-a813-2b851a6a969c", + "issuanceDate": "2023-04-20T08:52:45.941461+02:00", + "issuer": "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm", + "proof": { + "created": "2023-04-20T09:53:03Z", + "expires": "2023-04-24T09:53:03Z", + "jws": "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sImtpZCI6ImRpZDpudXRzOjhOWXpmc25kWkpIaDZHcXpLaVNCcHlFUnJGeHVYNjR6NnRFNXJhYTduRWptI2JZY3VldDZFSG9qTWxhTXF3Tm9DM2M2ZXRLbFVIb0o5clJ2VXUzWktFRXcifQ..VhEbDoth8GrAni_LhZm-12VnlJToAbX0FDg1Rf7u7qIy3W54IcxAxkZP28YxGG681WpufwPeqHrtnYLsW8Fh7w", + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "did:nuts:8NYzfsndZJHh6GqzKiSBpyERrFxuX64z6tE5raa7nEjm#bYcuet6EHojMlaMqwNoC3c6etKlUHoJ9rRvUu3ZKEEw" + }, + "type": [ + "NutsEmployeeCredential", + "VerifiableCredential" + ] + } + ] +} +"""; + assertDoesNotThrow(() -> mapper.readValue(raw, nl.reinkrul.nuts.common.VerifiablePresentation.class)); + } +} \ No newline at end of file diff --git a/src/test/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredentialTest.java b/src/test/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredentialTest.java new file mode 100644 index 0000000..9beeade --- /dev/null +++ b/src/test/java/nl/reinkrul/nuts/credentials/NutsEmployeeCredentialTest.java @@ -0,0 +1,27 @@ +package nl.reinkrul.nuts.credentials; + +import com.fasterxml.jackson.core.JsonProcessingException; +import nl.reinkrul.nuts.Configuration; +import nl.reinkrul.nuts.common.VerifiableCredential; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NutsEmployeeCredentialTest { + + @Test + void getCredential() throws JsonProcessingException { + var employeeIdentifier = "12345"; + var employeeInitials = "A.B."; + var employeeName = "John Doe"; + var employeeRole = "Manager"; + + var credential = new NutsEmployeeCredential(employeeIdentifier, employeeInitials, employeeName, employeeRole).getCredential(); + + var expected = "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\",\"https://nuts.nl/credentials/v1\"],\"type\":[\"VerifiableCredential\",\"NutsEmployeeCredential\"],\"credentialSubject\":{\"type\":\"Organization\",\"member\":{\"type\":\"EmployeeRole\",\"identifier\":\"12345\",\"member\":{\"type\":\"Person\",\"familyName\":\"John Doe\",\"initials\":\"A.B.\"},\"roleName\":\"Manager\"}}}"; + var expectedVC = Configuration.create().getJSON().getMapper().readValue(expected, VerifiableCredential.class); + var actualVC = Configuration.create().getJSON().getMapper().readValue(credential.source, VerifiableCredential.class); + assertEquals(expectedVC, actualVC); + } +} \ No newline at end of file