-
Notifications
You must be signed in to change notification settings - Fork 236
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement Delegated Authentication Service (#4270)
* add jwks resolver and auth service * improved unit tests by splitting * added extension test * added fallback header, key caching [skip ci] * updated log line * license header * license header * DEPENDENCIES * DEPENDENCIES * only register tokenbased if no other is registered * DEPENDENCIES * pr remarks * use factory method for JwksPublicKeyResolverImpl
- Loading branch information
1 parent
bacdf5a
commit 4441956
Showing
19 changed files
with
967 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) | ||
* | ||
* 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: | ||
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation | ||
* | ||
*/ | ||
|
||
plugins { | ||
`java-library` | ||
} | ||
|
||
dependencies { | ||
api(project(":spi:common:auth-spi")) | ||
api(project(":spi:common:token-spi")) | ||
implementation(project(":core:common:token-core")) // for the validation rules | ||
implementation(project(":core:common:lib:crypto-common-lib")) | ||
implementation(libs.jakarta.rsApi) | ||
implementation(libs.nimbus.jwt) | ||
|
||
testImplementation(project(":core:common:junit")) | ||
testImplementation(project(":core:common:lib:keys-lib")) | ||
testImplementation(libs.mockserver.netty) | ||
testImplementation(libs.awaitility) | ||
} | ||
|
||
|
85 changes: 85 additions & 0 deletions
85
...ed/src/main/java/org/eclipse/edc/api/auth/delegated/DelegatedAuthenticationExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) | ||
* | ||
* 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: | ||
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation | ||
* | ||
*/ | ||
|
||
package org.eclipse.edc.api.auth.delegated; | ||
|
||
import org.eclipse.edc.api.auth.spi.registry.ApiAuthenticationRegistry; | ||
import org.eclipse.edc.keys.spi.KeyParserRegistry; | ||
import org.eclipse.edc.runtime.metamodel.annotation.Extension; | ||
import org.eclipse.edc.runtime.metamodel.annotation.Inject; | ||
import org.eclipse.edc.runtime.metamodel.annotation.Setting; | ||
import org.eclipse.edc.spi.system.ServiceExtension; | ||
import org.eclipse.edc.spi.system.ServiceExtensionContext; | ||
import org.eclipse.edc.token.rules.ExpirationIssuedAtValidationRule; | ||
import org.eclipse.edc.token.rules.NotBeforeValidationRule; | ||
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry; | ||
import org.eclipse.edc.token.spi.TokenValidationService; | ||
|
||
import java.time.Clock; | ||
|
||
import static com.nimbusds.jose.jwk.source.JWKSourceBuilder.DEFAULT_CACHE_TIME_TO_LIVE; | ||
import static org.eclipse.edc.api.auth.delegated.DelegatedAuthenticationService.MANAGEMENT_API_CONTEXT; | ||
|
||
/** | ||
* Extension that registers an AuthenticationService that delegates authentication and authorization to a third-party IdP | ||
*/ | ||
@Extension(value = DelegatedAuthenticationExtension.NAME) | ||
public class DelegatedAuthenticationExtension implements ServiceExtension { | ||
|
||
public static final int DEFAULT_VALIDATION_TOLERANCE = 5_000; | ||
public static final String NAME = "Delegating Authentication Service Extension"; | ||
@Setting(value = "Duration (in ms) that the internal key cache is valid", type = "Long", defaultValue = "" + DEFAULT_CACHE_TIME_TO_LIVE) | ||
public static final String AUTH_SETTING_CACHE_VALIDITY_MS = "edc.api.auth.dac.cache.validity"; | ||
@Setting(value = "URL where the third-party IdP's public key(s) can be resolved") | ||
public static final String AUTH_SETTING_KEY_URL = "edc.api.auth.dac.key.url"; | ||
@Setting(value = "Default token validation time tolerance (in ms), e.g. for nbf or exp claims", defaultValue = "" + DEFAULT_VALIDATION_TOLERANCE) | ||
private static final String AUTH_SETTING_VALIDATION_TOLERANCE_MS = "edc.api.auth.dac.validation.tolerance"; | ||
@Inject | ||
private ApiAuthenticationRegistry authenticationRegistry; | ||
@Inject | ||
private TokenValidationRulesRegistry tokenValidationRulesRegistry; | ||
@Inject | ||
private KeyParserRegistry keyParserRegistry; | ||
@Inject | ||
private TokenValidationService tokenValidationService; | ||
@Inject | ||
private Clock clock; | ||
|
||
@Override | ||
public String name() { | ||
return NAME; | ||
} | ||
|
||
@Override | ||
public void initialize(ServiceExtensionContext context) { | ||
var monitor = context.getMonitor().withPrefix("Delegated API Authentication"); | ||
|
||
var keyUrl = context.getConfig().getString(AUTH_SETTING_KEY_URL, null); | ||
if (keyUrl == null) { | ||
monitor.warning("The '%s' setting was not provided, so the DelegatedAuthenticationService will NOT be registered. In this case, the TokenBasedAuthenticationService usually acts as fallback.".formatted(AUTH_SETTING_KEY_URL)); | ||
return; | ||
} | ||
var cacheValidityMs = context.getConfig().getLong(AUTH_SETTING_CACHE_VALIDITY_MS, DEFAULT_CACHE_TIME_TO_LIVE); | ||
var tolerance = context.getConfig().getInteger(AUTH_SETTING_VALIDATION_TOLERANCE_MS, DEFAULT_VALIDATION_TOLERANCE); | ||
|
||
//todo: currently, only JWKS urls are supported | ||
var resolver = JwksPublicKeyResolver.create(keyParserRegistry, keyUrl, monitor, cacheValidityMs); | ||
|
||
tokenValidationRulesRegistry.addRule(MANAGEMENT_API_CONTEXT, new NotBeforeValidationRule(clock, tolerance, true)); | ||
tokenValidationRulesRegistry.addRule(MANAGEMENT_API_CONTEXT, new ExpirationIssuedAtValidationRule(clock, tolerance, true)); | ||
|
||
// always register - this would potentially overwrite other services | ||
authenticationRegistry.register("management-api", new DelegatedAuthenticationService(resolver, monitor, tokenValidationService, tokenValidationRulesRegistry)); | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
...ated/src/main/java/org/eclipse/edc/api/auth/delegated/DelegatedAuthenticationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
/* | ||
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) | ||
* | ||
* 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: | ||
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation | ||
* | ||
*/ | ||
|
||
package org.eclipse.edc.api.auth.delegated; | ||
|
||
import org.eclipse.edc.api.auth.spi.AuthenticationService; | ||
import org.eclipse.edc.keys.spi.PublicKeyResolver; | ||
import org.eclipse.edc.spi.monitor.Monitor; | ||
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry; | ||
import org.eclipse.edc.token.spi.TokenValidationService; | ||
import org.eclipse.edc.web.spi.exception.AuthenticationFailedException; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; | ||
|
||
public class DelegatedAuthenticationService implements AuthenticationService { | ||
|
||
public static final String MANAGEMENT_API_CONTEXT = "management-api"; | ||
@Deprecated(since = "0.7.1") | ||
private static final String X_API_KEY = "x-api-key"; | ||
public static final String OLD_API_KEY_WARNING = ("Header '%s' found with the DelegatedAuthenticationService. " + | ||
"Please migrate to using the '%s' header at your earliest convenience, this compatibility feature will be removed in upcoming releases!").formatted(X_API_KEY, AUTHORIZATION); | ||
private final PublicKeyResolver publicKeyResolver; | ||
private final Monitor monitor; | ||
private final TokenValidationService tokenValidationService; | ||
private final TokenValidationRulesRegistry rulesRegistry; | ||
|
||
public DelegatedAuthenticationService(PublicKeyResolver publicKeyResolver, | ||
Monitor monitor, | ||
TokenValidationService tokenValidationService, | ||
TokenValidationRulesRegistry rulesRegistry) { | ||
this.publicKeyResolver = publicKeyResolver; | ||
this.monitor = monitor; | ||
this.tokenValidationService = tokenValidationService; | ||
this.rulesRegistry = rulesRegistry; | ||
} | ||
|
||
@Override | ||
public boolean isAuthenticated(Map<String, List<String>> headers) { | ||
|
||
if (headers == null) { | ||
var msg = "Headers were null"; | ||
monitor.warning(msg); | ||
throw new AuthenticationFailedException(msg); | ||
} | ||
|
||
var authHeaders = headers.get(AUTHORIZATION); | ||
if (authHeaders == null || authHeaders.isEmpty()) { | ||
// fall back to X-API-Key - backwards compatibility | ||
authHeaders = headers.get(X_API_KEY); | ||
if (authHeaders != null && !authHeaders.isEmpty()) { | ||
monitor.warning(OLD_API_KEY_WARNING); | ||
} | ||
} | ||
|
||
return Optional.ofNullable(authHeaders) | ||
.map(this::performTokenValidation) | ||
.orElseThrow(() -> { | ||
var msg = "Header '%s' not present".formatted(AUTHORIZATION); | ||
monitor.warning(msg); | ||
return new AuthenticationFailedException(msg); | ||
}); | ||
|
||
} | ||
|
||
private boolean performTokenValidation(List<String> authHeaders) { | ||
if (authHeaders.size() != 1) { | ||
monitor.warning("Expected exactly 1 Authorization header, found %d".formatted(authHeaders.size())); | ||
return false; | ||
} | ||
var token = authHeaders.get(0); | ||
if (!token.toLowerCase().startsWith("bearer ")) { | ||
monitor.warning("Authorization header must start with 'Bearer '"); | ||
return false; | ||
} | ||
token = token.substring(6).trim(); // "bearer" has 7 characters, it could be upper case, lower case or capitalized | ||
|
||
var rules = rulesRegistry.getRules(MANAGEMENT_API_CONTEXT); | ||
return tokenValidationService.validate(token, publicKeyResolver, rules).succeeded(); | ||
} | ||
|
||
} |
Oops, something went wrong.