Skip to content

Commit

Permalink
chore: backport of issue upstream#3607
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood committed Feb 14, 2024
1 parent c997330 commit 2eefce4
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 4 deletions.
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 {

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
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
Loading

0 comments on commit 2eefce4

Please sign in to comment.