From 2eefce4dc5a56c7c222bb065d78fb1d377d6691f Mon Sep 17 00:00:00 2001 From: Enrico Risa Date: Wed, 14 Feb 2024 18:58:02 +0100 Subject: [PATCH] chore: backport of issue upstream#3607 --- .../edc-controlplane-base/build.gradle.kts | 2 +- .../edc-dataplane-base/build.gradle.kts | 2 +- edc-extensions/auth-tokenbased/README.md | 21 +++++ .../auth-tokenbased/build.gradle.kts | 26 ++++++ .../TokenBasedAuthenticationExtension.java | 65 +++++++++++++++ .../TokenBasedAuthenticationService.java | 55 +++++++++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 ++++ ...TokenBasedAuthenticationExtensionTest.java | 80 +++++++++++++++++++ .../TokenBasedAuthenticationServiceTest.java | 76 ++++++++++++++++++ .../edc-dataplane-proxy-e2e/build.gradle.kts | 2 +- gradle/libs.versions.toml | 1 - settings.gradle.kts | 1 + 12 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 edc-extensions/auth-tokenbased/README.md create mode 100644 edc-extensions/auth-tokenbased/build.gradle.kts create mode 100644 edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtension.java create mode 100644 edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationService.java create mode 100644 edc-extensions/auth-tokenbased/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtensionTest.java create mode 100644 edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationServiceTest.java diff --git a/edc-controlplane/edc-controlplane-base/build.gradle.kts b/edc-controlplane/edc-controlplane-base/build.gradle.kts index f8bc52809..6c3d55709 100644 --- a/edc-controlplane/edc-controlplane-base/build.gradle.kts +++ b/edc-controlplane/edc-controlplane-base/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { runtimeOnly(project(":edc-extensions:edr:edr-api")) runtimeOnly(project(":edc-extensions:edr:edr-callback")) + runtimeOnly(project(":edc-extensions:auth-tokenbased")) // needed for BPN validation runtimeOnly(project(":edc-extensions:bpn-validation")) @@ -44,7 +45,6 @@ dependencies { runtimeOnly(libs.edc.core.controlplane) runtimeOnly(libs.edc.config.filesystem) - runtimeOnly(libs.edc.auth.tokenbased) runtimeOnly(libs.edc.api.management) runtimeOnly(libs.edc.api.management.config) diff --git a/edc-dataplane/edc-dataplane-base/build.gradle.kts b/edc-dataplane/edc-dataplane-base/build.gradle.kts index 03a5ee3db..2d029ea09 100644 --- a/edc-dataplane/edc-dataplane-base/build.gradle.kts +++ b/edc-dataplane/edc-dataplane-base/build.gradle.kts @@ -23,13 +23,13 @@ plugins { } dependencies { + runtimeOnly(project(":edc-extensions:auth-tokenbased")) runtimeOnly(project(":core:edr-cache-core")) runtimeOnly(project(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-consumer-api")) runtimeOnly(project(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-provider-api")) runtimeOnly(project(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-provider-core")) runtimeOnly(libs.edc.config.filesystem) - runtimeOnly(libs.edc.auth.tokenbased) runtimeOnly(libs.edc.dpf.awss3) runtimeOnly(libs.edc.dpf.oauth2) runtimeOnly(libs.edc.dpf.http) diff --git a/edc-extensions/auth-tokenbased/README.md b/edc-extensions/auth-tokenbased/README.md new file mode 100644 index 000000000..a19a72002 --- /dev/null +++ b/edc-extensions/auth-tokenbased/README.md @@ -0,0 +1,21 @@ +# Token Based Authentication Service + +The token based authentication service extension is used to secure connector APIs. These APIs are not protected by the `AuthenticationService` by default. To find out how a specific API is protected please consult its documentation. + +APIs, protected by this extension, require a client to authenticate by adding a authentication key to the request header. + +Authentication Header Example: +``` +curl --header "X-API-Key: " +``` + +## Configuration + +| Key | Description | Required | +|:-----------------------|:-------------------------------------------------------------|:---------| +| edc.api.auth.key | API Key Header Value | false | +| edc.api.auth.key.alias | Secret name of the API Key Header Value, stored in the vault | false | + +- If the API key is stored in the Vault _and_ in the configuration, the extension will take the key from the vault. + +- If no API key is defined, a random value is generated and printed out into the logs. \ No newline at end of file diff --git a/edc-extensions/auth-tokenbased/build.gradle.kts b/edc-extensions/auth-tokenbased/build.gradle.kts new file mode 100644 index 000000000..5e1798077 --- /dev/null +++ b/edc-extensions/auth-tokenbased/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 - 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(libs.edc.spi.auth) + implementation(libs.jakarta.rsApi) + + testImplementation(testFixtures(libs.edc.junit)) +} + + diff --git a/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtension.java b/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtension.java new file mode 100644 index 000000000..5a299eb8f --- /dev/null +++ b/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtension.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 - 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * Mercedes-Benz Tech Innovation GmbH - add README.md; authentication key can be retrieved from vault + * Fraunhofer Institute for Software and Systems Engineering - update monitor info + * + */ + +package org.eclipse.tractusx.edc.api.auth.token; + +import org.eclipse.edc.api.auth.spi.AuthenticationService; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +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 java.util.UUID; + +/** + * Extension that registers an AuthenticationService that uses API Keys + */ +@Provides(AuthenticationService.class) +@Extension(value = TokenBasedAuthenticationExtension.NAME) +public class TokenBasedAuthenticationExtension implements ServiceExtension { + + public static final String NAME = "Static token API Authentication"; + @Setting + private static final String AUTH_SETTING_APIKEY = "edc.api.auth.key"; + @Setting + private static final String AUTH_SETTING_APIKEY_ALIAS = "edc.api.auth.key.alias"; + @Inject + private Vault vault; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + String apiKey = null; + + var apiKeyAlias = context.getSetting(AUTH_SETTING_APIKEY_ALIAS, null); + if (apiKeyAlias != null) { + apiKey = vault.resolveSecret(apiKeyAlias); + } + + if (apiKey == null) { + apiKey = context.getSetting(AUTH_SETTING_APIKEY, UUID.randomUUID().toString()); + } + + context.registerService(AuthenticationService.class, new TokenBasedAuthenticationService(apiKey)); + } +} diff --git a/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationService.java b/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationService.java new file mode 100644 index 000000000..06ed866cc --- /dev/null +++ b/edc-extensions/auth-tokenbased/src/main/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 - 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.api.auth.token; + +import org.eclipse.edc.api.auth.spi.AuthenticationService; +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class TokenBasedAuthenticationService implements AuthenticationService { + + private static final String API_KEY_HEADER_NAME = "x-api-key"; + private final String hardCodedApiKey; //todo: have a list of API keys? + + public TokenBasedAuthenticationService(String hardCodedApiKey) { + this.hardCodedApiKey = hardCodedApiKey; + } + + /** + * Checks whether a particular request is authorized based on the "X-Api-Key" header. + * + * @param headers The headers, that have to contain the "X-Api-Key" header. + * @throws IllegalArgumentException The map of headers did not contain the "X-Api-Key" header + */ + @Override + public boolean isAuthenticated(Map> headers) { + + Objects.requireNonNull(headers, "headers"); + + var apiKey = headers.keySet().stream() + .filter(k -> k.equalsIgnoreCase(API_KEY_HEADER_NAME)) + .map(headers::get) + .findFirst(); + + return apiKey.map(this::checkApiKeyValid).orElseThrow(() -> new AuthenticationFailedException(API_KEY_HEADER_NAME + " not found")); + } + + private boolean checkApiKeyValid(List apiKeys) { + return apiKeys.size() == 1 && apiKeys.stream().allMatch(hardCodedApiKey::equalsIgnoreCase); + } +} diff --git a/edc-extensions/auth-tokenbased/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/auth-tokenbased/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..2b47ab2dd --- /dev/null +++ b/edc-extensions/auth-tokenbased/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ + # +# Copyright (c) 2020 - 2022 Microsoft Corporation +# +# 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: +# Microsoft Corporation - initial API and implementation +# +# + +org.eclipse.tractusx.edc.api.auth.token.TokenBasedAuthenticationExtension diff --git a/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtensionTest.java b/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtensionTest.java new file mode 100644 index 000000000..4cfcd052c --- /dev/null +++ b/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationExtensionTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 Mercedes-Benz Tech Innovation GmbH + * + * 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: + * Mercedes-Benz Tech Innovation GmbH - initial implementation + * + */ + +package org.eclipse.tractusx.edc.api.auth.token; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.security.Vault; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class TokenBasedAuthenticationExtensionTest { + + private static final String AUTH_SETTING_APIKEY = "edc.api.auth.key"; + private static final String AUTH_SETTING_APIKEY_ALIAS = "edc.api.auth.key.alias"; + private static final String VAULT_KEY = "foo"; + + private final Vault vault = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(Vault.class, vault); + + when(vault.resolveSecret(VAULT_KEY)).thenReturn("foo"); + } + + @Test + public void testPrimaryMethod_loadKeyFromVault(ServiceExtensionContext context, TokenBasedAuthenticationExtension extension) { + when(context.getSetting(eq(AUTH_SETTING_APIKEY_ALIAS), isNull())).thenReturn(VAULT_KEY); + when(context.getSetting(eq(AUTH_SETTING_APIKEY), anyString())).thenReturn("bar"); + + extension.initialize(context); + + verify(context, never()) + .getSetting(eq(AUTH_SETTING_APIKEY), anyString()); + + verify(context) + .getSetting(AUTH_SETTING_APIKEY_ALIAS, null); + + verify(vault).resolveSecret(VAULT_KEY); + } + + @Test + public void testSecondaryMethod_loadKeyFromConfig(ServiceExtensionContext context, TokenBasedAuthenticationExtension extension) { + when(context.getSetting(eq(AUTH_SETTING_APIKEY_ALIAS), isNull())).thenReturn(null); + when(context.getSetting(eq(AUTH_SETTING_APIKEY), anyString())).thenReturn("bar"); + + extension.initialize(context); + + verify(context) + .getSetting(eq(AUTH_SETTING_APIKEY), anyString()); + + verify(context) + .getSetting(AUTH_SETTING_APIKEY_ALIAS, null); + + verify(vault, never()).resolveSecret(anyString()); + } + +} diff --git a/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationServiceTest.java b/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationServiceTest.java new file mode 100644 index 000000000..4304ea50b --- /dev/null +++ b/edc-extensions/auth-tokenbased/src/test/java/org/eclipse/tractusx/edc/api/auth/token/TokenBasedAuthenticationServiceTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2020 - 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.tractusx.edc.api.auth.token; + +import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TokenBasedAuthenticationServiceTest { + + private static final String TEST_API_KEY = "test-key"; + private TokenBasedAuthenticationService service; + + @BeforeEach + void setUp() { + service = new TokenBasedAuthenticationService(TEST_API_KEY); + } + + @ParameterizedTest + @ValueSource(strings = { "x-api-key", "X-API-KEY", "X-Api-Key" }) + void isAuthorized(String validKey) { + var map = Map.of(validKey, List.of(TEST_API_KEY)); + assertThat(service.isAuthenticated(map)).isTrue(); + } + + @Test + void isAuthorized_headerNotPresent_throwsException() { + var map = Map.of("header1", List.of("val1, val2"), + "header2", List.of("anotherval1", "anotherval2")); + assertThatThrownBy(() -> service.isAuthenticated(map)).isInstanceOf(AuthenticationFailedException.class).hasMessage("x-api-key not found"); + } + + @Test + void isAuthorized_headersEmpty_throwsException() { + Map> map = Collections.emptyMap(); + assertThatThrownBy(() -> service.isAuthenticated(map)).isInstanceOf(AuthenticationFailedException.class).hasMessage("x-api-key not found"); + } + + @Test + void isAuthorized_headersNull_throwsException() { + assertThatThrownBy(() -> service.isAuthenticated(null)).isInstanceOf(NullPointerException.class); + } + + @Test + void isAuthorized_notAuthorized() { + var map = Map.of("x-api-key", List.of("invalid_api_key")); + assertThat(service.isAuthenticated(map)).isFalse(); + } + + @Test + void isAuthorized_multipleValues_oneAuthorized_shouldReturnFalse() { + var map = Map.of("x-api-key", List.of("invalid_api_key", TEST_API_KEY)); + assertThat(service.isAuthenticated(map)).isFalse(); + } +} \ No newline at end of file diff --git a/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts b/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts index eeb8473f3..f5b46c633 100644 --- a/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts +++ b/edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { // test runtime config testImplementation(libs.edc.config.filesystem) testImplementation(libs.edc.dpf.http) - testImplementation(libs.edc.auth.tokenbased) + testImplementation(project(":edc-extensions:auth-tokenbased")) testImplementation(project(":spi:edr-spi")) testImplementation(project(":core:edr-cache-core")) testImplementation(project(":edc-extensions:dataplane-proxy:edc-dataplane-proxy-consumer-api")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62eedbb9f..516796d8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,7 +70,6 @@ edc-api-transferprocess = { module = "org.eclipse.edc:transfer-process-api", ver edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-iam-mock = { module = "org.eclipse.edc:iam-mock", version.ref = "edc" } edc-policy-engine = { module = "org.eclipse.edc:policy-engine", version.ref = "edc" } -edc-auth-tokenbased = { module = "org.eclipse.edc:auth-tokenbased", version.ref = "edc" } edc-auth-oauth2-core = { module = "org.eclipse.edc:oauth2-core", version.ref = "edc" } edc-auth-oauth2-daps = { module = "org.eclipse.edc:oauth2-daps", version.ref = "edc" } edc-auth-oauth2-client = { module = "org.eclipse.edc:oauth2-client", version.ref = "edc" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 2be4c8d8e..c83594d7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,6 +33,7 @@ include(":core:json-ld-core") include(":edc-extensions:bpn-validation") +include(":edc-extensions:auth-tokenbased") include(":edc-extensions:bpn-validation:bpn-validation-api") include(":edc-extensions:bpn-validation:bpn-validation-spi") include(":edc-extensions:bpn-validation:bpn-validation-core")