From 71408db5875e755ada52d24b5a755918322e8501 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 11 Mar 2024 09:25:25 +0100 Subject: [PATCH] feat: add DataPlane Token Refresh API --- .../edc-dataplane-base/build.gradle.kts | 3 +- .../token-refresh-api/build.gradle.kts | 36 +++ .../api/TokenRefreshApiExtension.java | 53 ++++ .../tokenrefresh/api/v1/TokenRefreshApi.java | 58 ++++ .../api/v1/TokenRefreshApiController.java | 59 ++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 20 ++ .../api/v1/TokenRefreshApiControllerTest.java | 146 ++++++++++ ...DataPlaneTokenRefreshServiceExtension.java | 21 +- .../DataPlaneTokenRefreshServiceImpl.java | 18 +- ...eTokenRefreshServiceImplComponentTest.java | 23 ++ .../build.gradle.kts | 4 +- .../DataPlaneTokenRefreshEndToEndTest.java | 253 +++++++++++++++++- .../tokenrefresh/e2e/RuntimeConfig.java | 53 +++- gradle/libs.versions.toml | 1 + settings.gradle.kts | 1 + 15 files changed, 726 insertions(+), 23 deletions(-) create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/build.gradle.kts create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/TokenRefreshApiExtension.java create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApi.java create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiController.java create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiControllerTest.java diff --git a/edc-dataplane/edc-dataplane-base/build.gradle.kts b/edc-dataplane/edc-dataplane-base/build.gradle.kts index 85af80bb9..a6221ec65 100644 --- a/edc-dataplane/edc-dataplane-base/build.gradle.kts +++ b/edc-dataplane/edc-dataplane-base/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { runtimeOnly(project(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-provider-api")) runtimeOnly(project(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-provider-core")) runtimeOnly(project(":edc-extensions:dataplane:dataplane-token-refresh:token-refresh-core")) + runtimeOnly(project(":edc-extensions:dataplane:dataplane-token-refresh:token-refresh-api")) runtimeOnly(libs.edc.jsonld) // needed by the DataPlaneSignalingApi runtimeOnly(libs.edc.identity.core.did) // for the DID Public Key Resolver @@ -43,7 +44,7 @@ dependencies { runtimeOnly(libs.edc.dpf.api.control) runtimeOnly(libs.edc.dpf.api.signaling) - runtimeOnly(libs.edc.dpf.api.public) + runtimeOnly(libs.edc.dpf.api.public.v2) runtimeOnly(libs.edc.core.connector) runtimeOnly(libs.edc.boot) diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/build.gradle.kts b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/build.gradle.kts new file mode 100644 index 000000000..0439436fa --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + `java-library` + `maven-publish` + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +dependencies { + api(project(":spi:tokenrefresh-spi")) + implementation(libs.edc.spi.core) + implementation(libs.edc.spi.web) + implementation(libs.jakarta.rsApi) + + testImplementation(libs.edc.junit) + testImplementation(libs.restAssured) + testImplementation(testFixtures(libs.edc.core.jersey)) +} + diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/TokenRefreshApiExtension.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/TokenRefreshApiExtension.java new file mode 100644 index 000000000..3fb4a88b6 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/TokenRefreshApiExtension.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.tractusx.edc.dataplane.tokenrefresh.api; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.v1.TokenRefreshApiController; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService; + +import static org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.TokenRefreshApiExtension.NAME; + +@Extension(value = NAME) +public class TokenRefreshApiExtension implements ServiceExtension { + + public static final String NAME = "DataPlane Token Refresh API Extension"; + private static final String PUBLIC_API_CONTEXT = "public"; + @Inject + private DataPlaneTokenRefreshService refreshService; + + @Inject + private WebService webService; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var controller = new TokenRefreshApiController(refreshService); + webService.registerResource(PUBLIC_API_CONTEXT, controller); + } +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApi.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApi.java new file mode 100644 index 000000000..f0a85a0ed --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApi.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.v1; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.eclipse.edc.web.spi.ApiErrorDetail; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse; + +@SecurityScheme(name = "Authentication", + description = "Self-Issued ID token containing an access_token", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT") +@OpenAPIDefinition(info = @Info(description = "With this API clients can refresh their access token for a provider's HTTP data plane using an authentication token and a refresh token.", title = "Token Refresh API")) +@Tag(name = "Token Refresh API") +public interface TokenRefreshApi { + + @Operation(description = "Resolves all groups for a particular BPN", + parameters = { @Parameter(name = "grant_type", description = "The grant type. Must be \"refresh_token\""), + @Parameter(name = "refresh_token", description = "The refresh token") }, + responses = { + @ApiResponse(responseCode = "200", description = "The access token and refresh token were updated. Expiry should be " + + "interpreted as starting from the time of message reception, allowing for some leeway.", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), + @ApiResponse(responseCode = "401", description = "The token could not be refreshed due to an authentication error, either the refresh token or the Authorization header were invalid.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))), + @ApiResponse(responseCode = "400", description = "Request body was malformed, query parameters were missing, etc.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))) + }) + TokenResponse refreshToken(String grantType, String refreshToken, String bearerToken); +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiController.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiController.java new file mode 100644 index 000000000..c2e08290f --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiController.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.v1; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.eclipse.edc.web.spi.exception.InvalidRequestException; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse; + +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; + +@Produces({ MediaType.APPLICATION_JSON }) +@Path("/token") +public class TokenRefreshApiController implements TokenRefreshApi { + private static final String REFRESH_TOKEN_GRANT = "refresh_token"; + private final DataPlaneTokenRefreshService tokenRefreshService; + + public TokenRefreshApiController(DataPlaneTokenRefreshService tokenRefreshService) { + this.tokenRefreshService = tokenRefreshService; + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Override + public TokenResponse refreshToken(@QueryParam("grant_type") String grantType, + @QueryParam("refresh_token") String refreshToken, + @HeaderParam(AUTHORIZATION) String bearerToken) { + if (!REFRESH_TOKEN_GRANT.equals(grantType)) { + throw new InvalidRequestException("Grant type MUST be '%s' but was '%s'".formatted(REFRESH_TOKEN_GRANT, grantType)); + } + + return tokenRefreshService.refreshToken(refreshToken, bearerToken) + .orElseThrow(f -> new AuthenticationFailedException(f.getFailureDetail())); + } +} diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..db16e33f7 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,20 @@ +################################################################################# +# Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################# + +org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.TokenRefreshApiExtension \ No newline at end of file diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiControllerTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiControllerTest.java new file mode 100644 index 000000000..bb2b805c2 --- /dev/null +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-api/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/api/v1/TokenRefreshApiControllerTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.tractusx.edc.dataplane.tokenrefresh.api.v1; + +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TokenRefreshApiControllerTest extends RestControllerTestBase { + + private final DataPlaneTokenRefreshService refreshService = mock(); + + @DisplayName("Expect HTTP 400 when no Authorization header is present") + @Test + void refresh_noAuthHeader_expect401() { + baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", "foo-token") + /* missing: .header(AUTHORIZATION, "auth-token") */ + .contentType(ContentType.URLENC) + .then() + .statusCode(401); + } + + @DisplayName("Expect HTTP 200 when the token was successfully refreshed") + @Test + void refresh_expect200() { + when(refreshService.refreshToken(any(), any())).thenReturn(Result.success(new TokenResponse("new-accesstoken", "new-refreshtoken", 3000L, "bearer"))); + baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", "foo-token") + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(200) + .body(Matchers.isA(TokenResponse.class)); + } + + @DisplayName("Expect HTTP 400 when an invalid grant type was provided") + @ParameterizedTest(name = "Invalid grant_type: {0}") + @ValueSource(strings = { "REFRESH_TOKEN", "refreshToken", "invalid_grant", "client_credentials", "" }) + @NullSource + void refresh_invalidGrantType_expect400(String grant) { + baseRequest() + .queryParam("grant_type", grant) + .queryParam("refresh_token", "foo-token") + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(400); + } + + @DisplayName("Expect HTTP 400 when an invalid refresh token was provided") + @ParameterizedTest(name = "Invalid refresh_token: {0}") + @NullSource + @EmptySource + void refresh_invalidRefreshToken_expect400(String refreshToken) { + baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(400); + } + + @DisplayName("Expect HTTP 400 when one of the query params was missing") + @Test + void refresh_queryParamsMissing() { + baseRequest() + .queryParam("grant_type", "refresh_token") + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(400); + + baseRequest() + .queryParam("refresh_token", "foo-token") + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(400); + } + + @DisplayName("Expect HTTP 401 if the auth header or refresh token are invalid") + @Test + void refresh_tokenInvalid_expect401() { + when(refreshService.refreshToken(any(), any())).thenReturn(Result.failure("Invalid auth token")); + + baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", "foo-token") + .header(AUTHORIZATION, "auth-token") + .contentType(ContentType.URLENC) + .then() + .statusCode(401) + .body(containsString("Invalid auth token")); + } + + @Override + protected Object controller() { + return new TokenRefreshApiController(refreshService); + } + + private RequestSpecification baseRequest() { + return given() + .baseUri("http://localhost:" + port) + .basePath("/token") + .when(); + } + +} \ No newline at end of file diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java index 0acc3a818..8f03fc2f3 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceExtension.java @@ -30,6 +30,7 @@ import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.token.JwtGenerationService; import org.eclipse.edc.token.spi.TokenValidationService; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.DataPlaneTokenRefreshService; import org.jetbrains.annotations.NotNull; import java.security.PrivateKey; @@ -50,15 +51,31 @@ public class DataPlaneTokenRefreshServiceExtension implements ServiceExtension { @Inject private PrivateKeyResolver privateKeyResolver; + private DataPlaneTokenRefreshServiceImpl tokenRefreshService; @Override public String name() { return NAME; } + // exposes the service as access token service @Provider - public DataPlaneAccessTokenService createRefreshAccessTokenService(ServiceExtensionContext context) { - return new DataPlaneTokenRefreshServiceImpl(tokenValidationService, didPkResolver, accessTokenDataStore, new JwtGenerationService(), getPrivateKeySupplier(context), context.getMonitor(), "foo.bar"); + public DataPlaneAccessTokenService createAccessTokenService(ServiceExtensionContext context) { + return getTokenRefreshService(context); + } + + // exposes the service as pure refresh service + @Provider + public DataPlaneTokenRefreshService createRefreshTokenService(ServiceExtensionContext context) { + return getTokenRefreshService(context); + } + + @NotNull + private DataPlaneTokenRefreshServiceImpl getTokenRefreshService(ServiceExtensionContext context) { + if (tokenRefreshService == null) { + tokenRefreshService = new DataPlaneTokenRefreshServiceImpl(tokenValidationService, didPkResolver, accessTokenDataStore, new JwtGenerationService(), getPrivateKeySupplier(context), context.getMonitor(), "foo.bar"); + } + return tokenRefreshService; } @NotNull diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java index ea47788c9..7e6fdc0f1 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/main/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImpl.java @@ -50,6 +50,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE; + /** * This implementation of the {@link DataPlaneTokenRefreshService} validates an incoming authentication token. */ @@ -58,7 +60,10 @@ public class DataPlaneTokenRefreshServiceImpl implements DataPlaneTokenRefreshSe public static final String TOKEN_ID_CLAIM = "jti"; public static final String REFRESH_TOKEN_PROPERTY = "refreshToken"; private static final Long DEFAULT_EXPIRY_IN_SECONDS = 60 * 5L; - private final List tokenValidationRules = List.of(new IssuerEqualsSubjectRule(), new ClaimIsPresentRule(ACCESS_TOKEN_CLAIM), new ClaimIsPresentRule(TOKEN_ID_CLAIM)); + private final List authenticationTokenValidationRules = List.of(new IssuerEqualsSubjectRule(), + new ClaimIsPresentRule(AUDIENCE), // we don't check the contents, only it is present + new ClaimIsPresentRule(ACCESS_TOKEN_CLAIM), + new ClaimIsPresentRule(TOKEN_ID_CLAIM)); private final TokenValidationService tokenValidationService; private final DidPublicKeyResolver publicKeyResolver; private final AccessTokenDataStore accessTokenDataStore; @@ -71,7 +76,10 @@ public class DataPlaneTokenRefreshServiceImpl implements DataPlaneTokenRefreshSe public DataPlaneTokenRefreshServiceImpl(TokenValidationService tokenValidationService, DidPublicKeyResolver publicKeyResolver, AccessTokenDataStore accessTokenDataStore, - TokenGenerationService tokenGenerationService, Supplier privateKeySupplier, Monitor monitor, String refreshEndpoint) { + TokenGenerationService tokenGenerationService, + Supplier privateKeySupplier, + Monitor monitor, + String refreshEndpoint) { this.tokenValidationService = tokenValidationService; this.publicKeyResolver = publicKeyResolver; this.accessTokenDataStore = accessTokenDataStore; @@ -100,9 +108,11 @@ public DataPlaneTokenRefreshServiceImpl(TokenValidationService tokenValidationSe @Override public Result refreshToken(String refreshToken, String authenticationToken) { - var allRules = new ArrayList<>(tokenValidationRules); + var allRules = new ArrayList<>(authenticationTokenValidationRules); allRules.add(new RefreshTokenMustExistRule(accessTokenDataStore, refreshToken)); + authenticationToken = authenticationToken.replace("Bearer", "").trim(); + var accessTokenDataResult = resolveToken(authenticationToken, allRules); if (accessTokenDataResult.failed()) { return accessTokenDataResult.mapTo(); @@ -182,7 +192,7 @@ public Result obtainToken(TokenParameters tokenParameters, @Override public Result resolve(String token) { - return resolveToken(token, tokenValidationRules); + return resolveToken(token, authenticationTokenValidationRules); } /** diff --git a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java index 28240b505..dab07a6e3 100644 --- a/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java +++ b/edc-extensions/dataplane/dataplane-token-refresh/token-refresh-core/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/core/DataPlaneTokenRefreshServiceImplComponentTest.java @@ -212,6 +212,29 @@ void refresh_whenNoAccessTokenClaim() throws JOSEException { .isEqualTo("Required claim 'access_token' not present on token."); } + @DisplayName("Verify that the equality of the 'iss' and the 'sub' claim of the authentication token") + @Test + void refresh_whenIssNotEqualToSub() throws JOSEException { + var tokenId = "test-token-id"; + var edr = tokenRefreshService.obtainToken(tokenParams(tokenId), DataAddress.Builder.newInstance().type("test-type").build(), Map.of("audience", CONSUMER_DID)) + .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + + var accessToken = edr.getToken(); + var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES384).keyID(consumerKey.getKeyID()).build(); + var claimsSet = getAuthTokenClaims(tokenId, accessToken) + .issuer(CONSUMER_DID) + .subject("violating-subject") + .build(); + + var signedAuthToken = new SignedJWT(jwsHeader, claimsSet); + signedAuthToken.sign(CryptoConverter.createSigner(consumerKey)); + var tokenResponse = tokenRefreshService.refreshToken(edr.getAdditional().get("refreshToken").toString(), signedAuthToken.serialize()); + + assertThat(tokenResponse).isFailed() + .detail() + .isEqualTo("The 'iss' and 'sub' claims must be non-null and identical."); + } + private JWTClaimsSet.Builder getAuthTokenClaims(String tokenId, String accessToken) { return new JWTClaimsSet.Builder() .jwtID(tokenId) diff --git a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/build.gradle.kts b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/build.gradle.kts index 57453561e..2f813d5fa 100644 --- a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/build.gradle.kts +++ b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/build.gradle.kts @@ -23,12 +23,14 @@ plugins { dependencies { + testImplementation(project(":spi:tokenrefresh-spi")) testImplementation(project(":edc-tests:e2e-tests")) testImplementation(libs.edc.junit) testImplementation(libs.restAssured) testImplementation(libs.edc.dpf.http) - + testImplementation(libs.edc.spi.identity.did) + testImplementation(libs.nimbus.jwt) } diff --git a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/DataPlaneTokenRefreshEndToEndTest.java b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/DataPlaneTokenRefreshEndToEndTest.java index 2302d19b1..2432700f7 100644 --- a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/DataPlaneTokenRefreshEndToEndTest.java +++ b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/DataPlaneTokenRefreshEndToEndTest.java @@ -19,27 +19,268 @@ package org.eclipse.tractusx.edc.dataplane.tokenrefresh.e2e; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService; +import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; import org.eclipse.edc.junit.annotations.EndToEndTest; import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.types.domain.DataAddress; +import org.eclipse.edc.spi.types.domain.transfer.DataFlowStartMessage; +import org.eclipse.edc.spi.types.domain.transfer.FlowType; +import org.eclipse.tractusx.edc.dataplane.tokenrefresh.spi.model.TokenResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import java.net.URI; +import java.text.ParseException; +import java.util.Map; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.spi.CoreConstants.EDC_NAMESPACE; +import static org.hamcrest.Matchers.containsString; @EndToEndTest public class DataPlaneTokenRefreshEndToEndTest { - private static final int PROVIDER_CONTROL_PORT = getFreePort(); // port of the control api - + public static final String CONSUMER_DID = "did:web:alice"; + public static final String PROVIDER_DID = "did:web:bob"; + public static final String PROVIDER_KEY_ID = PROVIDER_DID + "#key-1"; + public static final String CONSUMER_KEY_ID = CONSUMER_DID + "#cons-1"; + private static final RuntimeConfig RUNTIME_CONFIG = new RuntimeConfig(); @RegisterExtension protected static final EdcRuntimeExtension DATAPLANE_RUNTIME = new EdcRuntimeExtension( ":edc-tests:runtime:dataplane-cloud", "Token-Refresh-Dataplane", - RuntimeConfig.baseConfig("/signaling", PROVIDER_CONTROL_PORT) + with(RUNTIME_CONFIG.baseConfig(), Map.of("edc.transfer.proxy.token.signer.privatekey.alias", PROVIDER_KEY_ID)) ); + private ECKey providerKey; + private ECKey consumerKey; + + private static Map with(Map baseConfig, Map additionalConfig) { + baseConfig.putAll(additionalConfig); + return baseConfig; + } + + + @BeforeEach + void setup() throws JOSEException { + providerKey = new ECKeyGenerator(Curve.P_384).keyID(PROVIDER_KEY_ID).generate(); + consumerKey = new ECKeyGenerator(Curve.P_256).keyID(CONSUMER_KEY_ID).generate(); + + // mock the did resolver, hard-wire it to the provider or consumer DID + DATAPLANE_RUNTIME.registerServiceMock(DidPublicKeyResolver.class, s -> { + try { + if (s.startsWith(CONSUMER_DID)) { + return Result.success(consumerKey.toPublicKey()); + } else if (s.startsWith(PROVIDER_DID)) { + return Result.success(providerKey.toPublicKey()); + } + throw new IllegalArgumentException("DID '%s' could not be resolved.".formatted(s)); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + }); + } + + @Test + void refresh_success() { + + // register generator and secrets + prepareDataplaneRuntime(); + + var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class); + var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID)) + .orElseThrow(f -> new AssertionError(f.getFailureDetail())); + + var refreshToken = edr.getStringProperty("refreshToken"); + var accessToken = edr.getStringProperty("authorization"); + var authToken = createAuthToken(accessToken, consumerKey); + + var tokenResponse = RUNTIME_CONFIG.getRefreshApi().baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "Bearer " + authToken) + .post("/token") + .then() + .log().ifError() + .statusCode(200) + .extract().body().as(TokenResponse.class); + + assertThat(tokenResponse).isNotNull(); + } + + @DisplayName("The sign key of the authentication token is different from the public key from the DID") + @Test + void refresh_spoofedAuthToken() throws JOSEException { + prepareDataplaneRuntime(); + + var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class); + var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID)) + .orElseThrow(f -> new AssertionError(f.getFailureDetail())); + + var refreshToken = edr.getStringProperty("refreshToken"); + var accessToken = edr.getStringProperty("authorization"); + var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID(CONSUMER_KEY_ID).generate(); + var authTokenWithSpoofedKey = createAuthToken(accessToken, spoofedKey); + + RUNTIME_CONFIG.getRefreshApi().baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "Bearer " + authTokenWithSpoofedKey) + .post("/token") + .then() + .log().ifValidationFails() + .statusCode(401) + .body(containsString("Token verification failed")); + } + + @DisplayName("The refresh token does not match the stored one") + @Test + void refresh_withWrongRefreshToken() { + prepareDataplaneRuntime(); + + var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class); + var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID)) + .orElseThrow(f -> new AssertionError(f.getFailureDetail())); + var refreshToken = "invalid_refresh_token"; + var accessToken = edr.getStringProperty("authorization"); + + RUNTIME_CONFIG.getRefreshApi().baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "Bearer " + createAuthToken(accessToken, consumerKey)) + .post("/token") + .then() + .log().ifValidationFails() + .statusCode(401) + .body(containsString("Provided refresh token does not match the stored refresh token.")); + } + + + @DisplayName("The authentication token misses required claims: access_token") @Test - void foo() { - // will be used once the RefreshAPI is here as well + void refresh_invalidAuthenticationToken_missingAccessToken() { + prepareDataplaneRuntime(); + + var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class); + var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID)) + .orElseThrow(f -> new AssertionError(f.getFailureDetail())); + + var refreshToken = edr.getStringProperty("refreshToken"); + var accessToken = edr.getStringProperty("authorization"); + + var claims = new JWTClaimsSet.Builder() + /* missing: .claim("access_token", accessToken)*/ + .issuer(CONSUMER_DID) + .subject(CONSUMER_DID) + .audience("did:web:bob") + .jwtID(getJwtId(accessToken)) + .build(); + var authToken = createJwt(consumerKey, claims); + + RUNTIME_CONFIG.getRefreshApi().baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "Bearer " + authToken) + .post("/token") + .then() + .log().ifValidationFails() + .statusCode(401) + .body(containsString("Required claim 'access_token' not present on token.")); + } + + @DisplayName("The authentication token misses required claims: audience") + @Test + void refresh_invalidAuthenticationToken_missingAudience() { + prepareDataplaneRuntime(); + + var authorizationService = DATAPLANE_RUNTIME.getService(DataPlaneAuthorizationService.class); + var edr = authorizationService.createEndpointDataReference(createStartMessage("test-process-id", CONSUMER_DID)) + .orElseThrow(f -> new AssertionError(f.getFailureDetail())); + + var refreshToken = edr.getStringProperty("refreshToken"); + var accessToken = edr.getStringProperty("authorization"); + + var claims = new JWTClaimsSet.Builder() + .claim("access_token", accessToken) + .issuer(CONSUMER_DID) + .subject(CONSUMER_DID) + /* missing: .audience("did:web:bob")*/ + .jwtID(getJwtId(accessToken)) + .build(); + var authToken = createJwt(consumerKey, claims); + + RUNTIME_CONFIG.getRefreshApi().baseRequest() + .queryParam("grant_type", "refresh_token") + .queryParam("refresh_token", refreshToken) + .header(AUTHORIZATION, "Bearer " + authToken) + .post("/token") + .then() + .log().ifValidationFails() + .statusCode(401) + .body(containsString("Required claim 'aud' not present on token.")); + } + + private void prepareDataplaneRuntime() { + var vault = DATAPLANE_RUNTIME.getContext().getService(Vault.class); + vault.storeSecret(PROVIDER_KEY_ID, providerKey.toJSONString()); + } + + private String createAuthToken(String accessToken, ECKey signerKey) { + var claims = new JWTClaimsSet.Builder() + .claim("access_token", accessToken) + .issuer(CONSUMER_DID) + .subject(CONSUMER_DID) + .audience("did:web:bob") + .jwtID(getJwtId(accessToken)) + .build(); + return createJwt(signerKey, claims); + } + + private String createJwt(ECKey signerKey, JWTClaimsSet claims) { + var header = new JWSHeader.Builder(JWSAlgorithm.ES256).keyID(consumerKey.getKeyID()).build(); + var jwt = new SignedJWT(header, claims); + try { + jwt.sign(new ECDSASigner(signerKey)); + return jwt.serialize(); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + private String getJwtId(String accessToken) { + try { + return SignedJWT.parse(accessToken).getJWTClaimsSet().getJWTID(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + private DataFlowStartMessage createStartMessage(String processId, String audience) { + return DataFlowStartMessage.Builder.newInstance() + .processId(processId) + .sourceDataAddress(DataAddress.Builder.newInstance().type("HttpData").property(EDC_NAMESPACE + "baseUrl", "http://foo.bar/").build()) + .destinationDataAddress(DataAddress.Builder.newInstance().type("HttpData").property(EDC_NAMESPACE + "baseUrl", "http://fizz.buzz").build()) + .flowType(FlowType.PULL) + .participantId("some-participantId") + .assetId("test-asset") + .callbackAddress(URI.create("https://foo.bar/callback")) + .agreementId("test-agreement") + .property("audience", audience) + .build(); } } diff --git a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/RuntimeConfig.java b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/RuntimeConfig.java index a1ff0e1f5..11c8da8fe 100644 --- a/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/RuntimeConfig.java +++ b/edc-tests/edc-dataplane/edc-dataplane-tokenrefresh-tests/src/test/java/org/eclipse/tractusx/edc/dataplane/tokenrefresh/e2e/RuntimeConfig.java @@ -19,33 +19,68 @@ package org.eclipse.tractusx.edc.dataplane.tokenrefresh.e2e; +import io.restassured.specification.RequestSpecification; + +import java.net.URI; import java.util.HashMap; import java.util.Map; +import static io.restassured.RestAssured.given; import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; /** * Configuration baseline for Data-Plane e2e tests */ public class RuntimeConfig { + + private final Endpoint publicApi = new Endpoint(URI.create("http://localhost:%d/public".formatted(getFreePort()))); + private final Endpoint signalingApi = new Endpoint(URI.create("http://localhost:%d/signaling".formatted(getFreePort()))); + private final Endpoint refreshApi = publicApi; + private final Endpoint defaultApi = new Endpoint(URI.create("http://localhost:%d/api".formatted(getFreePort()))); + /** * Configures the data plane token endpoint, and all relevant HTTP contexts */ - public static Map baseConfig(String signalingPath, int signalingPort) { + public Map baseConfig() { return new HashMap<>() { { put("edc.dataplane.token.validation.endpoint", "http://token-validation.com"); - put("web.http.path", "/api"); - put("web.http.port", String.valueOf(getFreePort())); - put("web.http.public.path", "/public"); - put("web.http.public.port", String.valueOf(getFreePort())); - put("web.http.consumer.api.path", "/api/consumer"); - put("web.http.consumer.api.port", String.valueOf(getFreePort())); - put("web.http.signaling.path", signalingPath); - put("web.http.signaling.port", String.valueOf(signalingPort)); + put("web.http.path", defaultApi.url().getPath()); + put("web.http.port", String.valueOf(defaultApi.url().getPort())); + put("web.http.public.path", publicApi.url().getPath()); + put("web.http.public.port", String.valueOf(publicApi.url().getPort())); + put("web.http.signaling.path", signalingApi.url().getPath()); + put("web.http.signaling.port", String.valueOf(signalingApi.url().getPort())); } }; } + public Endpoint getPublicApi() { + return publicApi; + } + + public Endpoint getSignalingApi() { + return signalingApi; + } + + public Endpoint getRefreshApi() { + return refreshApi; + } + + public Endpoint getDefaultApi() { + return defaultApi; + } + + public record Endpoint(URI url, Map headers) { + public Endpoint(URI url) { + this(url, Map.of()); + } + + public RequestSpecification baseRequest() { + return given().baseUri(url.toString()).headers(headers); + } + + } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f9b5dfdf..51b753a60 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -161,6 +161,7 @@ edc-dpf-http = { module = "org.eclipse.edc:data-plane-http", version.ref = "edc" edc-dpf-oauth2 = { module = "org.eclipse.edc:data-plane-http-oauth2", version.ref = "edc" } edc-dpf-api-control = { module = "org.eclipse.edc:data-plane-control-api", version.ref = "edc" } edc-dpf-api-public = { module = "org.eclipse.edc:data-plane-public-api", version.ref = "edc" } +edc-dpf-api-public-v2 = { module = "org.eclipse.edc:data-plane-public-api-v2", version.ref = "edc" } edc-dpf-api-signaling = { module = "org.eclipse.edc:data-plane-signaling-api", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5ede9eab1..dffc3350d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -63,6 +63,7 @@ include(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-provider- include(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-provider-api") include(":edc-extensions:dataplane:dataplane-selector-configuration") include(":edc-extensions:dataplane:dataplane-token-refresh:token-refresh-core") +include(":edc-extensions:dataplane:dataplane-token-refresh:token-refresh-api") // test modules include(":edc-tests:e2e-tests")