Skip to content

Commit

Permalink
feat: OAuth2 endpoint audience (#2000)
Browse files Browse the repository at this point in the history
* Move interface to spi

* Add enpoint audience configuration setting

* Refactor

* Introduce oauth2 default providers

* PR remark

* PR remarks
  • Loading branch information
ndr-brt committed Sep 26, 2022
1 parent d194b9a commit a862c02
Show file tree
Hide file tree
Showing 25 changed files with 599 additions and 387 deletions.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.rule;

import org.eclipse.dataspaceconnector.spi.iam.ClaimToken;
import org.eclipse.dataspaceconnector.spi.jwt.TokenValidationRule;
import org.eclipse.dataspaceconnector.spi.result.Result;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Map;

import static org.eclipse.dataspaceconnector.spi.jwt.JwtRegisteredClaimNames.AUDIENCE;

/**
* Token validation rule that checks if the "audience" of token contains the expected audience
*/
public class Oauth2AudienceValidationRule implements TokenValidationRule {

private final String endpointAudience;

public Oauth2AudienceValidationRule(String endpointAudience) {
this.endpointAudience = endpointAudience;
}

@Override
public Result<Void> checkRule(@NotNull ClaimToken toVerify, @Nullable Map<String, Object> additional) {
var audiences = toVerify.getListClaim(AUDIENCE);
if (audiences.isEmpty()) {
return Result.failure("Required audience (aud) claim is missing in token");
} else if (!audiences.contains(endpointAudience)) {
return Result.failure("Token audience (aud) claim did not contain connector audience: " + endpointAudience);
}

return Result.success();
}
}
Loading

0 comments on commit a862c02

Please sign in to comment.