Skip to content

Commit

Permalink
feat: create pluggable CredentialService url resolver (#3654)
Browse files Browse the repository at this point in the history
* feat: create pluggable CredentialService url resolver

* DEPENDENCIES
  • Loading branch information
paullatzelsperger committed Nov 24, 2023
1 parent 906bc17 commit 66ded5e
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 19 deletions.
20 changes: 10 additions & 10 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.15.3, Apache-2.
maven/mavencentral/com.fasterxml.jackson.core/jackson-databind/2.16.0, Apache-2.0, approved, #11605
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.14.0, Apache-2.0, approved, #5933
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.1, Apache-2.0, approved, #8802
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.3, Apache-2.0, approved, #8802
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.15.3, Apache-2.0, approved, #9179
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.14.0, Apache-2.0, approved, #4699
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.1, Apache-2.0, approved, #7930
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.3, Apache-2.0, approved, #7930
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.15.3, Apache-2.0, approved, #9235
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.1, Apache-2.0, approved, #9236
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.3, Apache-2.0, approved, #9236
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.14.1, Apache-2.0, approved, #5308
maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.15.3, Apache-2.0, approved, #9241
maven/mavencentral/com.fasterxml.jackson.module/jackson-module-jakarta-xmlbind-annotations/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.15.1, Apache-2.0, approved, #7929
maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.15.3, Apache-2.0, approved, #7929
maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.16.0, , restricted, clearlydefined
maven/mavencentral/com.fasterxml.uuid/java-uuid-generator/4.1.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.github.cliftonlabs/json-simple/3.0.2, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -125,10 +125,10 @@ maven/mavencentral/io.netty/netty-tcnative-boringssl-static/2.0.56.Final, Apache
maven/mavencentral/io.netty/netty-tcnative-classes/2.0.56.Final, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.netty/netty-transport-native-unix-common/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.netty/netty-transport/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.32.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.32.0, Apache-2.0, approved, #11684
maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.0.0-alpha, Apache-2.0, approved, #10044
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.32.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.32.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.32.0, Apache-2.0, approved, #11682
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.32.0, Apache-2.0, approved, #11683
maven/mavencentral/io.prometheus/simpleclient/0.16.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.prometheus/simpleclient_common/0.16.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.prometheus/simpleclient_httpserver/0.16.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -310,7 +310,7 @@ maven/mavencentral/org.ow2.asm/asm-tree/9.6, BSD-3-Clause, approved, #10773
maven/mavencentral/org.ow2.asm/asm/9.1, BSD-3-Clause, approved, CQ23029
maven/mavencentral/org.ow2.asm/asm/9.2, BSD-3-Clause, approved, CQ23635
maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776
maven/mavencentral/org.postgresql/postgresql/42.7.0, , restricted, clearlydefined
maven/mavencentral/org.postgresql/postgresql/42.7.0, BSD-2-Clause AND LicenseRef-scancode-free-unknown AND Apache-2.0, restricted, #11681
maven/mavencentral/org.reflections/reflections/0.10.2, Apache-2.0 AND WTFPL, approved, clearlydefined
maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined
maven/mavencentral/org.slf4j/slf4j-api/1.7.22, MIT, approved, CQ11943
Expand All @@ -332,4 +332,4 @@ maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272
maven/mavencentral/org.xmlunit/xmlunit-placeholders/2.9.1, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.yaml/snakeyaml/1.33, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.yaml/snakeyaml/2.0, Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause OR EPL-1.0 OR GPL-2.0-or-later OR LGPL-2.1-or-later), approved, #7275
maven/mavencentral/org.yaml/snakeyaml/2.1, Apache-2.0, approved, #9847
maven/mavencentral/org.yaml/snakeyaml/2.2, Apache-2.0 AND (Apache-2.0 OR BSD-3-Clause OR EPL-1.0 OR GPL-2.0-or-later OR LGPL-2.1-or-later), approved, #10232
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import jakarta.json.Json;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.iam.identitytrust.DidCredentialServiceUrlResolver;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultCredentialServiceClient;
import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator;
Expand Down Expand Up @@ -82,16 +83,18 @@ public class IdentityAndTrustExtension implements ServiceExtension {
private EdcHttpClient httpClient;
@Inject
private TypeTransformerRegistry typeTransformerRegistry;

@Inject
private DidResolverRegistry didResolverRegistry;

private JwtValidator jwtValidator;
private JwtVerifier jwtVerifier;
private PresentationVerifier presentationVerifier;

@Provider
public IdentityService createIdentityService(ServiceExtensionContext context) {
var credentialServiceUrlResolver = new DidCredentialServiceUrlResolver(didResolverRegistry);
return new IdentityAndTrustService(secureTokenService, getOwnDid(context), context.getParticipantId(), getPresentationVerifier(context),
getCredentialServiceclient(context), getJwtValidator(), getJwtVerifier(), registry, clock);
getCredentialServiceClient(context), getJwtValidator(), getJwtVerifier(), registry, clock, credentialServiceUrlResolver);
}

