Skip to content

Commit

Permalink
Distributed identity: JWT audience claim (#1520)
Browse files Browse the repository at this point in the history
* Update CHANGELOG.md

* Feature/1176/269 jwt audience claim (#293)

* Use Result type

* Removed public args source

* Introduce Parameter Object for obtainClientCredentials

* Extended debug message

* Added private constructor

* Include error information in failure message

* Use ArgumentsSource

* checkstyle
  • Loading branch information
algattik committed Jun 28, 2022
1 parent 4bd5b29 commit 1648d14
Show file tree
Hide file tree
Showing 37 changed files with 514 additions and 486 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ in the detailed section referring to by linking pull requests or issues.
* Refactored query capabilities for `Asset` (#1459)
* Refactored query capabilities for `ContractDefinition` (#1458)
* Refactored state machine and in-memory persistence (#1511)
* JWT audience claim check with DID (#1520)

#### Removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* Contributors:
* Amadeus - initial API and implementation
* Microsoft Corporation - Simplified token representation
*
*/

Expand Down Expand Up @@ -63,15 +64,7 @@ public Result<TokenRepresentation> generate(@NotNull JwtDecorator... decorators)
} catch (JOSEException e) {
return Result.failure("Failed to sign token");
}
return Result.success(createTokenRepresentation(token.serialize(), claims));
}

private static TokenRepresentation createTokenRepresentation(String token, JWTClaimsSet claimsSet) {
var builder = TokenRepresentation.Builder.newInstance().token(token);
if (claimsSet.getExpirationTime() != null) {
builder.expiresIn(claimsSet.getExpirationTime().getTime());
}
return builder.build();
return Result.success(TokenRepresentation.Builder.newInstance().token(token.serialize()).build());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

Expand All @@ -18,22 +19,21 @@
* Holds configuration information for the IDS API context.
*/
public class IdsApiConfiguration {

private final String contextAlias;
private final String path;
public IdsApiConfiguration(String contextAlias, String path) {

private final String idsWebhookAddress;

public IdsApiConfiguration(String contextAlias, String idsWebhookAddress) {
this.contextAlias = contextAlias;
this.path = path;
this.idsWebhookAddress = idsWebhookAddress;
}

public String getContextAlias() {
return contextAlias;
}
public String getPath() {
return path;

public String getIdsWebhookAddress() {
return idsWebhookAddress;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

package org.eclipse.dataspaceconnector.ids.api.configuration;

import org.eclipse.dataspaceconnector.spi.EdcSetting;
import org.eclipse.dataspaceconnector.spi.WebServer;
import org.eclipse.dataspaceconnector.spi.system.Inject;
import org.eclipse.dataspaceconnector.spi.system.Provides;
Expand All @@ -27,7 +29,10 @@
*/
@Provides(IdsApiConfiguration.class)
public class IdsApiConfigurationExtension implements ServiceExtension {

@EdcSetting
public static final String IDS_WEBHOOK_ADDRESS = "ids.webhook.address";
public static final String DEFAULT_IDS_WEBHOOK_ADDRESS = "http://localhost";

public static final String IDS_API_CONFIG = "web.http.ids";
public static final String IDS_API_CONTEXT_ALIAS = "ids";

Expand Down Expand Up @@ -61,8 +66,11 @@ public void initialize(ServiceExtensionContext context) {
}

monitor.info(format("IDS API will be available at [path=%s], [port=%s].", path, port));

context.registerService(IdsApiConfiguration.class, new IdsApiConfiguration(contextAlias, path));

var webhookPath = path + (path.endsWith("/") ? "data" : "/data");
var idsWebhookAddress = context.getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS) + webhookPath;

context.registerService(IdsApiConfiguration.class, new IdsApiConfiguration(contextAlias, idsWebhookAddress));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

Expand All @@ -26,6 +27,9 @@
import java.util.HashMap;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.dataspaceconnector.ids.api.configuration.IdsApiConfigurationExtension.DEFAULT_IDS_API_PATH;
import static org.eclipse.dataspaceconnector.ids.api.configuration.IdsApiConfigurationExtension.DEFAULT_IDS_WEBHOOK_ADDRESS;
import static org.eclipse.dataspaceconnector.ids.api.configuration.IdsApiConfigurationExtension.IDS_WEBHOOK_ADDRESS;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
Expand All @@ -35,23 +39,25 @@
import static org.mockito.Mockito.when;

public class IdsApiConfigurationExtensionTest {

private WebServer webServer;
private ServiceExtensionContext context;
private IdsApiConfigurationExtension extension;

private ArgumentCaptor<String> contextNameCaptor;
private ArgumentCaptor<Integer> portCaptor;
private ArgumentCaptor<String> pathCaptor;
private ArgumentCaptor<IdsApiConfiguration> apiConfigCaptor;

@BeforeEach
void setUp() {
var monitor = new ConsoleMonitor();
webServer = mock(WebServer.class);
context = mock(ServiceExtensionContext.class);
when(context.getMonitor()).thenReturn(monitor);

when(context.getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS))
.thenReturn(DEFAULT_IDS_WEBHOOK_ADDRESS);

extension = new IdsApiConfigurationExtension();
contextNameCaptor = ArgumentCaptor.forClass(String.class);
portCaptor = ArgumentCaptor.forClass(Integer.class);
Expand All @@ -76,11 +82,12 @@ void initializeWithDefault() {
verify(context, times(1)).getMonitor();
verify(context, times(1)).getConfig(IdsApiConfigurationExtension.IDS_API_CONFIG);
verify(context, times(1)).registerService(eq(IdsApiConfiguration.class), apiConfigCaptor.capture());
verify(context, times(1)).getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS);
verifyNoMoreInteractions(context);

var idsApiConfig = apiConfigCaptor.getValue();
assertThat(idsApiConfig.getContextAlias()).isEqualTo("ids");
assertThat(idsApiConfig.getPath()).isEqualTo(IdsApiConfigurationExtension.DEFAULT_IDS_API_PATH);
assertThat(idsApiConfig.getIdsWebhookAddress()).isEqualTo(DEFAULT_IDS_WEBHOOK_ADDRESS + DEFAULT_IDS_API_PATH + "/data");
}

@Test
Expand All @@ -89,22 +96,27 @@ void initializeWithCustomSettings() {
var apiConfig = new HashMap<String, String>();
apiConfig.put("port", String.valueOf(8765));
apiConfig.put("path", path);

var config = ConfigFactory.fromMap(apiConfig);
when(context.getConfig(IdsApiConfigurationExtension.IDS_API_CONFIG)).thenReturn(config);


var address = "http://somehost:1234";
when(context.getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS))
.thenReturn(address);

extension.initialize(context);

verifyNoInteractions(webServer);

verify(context, times(1)).getMonitor();
verify(context, times(1)).getConfig(IdsApiConfigurationExtension.IDS_API_CONFIG);
verify(context, times(1)).registerService(eq(IdsApiConfiguration.class), apiConfigCaptor.capture());
verify(context, times(1)).getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS);
verifyNoMoreInteractions(context);

var idsApiConfig = apiConfigCaptor.getValue();
assertThat(idsApiConfig.getContextAlias()).isEqualTo("ids");
assertThat(idsApiConfig.getPath()).isEqualTo(path);
assertThat(idsApiConfig.getIdsWebhookAddress()).isEqualTo(address + path + "/data");
}

@Test
Expand All @@ -122,11 +134,12 @@ void initializeWithOnlyPortSet() {
verify(context, times(1)).getMonitor();
verify(context, times(1)).getConfig(IdsApiConfigurationExtension.IDS_API_CONFIG);
verify(context, times(1)).registerService(eq(IdsApiConfiguration.class), apiConfigCaptor.capture());
verify(context, times(1)).getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS);
verifyNoMoreInteractions(context);

var idsApiConfig = apiConfigCaptor.getValue();
assertThat(idsApiConfig.getContextAlias()).isEqualTo("ids");
assertThat(idsApiConfig.getPath()).isEqualTo(IdsApiConfigurationExtension.DEFAULT_IDS_API_PATH);
assertThat(idsApiConfig.getIdsWebhookAddress()).isEqualTo(DEFAULT_IDS_WEBHOOK_ADDRESS + DEFAULT_IDS_API_PATH + "/data");
}

@Test
Expand All @@ -145,11 +158,12 @@ void initializeWithOnlyPathSet() {
verify(context, times(1)).getMonitor();
verify(context, times(1)).getConfig(IdsApiConfigurationExtension.IDS_API_CONFIG);
verify(context, times(1)).registerService(eq(IdsApiConfiguration.class), apiConfigCaptor.capture());
verify(context, times(1)).getSetting(IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS);
verifyNoMoreInteractions(context);

var idsApiConfig = apiConfigCaptor.getValue();
assertThat(idsApiConfig.getContextAlias()).isEqualTo("ids");
assertThat(idsApiConfig.getPath()).isEqualTo(path);
assertThat(idsApiConfig.getIdsWebhookAddress()).isEqualTo(DEFAULT_IDS_WEBHOOK_ADDRESS + path + "/data");
}

private void setJettyService() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 Fraunhofer Institute for Software and Systems Engineering
* Copyright (c) 2021 - 2022 Fraunhofer Institute for Software and Systems Engineering
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
Expand All @@ -9,6 +9,7 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

Expand Down Expand Up @@ -47,10 +48,6 @@ public class IdsMultipartDispatcherServiceExtension implements ServiceExtension
public static final String EDC_IDS_ID = "edc.ids.id";
public static final String DEFAULT_EDC_IDS_ID = "urn:connector:edc";

@EdcSetting
public static final String IDS_WEBHOOK_ADDRESS = "ids.webhook.address";
public static final String DEFAULT_IDS_WEBHOOK_ADDRESS = "http://localhost";

@Inject
private Monitor monitor;
@Inject
Expand Down Expand Up @@ -79,10 +76,7 @@ public void initialize(ServiceExtensionContext context) {
// once https://github.com/eclipse-dataspaceconnector/DataSpaceConnector/issues/236 is done
var objectMapper = objectMapperFactory.getObjectMapper();

var idsWebhookAddress = getSetting(context, IDS_WEBHOOK_ADDRESS, DEFAULT_IDS_WEBHOOK_ADDRESS);
var idsApiPath = idsApiConfiguration.getPath();
var webhookPath = idsApiPath + (idsApiPath.endsWith("/") ? "data" : "/data");
idsWebhookAddress = idsWebhookAddress + webhookPath;
String idsWebhookAddress = idsApiConfiguration.getIdsWebhookAddress();

var multipartDispatcher = new IdsMultipartRemoteMessageDispatcher();
multipartDispatcher.register(new MultipartArtifactRequestSender(connectorId, httpClient, objectMapper, monitor, vault, identityService, transformerRegistry, idsWebhookAddress));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2021 Fraunhofer Institute for Software and Systems Engineering
* Copyright (c) 2020 - 2022 Fraunhofer Institute for Software and Systems Engineering
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
Expand All @@ -9,6 +9,7 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

Expand All @@ -35,6 +36,7 @@
import org.eclipse.dataspaceconnector.ids.spi.transform.IdsTransformerRegistry;
import org.eclipse.dataspaceconnector.spi.EdcException;
import org.eclipse.dataspaceconnector.spi.iam.IdentityService;
import org.eclipse.dataspaceconnector.spi.iam.TokenParameters;
import org.eclipse.dataspaceconnector.spi.message.MessageContext;
import org.eclipse.dataspaceconnector.spi.monitor.Monitor;
import org.eclipse.dataspaceconnector.spi.types.domain.message.RemoteMessage;
Expand Down Expand Up @@ -99,8 +101,14 @@ private static URI createConnectorIdUri(String connectorId) {
*/
@Override
public CompletableFuture<R> send(M request, MessageContext context) {
var remoteConnectorAddress = retrieveRemoteConnectorAddress(request);

// Get Dynamic Attribute Token
var tokenResult = identityService.obtainClientCredentials(TOKEN_SCOPE);
var tokenParameters = TokenParameters.Builder.newInstance()
.scope(TOKEN_SCOPE)
.audience(remoteConnectorAddress)
.build();
var tokenResult = identityService.obtainClientCredentials(tokenParameters);
if (tokenResult.failed()) {
String message = "Failed to obtain token: " + String.join(",", tokenResult.getFailureMessages());
monitor.severe(message);
Expand All @@ -114,8 +122,7 @@ public CompletableFuture<R> send(M request, MessageContext context) {


// Get recipient address
var connectorAddress = retrieveRemoteConnectorAddress(request);
var requestUrl = HttpUrl.parse(connectorAddress);
var requestUrl = HttpUrl.parse(remoteConnectorAddress);
if (requestUrl == null) {
return failedFuture(new IllegalArgumentException("Connector address not specified"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* Contributors:
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

Expand Down Expand Up @@ -78,7 +79,7 @@ protected void before(EdcExtension extension) {
var claimToken = ClaimToken.Builder.newInstance().claim("key", "value").build();
identityService = mock(IdentityService.class);
when(identityService.obtainClientCredentials(any())).thenReturn(Result.success(tokenResult));
when(identityService.verifyJwtToken(any(TokenRepresentation.class))).thenReturn(Result.success(claimToken));
when(identityService.verifyJwtToken(any(), any())).thenReturn(Result.success(claimToken));

extension.registerSystemExtension(ServiceExtension.class,
new IdsApiMultipartDispatcherV1IntegrationTestServiceExtension(ASSETS, identityService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.eclipse.dataspaceconnector.ids.api.multipart.dispatcher.sender;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.javafaker.Faker;
import de.fraunhofer.iais.eis.DynamicAttributeToken;
import de.fraunhofer.iais.eis.Message;
import okhttp3.OkHttpClient;
Expand All @@ -28,23 +29,26 @@
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class IdsMultipartSenderTest {
private static final Faker FAKER = new Faker();
private final IdentityService identityService = mock(IdentityService.class);
private final String remoteAddress = FAKER.internet().url();

@Test
void should_fail_if_token_retrieval_fails() {
when(identityService.obtainClientCredentials("idsc:IDS_CONNECTOR_ATTRIBUTES_ALL")).thenReturn(Result.failure("error"));
when(identityService.obtainClientCredentials(any())).thenReturn(Result.failure("error"));
var sender = new TestIdsMultipartSender("any", mock(OkHttpClient.class), new ObjectMapper(), mock(Monitor.class), identityService, mock(IdsTransformerRegistry.class));

var result = sender.send(new TestRemoteMessage(), () -> "any");

assertThat(result).failsWithin(1, TimeUnit.SECONDS);
}

private static class TestIdsMultipartSender extends IdsMultipartSender<TestRemoteMessage, Object> {
private class TestIdsMultipartSender extends IdsMultipartSender<TestRemoteMessage, Object> {

protected TestIdsMultipartSender(String connectorId, OkHttpClient httpClient, ObjectMapper objectMapper,
Monitor monitor, IdentityService identityService, IdsTransformerRegistry transformerRegistry) {
Expand All @@ -58,16 +62,16 @@ public Class<TestRemoteMessage> messageType() {

@Override
protected String retrieveRemoteConnectorAddress(TestRemoteMessage request) {
return null;
return remoteAddress;
}

@Override
protected Message buildMessageHeader(TestRemoteMessage request, DynamicAttributeToken token) throws Exception {
protected Message buildMessageHeader(TestRemoteMessage request, DynamicAttributeToken token) {
return null;
}

@Override
protected Object getResponseContent(IdsMultipartParts parts) throws Exception {
protected Object getResponseContent(IdsMultipartParts parts) {
return null;
}
}
Expand Down
Loading

0 comments on commit 1648d14

Please sign in to comment.