Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SSI): implements the MIW client #489

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.eclipse.tractusx.edc.iam.ssi.spi.SsiCredentialClient;
import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry;

@Provides({IdentityService.class, SsiValidationRuleRegistry.class})
@Provides({ IdentityService.class, SsiValidationRuleRegistry.class })
@Extension(SsiIdentityServiceExtension.EXTENSION_NAME)
public class SsiIdentityServiceExtension implements ServiceExtension {

Expand All @@ -46,5 +46,4 @@ public void initialize(ServiceExtensionContext context) {

context.registerService(IdentityService.class, identityService);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ void verifyJwtToken_success() {
@Test
void verifyJwtToken_failed() {
var token = TokenRepresentation.Builder.newInstance().token("test").build();
var claim = ClaimToken.Builder.newInstance().build();

when(tokenValidationService.validate(token)).thenReturn(Result.failure("fail"));

Expand Down
10 changes: 9 additions & 1 deletion edc-extensions/ssi/ssi-miw-credential-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,12 @@ just call the MIW for checking that the token and the VP claim inside are correc

For obtaining a `JWT` token also it reaches the MIW, that will create a token with the `VP` claim inside.

The MIW interaction in this first implementation is still WIP, since the MIW interface it's not stable or complete yet.
## Configuration

| Key | Required | Example | Description |
|-----------------------------------------|----------|----------------|-----------------------------------|
| tx.ssi.miw.url | X | | MIW URL |
| tx.ssi.miw.authority.id | X | | BPN number of the authority |
| tx.ssi.oauth.token.url | X | | Token URL (Keycloak) |
| tx.ssi.oauth.client.id | X | | Client id |
| tx.ssi.oauth.client.secret.alias | X | | Vault alias for the client secret |
5 changes: 5 additions & 0 deletions edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ dependencies {
implementation(project(":spi:ssi-spi"))
implementation(libs.edc.spi.core)
implementation(libs.edc.spi.http)
implementation(libs.edc.spi.jsonld)
implementation(libs.edc.auth.oauth2.client)
implementation(libs.edc.spi.jwt)
implementation(libs.jakartaJson)
implementation(libs.nimbus.jwt)

testImplementation(testFixtures(libs.edc.junit))
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient;
import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClientImpl;
import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2Client;


@Extension(SsiMiwApiClientExtension.EXTENSION_NAME)
Expand All @@ -35,6 +36,12 @@ public class SsiMiwApiClientExtension implements ServiceExtension {
@Setting(value = "MIW API base url")
public static final String MIW_BASE_URL = "tx.ssi.miw.url";

@Setting(value = "MIW Authority ID")
public static final String MIW_AUTHORITY_ID = "tx.ssi.miw.authority.id";

@Inject
private MiwOauth2Client oauth2Client;

@Inject
private EdcHttpClient httpClient;

Expand All @@ -52,8 +59,10 @@ public String name() {
@Provider
public MiwApiClient apiClient(ServiceExtensionContext context) {
var baseUrl = context.getConfig().getString(MIW_BASE_URL);
var authorityId = context.getConfig().getString(MIW_AUTHORITY_ID);


return new MiwApiClientImpl(httpClient, baseUrl, typeManager.getMapper(), monitor);
return new MiwApiClientImpl(httpClient, baseUrl, oauth2Client, context.getParticipantId(), authorityId, typeManager.getMapper(), monitor);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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.tractusx.edc.iam.ssi.miw;

import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2Client;
import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2ClientConfiguration;
import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2ClientImpl;

import java.util.Objects;


@Extension(SsiMiwOauth2ClientExtension.EXTENSION_NAME)
public class SsiMiwOauth2ClientExtension implements ServiceExtension {

public static final String EXTENSION_NAME = "SSI MIW OAuth2 Client";

@Setting(value = "OAuth2 endpoint for requesting a token")
public static final String TOKEN_URL = "tx.ssi.oauth.token.url";


@Setting(value = "OAuth2 client id")
public static final String CLIENT_ID = "tx.ssi.oauth.client.id";

@Setting(value = "Vault alias of OAuth2 client secret")
public static final String CLIENT_SECRET_ALIAS = "tx.ssi.oauth.client.secret.alias";

@Inject
private Oauth2Client oauth2Client;

@Inject
private Vault vault;

@Override
public String name() {
return EXTENSION_NAME;
}

@Provider
public MiwOauth2Client oauth2Client(ServiceExtensionContext context) {
return new MiwOauth2ClientImpl(oauth2Client, createConfiguration(context));
}

private MiwOauth2ClientConfiguration createConfiguration(ServiceExtensionContext context) {
var tokenUrl = context.getConfig().getString(TOKEN_URL);
var clientId = context.getConfig().getString(CLIENT_ID);
var clientSecretAlias = context.getConfig().getString(CLIENT_SECRET_ALIAS);
var clientSecret = vault.resolveSecret(clientSecretAlias);
Objects.requireNonNull(clientSecret, "Client secret not found in the vault");

return MiwOauth2ClientConfiguration.Builder.newInstance()
.tokenUrl(tokenUrl)
.clientId(clientId)
.clientSecret(clientSecret)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
public interface MiwApiClient {
String VP = "vp";

Result<List<Map<String, Object>>> getCredentials(Set<String> types, String holderIdentifier);
Result<List<Map<String, Object>>> getCredentials(Set<String> types);

Result<Map<String, Object>> createPresentation(List<Map<String, Object>> credentials, String holderIdentifier);
Result<Map<String, Object>> createPresentation(List<Map<String, Object>> credentials, String audience);

Result<Void> verifyPresentation(String jwtPresentation);
Result<Void> verifyPresentation(String jwtPresentation, String audience);

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,69 +22,125 @@
import okhttp3.RequestBody;
import okhttp3.Response;
import org.eclipse.edc.spi.http.EdcHttpClient;
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2Client;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static java.lang.String.format;

public class MiwApiClientImpl implements MiwApiClient {

public static final MediaType TYPE_JSON = MediaType.parse("application/json");
private static final String CREDENTIAL_PATH = "/api/credentials";
private static final String PRESENTATIONS_PATH = "/api/presentations";

public static final String CREDENTIAL_PATH = "/api/credentials";
public static final String PRESENTATIONS_PATH = "/api/presentations";
public static final String PRESENTATIONS_VALIDATION_PATH = "/api/presentations/validation";
public static final String HOLDER_IDENTIFIER = "holderIdentifier";

public static final String ISSUER_IDENTIFIER = "issuerIdentifier";
public static final String VERIFIABLE_CREDENTIALS = "verifiableCredentials";
public static final String VP_FIELD = "vp";
public static final String CONTENT_FIELD = "content";
private static final String PRESENTATIONS_QUERY_PARAMS = "?asJwt=true&audience=%s";
private final EdcHttpClient httpClient;
private final String baseUrl;
private final MiwOauth2Client oauth2Client;
private final ObjectMapper mapper;
private final Monitor monitor;

public MiwApiClientImpl(EdcHttpClient httpClient, String baseUrl, ObjectMapper mapper, Monitor monitor) {
private final String authorityId;

private final String participantId;

public MiwApiClientImpl(EdcHttpClient httpClient, String baseUrl, MiwOauth2Client oauth2Client, String participantId, String authorityId, ObjectMapper mapper, Monitor monitor) {
this.httpClient = httpClient;
this.baseUrl = baseUrl;
this.oauth2Client = oauth2Client;
this.participantId = participantId;
this.authorityId = authorityId;
this.mapper = mapper;
this.monitor = monitor;
}

@Override
public Result<List<Map<String, Object>>> getCredentials(Set<String> types, String holderIdentifier) {
public Result<List<Map<String, Object>>> getCredentials(Set<String> types) {

var params = new ArrayList<String>();
params.add(format("holderIdentifier=%s", holderIdentifier));
params.add(format("%s=%s", ISSUER_IDENTIFIER, authorityId));

if (!types.isEmpty()) {
params.add(format("type=%s", String.join(",", types)));
}

var queryParams = "?" + String.join("&", params);
var url = baseUrl + CREDENTIAL_PATH + queryParams;
var request = new Request.Builder().get().url(url).build();

return executeRequest(request, new TypeReference<>() {
});
return baseRequestWithToken().map(builder -> builder.get().url(url).build())
.compose(request -> executeRequest(request, new TypeReference<Map<String, Object>>() {
}))
.compose(this::handleGetCredentialResponse);
}

@Override
public Result<Map<String, Object>> createPresentation(List<Map<String, Object>> credentials, String holderIdentifier) {
public Result<Map<String, Object>> createPresentation(List<Map<String, Object>> credentials, String audience) {
try {
var body = Map.of("holderIdentifier", holderIdentifier, "verifiableCredentials", credentials);
var url = baseUrl + PRESENTATIONS_PATH + "?asJwt=true";
var body = Map.of(HOLDER_IDENTIFIER, participantId, VERIFIABLE_CREDENTIALS, credentials);
var url = baseUrl + PRESENTATIONS_PATH + format(PRESENTATIONS_QUERY_PARAMS, audience);
var requestBody = RequestBody.create(mapper.writeValueAsString(body), TYPE_JSON);
var request = new Request.Builder().post(requestBody).url(url).build();

return executeRequest(request, new TypeReference<>() {
});
return baseRequestWithToken().map(builder -> builder.post(requestBody).url(url).build())
.compose(request -> executeRequest(request, new TypeReference<>() {
}));
} catch (JsonProcessingException e) {
return Result.failure(e.getMessage());
}
}

@Override
public Result<Void> verifyPresentation(String jwtPresentation, String audience) {
try {
var body = Map.of(VP_FIELD, jwtPresentation);
var url = baseUrl + PRESENTATIONS_VALIDATION_PATH + format(PRESENTATIONS_QUERY_PARAMS, audience);
var requestBody = RequestBody.create(mapper.writeValueAsString(body), TYPE_JSON);

return baseRequestWithToken().map(builder -> builder.post(requestBody).url(url).build())
.compose(request -> executeRequest(request, new TypeReference<Map<String, Object>>() {
}))
.compose(this::handleVerifyResult);
} catch (JsonProcessingException e) {
return Result.failure(e.getMessage());
}
}

private Result<List<Map<String, Object>>> handleGetCredentialResponse(Map<String, Object> response) {
var content = response.get(CONTENT_FIELD);

if (content == null) {
return Result.failure("Missing content field in the credentials response");
}
return Result.success((List<Map<String, Object>>) content);
}

private Result<Void> handleVerifyResult(Map<String, Object> response) {
var valid = Optional.ofNullable(response.get("valid"))
.map(Boolean.TRUE::equals)
.orElse(false);

if (valid) {
return Result.success();
} else {
return Result.failure(format("Verification failed with response: %s", response));
}
}

private <R> Result<R> executeRequest(Request request, TypeReference<R> typeReference) {
try (var response = httpClient.execute(request)) {
return handleResponse(response, typeReference);
Expand All @@ -93,11 +149,6 @@ private <R> Result<R> executeRequest(Request request, TypeReference<R> typeRefer
}
}

@Override
public Result<Void> verifyPresentation(String jwtPresentation) {
return Result.success();
}

private <R> Result<R> handleResponse(Response response, TypeReference<R> tr) {
if (response.isSuccessful()) {
return handleSuccess(response, tr);
Expand All @@ -122,4 +173,14 @@ private <R> Result<R> handleError(Response response) {
return Result.failure(msg);
}


private Result<Request.Builder> baseRequestWithToken() {
return oauth2Client.obtainRequestToken()
.map(this::baseRequestWithToken);
}

private Request.Builder baseRequestWithToken(TokenRepresentation tokenRepresentation) {
return new Request.Builder()
.addHeader("Authorization", format("Bearer %s", tokenRepresentation.getToken()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
import java.text.ParseException;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE;
import static org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient.VP;

public class SsiMiwCredentialClient implements SsiCredentialClient {
Expand All @@ -39,12 +39,11 @@ public SsiMiwCredentialClient(MiwApiClient apiClient) {

@Override
public Result<TokenRepresentation> obtainClientCredentials(TokenParameters parameters) {
// TODO will need to take from the TokenParameters which are the credentials needed, REF https://github.com/eclipse-edc/Connector/pull/3150
return apiClient.getCredentials(Set.of(), parameters.getAudience())
return apiClient.getCredentials(parameters.getAdditional().keySet())
.compose(credentials -> createPresentation(credentials, parameters))
.compose(this::createToken);
}

@Override
public Result<ClaimToken> validate(TokenRepresentation tokenRepresentation) {
return extractClaims(tokenRepresentation)
Expand All @@ -69,8 +68,10 @@ private Result<Map<String, Object>> createPresentation(List<Map<String, Object>>
}

private Result<ClaimToken> validatePresentation(ClaimToken claimToken, TokenRepresentation tokenRepresentation) {
return apiClient.verifyPresentation(tokenRepresentation.getToken())
.compose(v -> Result.success(claimToken));
return claimToken.getListClaim(AUDIENCE).stream().map(String.class::cast).findFirst()
.map(audience -> apiClient.verifyPresentation(tokenRepresentation.getToken(), audience)
.compose(v -> Result.success(claimToken)))
.orElseGet(() -> Result.failure("Required audience (aud) claim is missing in token"));
}

private Result<ClaimToken> extractClaims(TokenRepresentation tokenRepresentation) {
Expand Down
Loading