@Provider
Expand All @@ -103,7 +106,7 @@ public JwtValidator getJwtValidator() {
}

@Provider
public CredentialServiceClient getCredentialServiceclient(ServiceExtensionContext context) {
public CredentialServiceClient getCredentialServiceClient(ServiceExtensionContext context) {
if (credentialServiceClient == null) {
credentialServiceClient = new DefaultCredentialServiceClient(httpClient, Json.createBuilderFactory(Map.of()),
typeManager.getMapper(JSON_LD), typeTransformerRegistry, jsonLd, context.getMonitor());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust;

import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.identitytrust.CredentialServiceUrlResolver;
import org.eclipse.edc.spi.result.Result;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* Resolves the URL of the credential service based on the issuer's DID document.
*/
public class DidCredentialServiceUrlResolver implements CredentialServiceUrlResolver {
private static final String CREDENTIAL_SERVICE_TYPE = "CredentialService";
private final DidResolverRegistry didResolverRegistry;

public DidCredentialServiceUrlResolver(DidResolverRegistry didResolverRegistry) {
this.didResolverRegistry = didResolverRegistry;
}

/**
* Resolves the IATP credential service URL from the DID document based on the issuer. The issuer is interpreted as DID
* identifier, and the resolved DID is expected to contain a "CredentialServiceUrl" service endpoint.
*
* @param issuer The issuer of the DID document.
* @return The result containing the service URL if found, or a failure if the DID was not resolvable, or if the required service endpoint wasn't found.
*/
@Override
public Result<String> resolve(String issuer) {
var didDocument = didResolverRegistry.resolve(issuer);
if (didDocument.failed()) {
return didDocument.mapTo();
}
return didDocument.getContent().getService().stream()
.filter(s -> s.getType().equals(CREDENTIAL_SERVICE_TYPE))
.findFirst()
.map(service -> success(service.getServiceEndpoint()))
.orElseGet(() -> failure("No Service endpoint '%s' found on DID Document.".formatted(CREDENTIAL_SERVICE_TYPE)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.edc.iam.identitytrust.validation.rules.IsNotExpired;
import org.eclipse.edc.iam.identitytrust.validation.rules.IsRevoked;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.CredentialServiceUrlResolver;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.TrustedIssuerRegistry;
import org.eclipse.edc.identitytrust.model.Issuer;
Expand All @@ -41,6 +42,7 @@
import java.util.List;
import java.util.Map;

import static com.nimbusds.jwt.JWTClaimNames.ISSUER;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

Expand All @@ -67,6 +69,7 @@ public class IdentityAndTrustService implements IdentityService {
private final JwtVerifier jwtVerifier;
private final TrustedIssuerRegistry trustedIssuerRegistry;
private final Clock clock;
private final CredentialServiceUrlResolver credentialServiceUrlResolver;

/**
* Constructs a new instance of the {@link IdentityAndTrustService}.
Expand All @@ -76,7 +79,7 @@ public class IdentityAndTrustService implements IdentityService {
*/
public IdentityAndTrustService(SecureTokenService secureTokenService, String myOwnDid, String participantId,
PresentationVerifier presentationVerifier, CredentialServiceClient credentialServiceClient,
JwtValidator jwtValidator, JwtVerifier jwtVerifier, TrustedIssuerRegistry trustedIssuerRegistry, Clock clock) {
JwtValidator jwtValidator, JwtVerifier jwtVerifier, TrustedIssuerRegistry trustedIssuerRegistry, Clock clock, CredentialServiceUrlResolver csUrlResolver) {
this.secureTokenService = secureTokenService;
this.myOwnDid = myOwnDid;
this.participantId = participantId;
Expand All @@ -86,6 +89,7 @@ public IdentityAndTrustService(SecureTokenService secureTokenService, String myO
this.jwtVerifier = jwtVerifier;
this.trustedIssuerRegistry = trustedIssuerRegistry;
this.clock = clock;
this.credentialServiceUrlResolver = csUrlResolver;
}

@Override
Expand Down Expand Up @@ -116,14 +120,17 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
// verify and validate incoming SI Token
var issuerResult = jwtVerifier.verify(tokenRepresentation.getToken(), audience)
.compose(v -> jwtValidator.validateToken(tokenRepresentation, audience))
.compose(claimToken -> success(claimToken.getStringClaim("iss")));
.compose(claimToken -> success(claimToken.getStringClaim(ISSUER)));

if (issuerResult.failed()) {
return issuerResult.mapTo();
}

// todo: create SI Token, extract scope strings
var vpResponse = credentialServiceClient.requestPresentation(null, null, null);

// get CS Url, execute VP request
var issuer = issuerResult.getContent(); // the issuer is a DID
var vpResponse = credentialServiceUrlResolver.resolve(issuer)
.compose(url -> credentialServiceClient.requestPresentation(url, null, null));

if (vpResponse.failed()) {
return vpResponse.mapTo();
Expand All @@ -137,7 +144,7 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
// in addition, verify that all VCs are valid
var filters = new ArrayList<>(List.of(
new IsNotExpired(clock),
new HasValidSubjectIds(issuerResult.getContent()),
new HasValidSubjectIds(issuer),
new IsRevoked(null),
new HasValidIssuer(getTrustedIssuerIds())));

Expand All @@ -152,6 +159,7 @@ public Result<ClaimToken> verifyJwtToken(TokenRepresentation tokenRepresentation
return result.map(u -> extractClaimToken(credentials));
}


private ClaimToken extractClaimToken(List<VerifiableCredential> credentials) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.iam.identitytrust;

import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class DidCredentialServiceUrlResolverTest {

public static final String CREDENTIAL_SERVICE_URL = "https://foo.bar/credentialservice";
private final DidResolverRegistry registryMock = mock();
private final DidCredentialServiceUrlResolver resolver = new DidCredentialServiceUrlResolver(registryMock);

@BeforeEach
void setup() {
when(registryMock.resolve(any())).thenReturn(success(createDid().build()));
}

@Test
void resolve() {
assertThat(resolver.resolve("did:web:participant")).isSucceeded().isEqualTo(CREDENTIAL_SERVICE_URL);
}

@Test
void resolve_didNotSupported() {
when(registryMock.resolve(any())).thenReturn(failure("DID method not supported"));
assertThat(resolver.resolve("did:web:participant"))
.isFailed()
.detail().isEqualTo("DID method not supported");
}

@Test
void resolve_didContainsNoServices() {
var did = createDid().build();
did.getService().clear();
when(registryMock.resolve(any())).thenReturn(success(did));
assertThat(resolver.resolve("did:web:participant"))
.isFailed()
.detail().isEqualTo("No Service endpoint 'CredentialService' found on DID Document.");
}

@Test
void resolve_serviceNotFound() {
var did = createDid().build();
did.getService().clear();
did.getService().add(new Service("foo", "bar", "https://foo.bar"));
when(registryMock.resolve(any())).thenReturn(success(did));
assertThat(resolver.resolve("did:web:participant"))
.isFailed()
.detail().isEqualTo("No Service endpoint 'CredentialService' found on DID Document.");
}

private DidDocument.Builder createDid() {
return DidDocument.Builder.newInstance()
.id("test-did")
.service(List.of(new Service("test-service", "CredentialService", CREDENTIAL_SERVICE_URL)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.CredentialServiceUrlResolver;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.TrustedIssuerRegistry;
import org.eclipse.edc.identitytrust.model.CredentialFormat;
Expand Down Expand Up @@ -71,11 +72,13 @@ class IdentityAndTrustServiceTest {
private final JwtValidator jwtValidatorMock = mock();
private final JwtVerifier jwtVerfierMock = mock();
private final TrustedIssuerRegistry trustedIssuerRegistryMock = mock();
private final CredentialServiceUrlResolver resolverMock = mock();
private final IdentityAndTrustService service = new IdentityAndTrustService(mockedSts, EXPECTED_OWN_DID, EXPECTED_PARTICIPANT_ID, mockedVerifier, mockedClient,
jwtValidatorMock, jwtVerfierMock, trustedIssuerRegistryMock, Clock.systemUTC());
jwtValidatorMock, jwtVerfierMock, trustedIssuerRegistryMock, Clock.systemUTC(), resolverMock);

@BeforeEach
void setup() {
when(resolverMock.resolve(any())).thenReturn(success("foobar"));
when(jwtValidatorMock.validateToken(any(), any())).thenReturn(success(ClaimToken.Builder.newInstance().claim("iss", CONSUMER_DID).build()));
when(jwtVerfierMock.verify(any(), any())).thenReturn(success());
}
Expand Down Expand Up @@ -236,5 +239,16 @@ void jwtTokenNotVerified() {
.messages().hasSize(1)
.containsExactly("test-failure");
}

@Test
void cannotResolveCredentialServiceUrl() {
when(resolverMock.resolve(any())).thenReturn(Result.failure("test-failure"));
assertThat(service.verifyJwtToken(createJwt(), "test-audience"))
.isFailed()
.detail()
.isEqualTo("test-failure");

verifyNoInteractions(mockedClient);
}
}
}
Loading

0 comments on commit 66ded5e

Please sign in to comment.