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

feat: OAuth2 endpoint audience #2000

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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.eclipse.dataspaceconnector.boot.system.injection.lifecycle;

import org.eclipse.dataspaceconnector.runtime.metamodel.annotation.Inject;
import org.eclipse.dataspaceconnector.runtime.metamodel.annotation.Provider;
import org.eclipse.dataspaceconnector.spi.monitor.Monitor;
import org.eclipse.dataspaceconnector.spi.system.ServiceExtension;
Expand All @@ -25,9 +26,9 @@
* {@link ServiceExtension} implementors should not be constructed by just invoking their constructors, instead they need to go through
* a lifecycle, which is what this class aims at doing. There are three major phases for initialization:
* <ol>
* <li>inject dependencies: all fields annotated with {@link org.eclipse.dataspaceconnector.spi.system.Inject} are set</li>
* <li>inject dependencies: all fields annotated with {@link Inject} are set</li>
* <li>initialize: invokes the {@link ServiceExtension#initialize(ServiceExtensionContext)} method</li>
* <li>provide: invokes all methods annotated with {@link org.eclipse.dataspaceconnector.spi.system.Provider} to register more services into the context</li>
* <li>provide: invokes all methods annotated with {@link Provider} to register more services into the context</li>
* </ol>
* <p>
* The sequence of these phases is actually important.
Expand Down Expand Up @@ -62,7 +63,7 @@ public static RegistrationPhase initialize(InitializePhase phase) {
}

/**
* Scans the {@linkplain ServiceExtension} for methods annotated with {@linkplain org.eclipse.dataspaceconnector.spi.system.Provider}
* Scans the {@linkplain ServiceExtension} for methods annotated with {@linkplain Provider}
* with the {@link Provider#isDefault()} flag set to {@code false}, invokes them and registers the bean into the {@link ServiceExtensionContext} if necessary.
*/
public static PreparePhase provide(RegistrationPhase phase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,16 @@ public Result<Void> checkRule(@NotNull ClaimToken toVerify, @Nullable Map<String
}

private Result<Void> verifyTokenIds(ClaimToken jwt, String issuerConnector, @Nullable String securityProfile) {
var claims = jwt.getClaims();

//referringConnector (DAT, optional) vs issuerConnector (Message-Header, mandatory)
var referringConnector = claims.get("referringConnector");
var referringConnector = jwt.getClaim("referringConnector");

if (validateReferring && !issuerConnector.equals(referringConnector)) {
return Result.failure("refferingConnector in token does not match issuerConnector in message");
}

//securityProfile (DAT, mandatory) vs securityProfile (Message-Payload, optional)
try {
var tokenSecurityProfile = claims.get("securityProfile");
var tokenSecurityProfile = jwt.getClaim("securityProfile");

if (securityProfile != null && !securityProfile.equals(tokenSecurityProfile)) {
return Result.failure("securityProfile in token does not match securityProfile in payload");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ void generateAndVerifyJwtToken_valid() {

Result<ClaimToken> verificationResult = identityService.verifyJwtToken(result.getContent(), "Bar");
assertTrue(verificationResult.succeeded());
assertEquals("eu", verificationResult.getContent().getClaims().get("region"));
assertEquals("eu", verificationResult.getContent().getStringClaim("region"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class DapsIntegrationTest {
"edc.oauth.token.url", DAPS_URL + "/token",
"edc.oauth.client.id", CLIENT_ID,
"edc.oauth.provider.audience", AUDIENCE_IDS_CONNECTORS_ALL,
"edc.oauth.endpoint.audience", AUDIENCE_IDS_CONNECTORS_ALL,
"edc.oauth.provider.jwks.url", DAPS_URL + "/.well-known/jwks.json",
"edc.oauth.public.key.alias", CLIENT_KEYSTORE_KEY_ALIAS,
"edc.oauth.private.key.alias", CLIENT_KEYSTORE_KEY_ALIAS
Expand Down
21 changes: 11 additions & 10 deletions extensions/common/iam/oauth2/oauth2-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ This extension provides an `IdentityService` implementation based on the OAuth2

## Configuration

| Parameter name | Description | Mandatory | Default value |
|:----------------------------------|:-------------------------------------------------------------------------------------------|:----------|:--------------------------------|
| `edc.oauth.token.url` | URL of the authorization server | true | null |
| `edc.oauth.provider.audience` | Provider audience | false | id of the connector |
| `edc.oauth.provider.jwks.url` | URL from which well-known public keys of Authorization server can be fetched | false | http://localhost/empty_jwks_url |
| `edc.oauth.public.key.alias` | Alias of public associated with client certificate | true | null |
| `edc.oauth.private.key.alias` | Alias of private key (used to sign the token) | true | null |
| `edc.oauth.provider.jwks.refresh` | Interval at which public keys are refreshed from Authorization server (in minutes) | false | 5 |
| `edc.oauth.client.id` | Public identifier of the client | true | null |
| `edc.oauth.validation.nbf.leeway` | Leeway in seconds added to current time to remedy clock skew on notBefore claim validation | false | 10 |
| Parameter name | Description | Mandatory | Default value |
|:----------------------------------|:-------------------------------------------------------------------------------------------|:----------|:------------------------------------|
| `edc.oauth.token.url` | URL of the authorization server | true | null |
| `edc.oauth.provider.audience` | Provider audience to be put in the outgoing token as 'aud' claim | false | id of the connector |
| `edc.oauth.endpoint.audience` | Endpoint audience to verify incoming token 'aud' claim | false | `edc.oauth.provider.audience` value |
| `edc.oauth.provider.jwks.url` | URL from which well-known public keys of Authorization server can be fetched | false | http://localhost/empty_jwks_url |
| `edc.oauth.public.key.alias` | Alias of public associated with client certificate | true | null |
| `edc.oauth.private.key.alias` | Alias of private key (used to sign the token) | true | null |
| `edc.oauth.provider.jwks.refresh` | Interval at which public keys are refreshed from Authorization server (in minutes) | false | 5 |
| `edc.oauth.client.id` | Public identifier of the client | true | null |
| `edc.oauth.validation.nbf.leeway` | Leeway in seconds added to current time to remedy clock skew on notBefore claim validation | false | 10 |

## Extensions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,19 @@
*/
public class Oauth2Configuration {

private final String tokenUrl;
private final String clientId;
private final PrivateKeyResolver privateKeyResolver;
private final CertificateResolver certificateResolver;
private final PublicKeyResolver identityProviderKeyResolver;
private final String privateKeyAlias;
private final String publicCertificateAlias;
private final String providerAudience;
private final int notBeforeValidationLeeway;

public Oauth2Configuration(String tokenUrl, String clientId, PrivateKeyResolver privateKeyResolver,
CertificateResolver certificateResolver, PublicKeyResolver identityProviderKeyResolver,
String privateKeyAlias, String publicCertificateAlias, String providerAudience,
int notBeforeValidationLeeway) {

this.tokenUrl = tokenUrl;
this.clientId = clientId;
this.privateKeyResolver = privateKeyResolver;
this.certificateResolver = certificateResolver;
this.identityProviderKeyResolver = identityProviderKeyResolver;
this.privateKeyAlias = privateKeyAlias;
this.publicCertificateAlias = publicCertificateAlias;
this.providerAudience = providerAudience;
this.notBeforeValidationLeeway = notBeforeValidationLeeway;
private String tokenUrl;
private String clientId;
private PrivateKeyResolver privateKeyResolver;
private CertificateResolver certificateResolver;
private PublicKeyResolver identityProviderKeyResolver;
private String privateKeyAlias;
private String publicCertificateAlias;
private String providerAudience;
private int notBeforeValidationLeeway;
private String endpointAudience;

private Oauth2Configuration() {

}

public String getTokenUrl() {
Expand Down Expand Up @@ -86,16 +75,12 @@ public int getNotBeforeValidationLeeway() {
return notBeforeValidationLeeway;
}

public String getEndpointAudience() {
return endpointAudience;
}

public static class Builder {
private String tokenUrl;
private String clientId;
private PrivateKeyResolver privateKeyResolver;
private CertificateResolver certificateResolver;
private PublicKeyResolver identityProviderKeyResolver;
private String privateKeyAlias;
private String publicCertificateAlias;
private String providerAudience;
private int notBeforeValidationLeeway;
private final Oauth2Configuration configuration = new Oauth2Configuration();

private Builder() {
}
Expand All @@ -105,60 +90,63 @@ public static Builder newInstance() {
}

public Builder tokenUrl(String url) {
this.tokenUrl = url;
configuration.tokenUrl = url;
return this;
}

public Builder clientId(String clientId) {
this.clientId = clientId;
configuration.clientId = clientId;
return this;
}

public Builder privateKeyResolver(PrivateKeyResolver privateKeyResolver) {
this.privateKeyResolver = privateKeyResolver;
configuration.privateKeyResolver = privateKeyResolver;
return this;
}

/**
* Resolves this runtime's certificate containing its public key.
*/
public Builder certificateResolver(CertificateResolver certificateResolver) {
this.certificateResolver = certificateResolver;
configuration.certificateResolver = certificateResolver;
return this;
}

/**
* Resolves the certificate containing the identity provider's public key.
*/
public Builder identityProviderKeyResolver(PublicKeyResolver identityProviderKeyResolver) {
this.identityProviderKeyResolver = identityProviderKeyResolver;
configuration.identityProviderKeyResolver = identityProviderKeyResolver;
return this;
}

public Builder privateKeyAlias(String privateKeyAlias) {
this.privateKeyAlias = privateKeyAlias;
configuration.privateKeyAlias = privateKeyAlias;
return this;
}

public Builder publicCertificateAlias(String publicCertificateAlias) {
this.publicCertificateAlias = publicCertificateAlias;
configuration.publicCertificateAlias = publicCertificateAlias;
return this;
}

public Builder providerAudience(String providerAudience) {
this.providerAudience = providerAudience;
configuration.providerAudience = providerAudience;
return this;
}

public Builder notBeforeValidationLeeway(int notBeforeValidationLeeway) {
this.notBeforeValidationLeeway = notBeforeValidationLeeway;
configuration.notBeforeValidationLeeway = notBeforeValidationLeeway;
return this;
}

public Builder endpointAudience(String endpointAudience) {
configuration.endpointAudience = endpointAudience;
return this;
}

public Oauth2Configuration build() {
return new Oauth2Configuration(tokenUrl, clientId, privateKeyResolver, certificateResolver,
identityProviderKeyResolver, privateKeyAlias, publicCertificateAlias, providerAudience,
notBeforeValidationLeeway);
return configuration;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 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.dataspaceconnector.iam.oauth2.core;

import org.eclipse.dataspaceconnector.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider;
import org.eclipse.dataspaceconnector.iam.oauth2.spi.NoopCredentialsRequestAdditionalParametersProvider;
import org.eclipse.dataspaceconnector.runtime.metamodel.annotation.Provider;
import org.eclipse.dataspaceconnector.spi.system.ServiceExtension;

/**
* Provides default service implementations for fallback
*/
public class Oauth2DefaultServicesExtension implements ServiceExtension {

@Override
public String name() {
return "OAuth2 Core Default Services";
}

@Provider(isDefault = true)
public CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider() {
return new NoopCredentialsRequestAdditionalParametersProvider();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
import okhttp3.OkHttpClient;
import org.eclipse.dataspaceconnector.core.jwt.TokenGenerationServiceImpl;
import org.eclipse.dataspaceconnector.core.jwt.TokenValidationServiceImpl;
import org.eclipse.dataspaceconnector.iam.oauth2.core.identity.CredentialsRequestAdditionalParametersProvider;
import org.eclipse.dataspaceconnector.iam.oauth2.core.identity.IdentityProviderKeyResolver;
import org.eclipse.dataspaceconnector.iam.oauth2.core.identity.IdentityProviderKeyResolverConfiguration;
import org.eclipse.dataspaceconnector.iam.oauth2.core.identity.Oauth2ServiceImpl;
import org.eclipse.dataspaceconnector.iam.oauth2.core.jwt.DefaultJwtDecorator;
import org.eclipse.dataspaceconnector.iam.oauth2.core.jwt.Oauth2JwtDecoratorRegistryRegistryImpl;
import org.eclipse.dataspaceconnector.iam.oauth2.core.rule.Oauth2ValidationRulesRegistryImpl;
import org.eclipse.dataspaceconnector.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider;
import org.eclipse.dataspaceconnector.iam.oauth2.spi.Oauth2JwtDecoratorRegistry;
import org.eclipse.dataspaceconnector.iam.oauth2.spi.Oauth2ValidationRulesRegistry;
import org.eclipse.dataspaceconnector.runtime.metamodel.annotation.EdcSetting;
Expand All @@ -36,16 +36,12 @@
import org.eclipse.dataspaceconnector.spi.security.PrivateKeyResolver;
import org.eclipse.dataspaceconnector.spi.system.ServiceExtension;
import org.eclipse.dataspaceconnector.spi.system.ServiceExtensionContext;
import org.jetbrains.annotations.NotNull;

import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import static java.util.Collections.emptyMap;

/**
* Provides OAuth2 client credentials flow support.
*/
Expand All @@ -57,9 +53,12 @@ public class Oauth2Extension implements ServiceExtension {
@EdcSetting
private static final String PROVIDER_JWKS_URL = "edc.oauth.provider.jwks.url";

@EdcSetting
@EdcSetting(value = "outgoing tokens 'aud' claim value, by default it's the connector id")
private static final String PROVIDER_AUDIENCE = "edc.oauth.provider.audience";

@EdcSetting(value = "incoming tokens 'aud' claim required value, by default it's the provider audience value")
private static final String ENDPOINT_AUDIENCE = "edc.oauth.endpoint.audience";

@EdcSetting
private static final String PUBLIC_KEY_ALIAS = "edc.oauth.public.key.alias";

Expand Down Expand Up @@ -92,7 +91,7 @@ public class Oauth2Extension implements ServiceExtension {
@Inject
private Clock clock;

@Inject(required = false)
@Inject
private CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider;

@Override
Expand Down Expand Up @@ -127,7 +126,7 @@ public void initialize(ServiceExtensionContext context) {
jwtDecoratorRegistry,
context.getTypeManager(),
new TokenValidationServiceImpl(configuration.getIdentityProviderKeyResolver(), validationRulesRegistry),
Optional.ofNullable(credentialsRequestAdditionalParametersProvider).orElse(noopCredentialsRequestAdditionalParametersProvider())
credentialsRequestAdditionalParametersProvider
);

context.registerService(IdentityService.class, oauth2Service);
Expand All @@ -143,11 +142,6 @@ public void shutdown() {
providerKeyResolver.stop();
}

@NotNull
private CredentialsRequestAdditionalParametersProvider noopCredentialsRequestAdditionalParametersProvider() {
return p -> emptyMap();
}

private byte[] getEncodedClientCertificate(Oauth2Configuration configuration) {
var certificate = configuration.getCertificateResolver().resolveCertificate(configuration.getPublicCertificateAlias());
if (certificate == null) {
Expand All @@ -163,6 +157,7 @@ private byte[] getEncodedClientCertificate(Oauth2Configuration configuration) {

private Oauth2Configuration createConfig(ServiceExtensionContext context) {
var providerAudience = context.getSetting(PROVIDER_AUDIENCE, context.getConnectorId());
var endpointAudience = context.getSetting(ENDPOINT_AUDIENCE, providerAudience);
var tokenUrl = context.getConfig().getString(TOKEN_URL);
var publicKeyAlias = context.getConfig().getString(PUBLIC_KEY_ALIAS);
var privateKeyAlias = context.getConfig().getString(PRIVATE_KEY_ALIAS);
Expand All @@ -171,6 +166,7 @@ private Oauth2Configuration createConfig(ServiceExtensionContext context) {
.identityProviderKeyResolver(providerKeyResolver)
.tokenUrl(tokenUrl)
.providerAudience(providerAudience)
.endpointAudience(endpointAudience)
.publicCertificateAlias(publicKeyAlias)
.privateKeyAlias(privateKeyAlias)
.clientId(clientId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.eclipse.dataspaceconnector.iam.oauth2.core.Oauth2Configuration;
import org.eclipse.dataspaceconnector.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider;
import org.eclipse.dataspaceconnector.spi.EdcException;
import org.eclipse.dataspaceconnector.spi.iam.ClaimToken;
import org.eclipse.dataspaceconnector.spi.iam.IdentityService;
Expand Down
Loading