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

chore: backport of issue upstream#3607 #1055

Merged
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
1 change: 0 additions & 1 deletion DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ maven/mavencentral/org.eclipse.edc/asset-api/0.2.1, Apache-2.0, approved, techno
maven/mavencentral/org.eclipse.edc/asset-index-sql/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/asset-spi/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/auth-spi/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/auth-tokenbased/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/autodoc-processor/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/aws-s3-core/0.2.1, Apache-2.0, approved, technology.edc
maven/mavencentral/org.eclipse.edc/boot/0.2.1, Apache-2.0, approved, technology.edc
Expand Down
2 changes: 1 addition & 1 deletion edc-controlplane/edc-controlplane-base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion edc-dataplane/edc-dataplane-base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions edc-extensions/auth-tokenbased/README.md
Original file line number Diff line number Diff line change
@@ -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 <url> --header "X-API-Key: <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.
26 changes: 26 additions & 0 deletions edc-extensions/auth-tokenbased/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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))
}


Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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 {
wolf4ood marked this conversation as resolved.
Show resolved Hide resolved

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<String, List<String>> 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<String> apiKeys) {
return apiKeys.size() == 1 && apiKeys.stream().allMatch(hardCodedApiKey::equalsIgnoreCase);
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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<String, List<String>> 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();
}
}
2 changes: 1 addition & 1 deletion edc-tests/edc-dataplane-proxy-e2e/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading
Loading