Skip to content

Commit

Permalink
feat: add resource parameter to the OAuth2 token request to follow RF…
Browse files Browse the repository at this point in the history
…C-8707
  • Loading branch information
scandinave committed Dec 18, 2024
1 parent abdc001 commit 5788b8a
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 17 deletions.
23 changes: 12 additions & 11 deletions extensions/common/iam/oauth2/oauth2-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ 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 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.certificate.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.certificate.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 |
| `edc.oauth.token.resource.enabled` | Adds `resource` form parameter in the access token request. Allows to specify an audience as defined in the RFC-8707 | false | false |

## Extensions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public class Oauth2ServiceConfiguration {
@Setting(description = "Token expiration in minutes. By default is 5 minutes", key = "edc.oauth.token.expiration", defaultValue = DEFAULT_TOKEN_EXPIRATION + "")
private Long tokenExpiration;

@Setting(description = "Enable the connector to request a token with a specific audience as defined in the RFC-8707.", key = "edc.oauth.token.resource.enabled", defaultValue = "false")
private boolean tokenResourceEnabled;

private Oauth2ServiceConfiguration() {

}
Expand Down Expand Up @@ -92,6 +95,10 @@ public Long getTokenExpiration() {
return tokenExpiration;
}

public boolean isTokenResourceEnabled() {
return tokenResourceEnabled;
}

public int getProviderJwksRefresh() {
return providerJwksRefresh;
}
Expand Down Expand Up @@ -161,6 +168,11 @@ public Builder tokenExpiration(long tokenExpiration) {
return this;
}

public Builder tokenResourceEnabled(boolean tokenResourceEnabled) {
configuration.tokenResourceEnabled = tokenResourceEnabled;
return this;
}

public Oauth2ServiceConfiguration build() {
return configuration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ private Oauth2ServiceImpl createOauth2Service(Oauth2ServiceConfiguration configu
jwtDecoratorRegistry,
tokenValidationRulesRegistry,
tokenValidationService,
providerKeyResolver
providerKeyResolver,
configuration.isTokenResourceEnabled()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client;
import org.eclipse.edc.iam.oauth2.spi.client.Oauth2CredentialsRequest;
import org.eclipse.edc.iam.oauth2.spi.client.PrivateKeyOauth2CredentialsRequest;
import org.eclipse.edc.iam.oauth2.spi.client.PrivateKeyOauth2CredentialsRequest.Builder;
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
import org.eclipse.edc.keys.spi.PublicKeyResolver;
import org.eclipse.edc.spi.iam.ClaimToken;
Expand Down Expand Up @@ -54,6 +55,7 @@ public class Oauth2ServiceImpl implements IdentityService {
private final TokenValidationService tokenValidationService;
private final PublicKeyResolver publicKeyResolver;
private final TokenValidationRulesRegistry tokenValidationRuleRegistry;
private final boolean tokenResourceEnabled;

/**
* Creates a new instance of the OAuth2 Service
Expand All @@ -63,10 +65,11 @@ public class Oauth2ServiceImpl implements IdentityService {
* @param client client for Oauth2 server
* @param jwtDecoratorRegistry Registry containing the decorator for build the JWT
* @param tokenValidationService Service used for token validation
* @param tokenResourceEnabled Add support for generating access token request with resource parameter
*/
public Oauth2ServiceImpl(String tokenUrl, TokenGenerationService tokenGenerationService, Supplier<String> privateKeyIdSupplier,
Oauth2Client client, TokenDecoratorRegistry jwtDecoratorRegistry, TokenValidationRulesRegistry tokenValidationRuleRegistry, TokenValidationService tokenValidationService,
PublicKeyResolver publicKeyResolver) {
PublicKeyResolver publicKeyResolver, boolean tokenResourceEnabled) {
this.tokenUrl = tokenUrl;
this.privateKeySupplier = privateKeyIdSupplier;
this.client = client;
Expand All @@ -75,6 +78,7 @@ public Oauth2ServiceImpl(String tokenUrl, TokenGenerationService tokenGeneration
this.tokenGenerationService = tokenGenerationService;
this.tokenValidationService = tokenValidationService;
this.publicKeyResolver = publicKeyResolver;
this.tokenResourceEnabled = tokenResourceEnabled;
}

@Override
Expand All @@ -98,12 +102,16 @@ private Result<String> generateClientAssertion() {

@NotNull
private Oauth2CredentialsRequest createRequest(TokenParameters parameters, String assertion) {
return PrivateKeyOauth2CredentialsRequest.Builder.newInstance()
PrivateKeyOauth2CredentialsRequest.Builder<?> builder = Builder.newInstance()
.url(tokenUrl)
.clientAssertion(assertion)
.scope(parameters.getStringClaim(JwtRegisteredClaimNames.SCOPE))
.grantType(GRANT_TYPE)
.build();
.grantType(GRANT_TYPE);

if (tokenResourceEnabled) {
builder.resource(parameters.getStringClaim(JwtRegisteredClaimNames.AUDIENCE));
}
return builder.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ void setUp() throws JOSEException {
.publicCertificateAlias(PUBLIC_CERTIFICATE_ALIAS)
.providerAudience(PROVIDER_AUDIENCE)
.endpointAudience(ENDPOINT_AUDIENCE)
.tokenResourceEnabled(true)
.build();

var tokenValidationService = new TokenValidationServiceImpl();
Expand All @@ -114,7 +115,7 @@ void setUp() throws JOSEException {
registry.addRule(OAUTH2_TOKEN_CONTEXT, new ExpirationIssuedAtValidationRule(Clock.systemUTC(), configuration.getIssuedAtLeeway()));

authService = new Oauth2ServiceImpl(configuration.getTokenUrl(), tokenGenerationService, () -> TEST_PRIVATE_KEY_ID, client, jwtDecoratorRegistry, registry,
tokenValidationService, publicKeyResolverMock);
tokenValidationService, publicKeyResolverMock, configuration.isTokenResourceEnabled());

}

Expand Down Expand Up @@ -143,6 +144,7 @@ void obtainClientCredentials() {
assertThat(capturedRequest.getScope()).isEqualTo("scope");
assertThat(capturedRequest.getClientAssertion()).isEqualTo("assertionToken");
assertThat(capturedRequest.getClientAssertionType()).isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
assertThat(capturedRequest.getResource()).isEqualTo("audience");
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright (c) 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:
* Amadeus - initial API and implementation
* Microsoft Corporation - Use IDS Webhook address for JWT audience claim
*
*/

package org.eclipse.edc.iam.oauth2.daps;

import org.eclipse.edc.junit.annotations.ComponentTest;
import org.eclipse.edc.junit.extensions.EdcExtension;
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
import org.eclipse.edc.policy.model.Policy;
import org.eclipse.edc.spi.iam.IdentityService;
import org.eclipse.edc.spi.iam.TokenParameters;
import org.eclipse.edc.spi.iam.VerificationContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;

import java.nio.file.Path;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.junit.testfixtures.TestUtils.findBuildRoot;

@ExtendWith(EdcExtension.class)
@ComponentTest
public class Rfc8707IntegrationTest {

public static final String CLIENT_CERTIFICATE_ALIAS = "1";
public static final String CLIENT_PRIVATE_KEY_ALIAS = "2";
private static final String AUDIENCE_IDS_CONNECTORS_ALL = "idsc:IDS_CONNECTORS_ALL";
private static final String AUDIENCE_CONNECTOR = "audience";
private static final String CLIENT_ID = "68:99:2E:D4:13:2D:FD:3A:66:6B:85:DE:FB:98:2E:2D:FD:E7:83:D7";
private static final String CLIENT_KEYSTORE_PASSWORD = "1234";

private final Path resourceFolder = findBuildRoot().toPath().resolve("extensions/common/iam/oauth2/oauth2-daps/src/test/resources");

@Container
private final GenericContainer<?> daps = new GenericContainer<>("ghcr.io/fraunhofer-aisec/omejdn-server:1.4.2")
.withExposedPorts(4567)
.withFileSystemBind(resourceFolder.resolve("config").toString(), "/opt/config")
.withFileSystemBind(resourceFolder.resolve("keys").toString(), "/opt/keys");

/**
* Verify that a connector with support for the RFC-8707 is able to set its audience via resource parameter.
*
* @param identityService The identity service used to obtains credentials
*/
@Test
void retrieveTokenAndValidate(IdentityService identityService) {
var tokenParameters = TokenParameters.Builder.newInstance()
.claims(JwtRegisteredClaimNames.SCOPE, "idsc:IDS_CONNECTOR_ATTRIBUTES_ALL")
.claims(JwtRegisteredClaimNames.AUDIENCE, AUDIENCE_CONNECTOR)
.build();
var tokenResult = identityService.obtainClientCredentials(tokenParameters);

assertThat(tokenResult.succeeded()).withFailMessage(tokenResult::getFailureDetail).isTrue();

var verificationContext = VerificationContext.Builder.newInstance()
.policy(Policy.Builder.newInstance().build())
.build();

var verificationResult = identityService.verifyJwtToken(tokenResult.getContent(), verificationContext);

assertThat(verificationResult.succeeded()).withFailMessage(verificationResult::getFailureDetail).isTrue();
}

@BeforeEach
protected void before(EdcExtension extension) {
System.setProperty("edc.keystore", "src/test/resources/keystore.p12");
System.setProperty("edc.keystore.password", CLIENT_KEYSTORE_PASSWORD);

var jwksPath = "/.well-known/jwks.json";
daps.waitingFor(Wait.forHttp(jwksPath)).start();

var dapsUrl = "http://%s:%s".formatted(daps.getHost(), daps.getFirstMappedPort());

extension.setConfiguration(Map.of(
"edc.oauth.token.url", dapsUrl + "/token",
"edc.oauth.client.id", CLIENT_ID,
"edc.oauth.provider.audience", AUDIENCE_IDS_CONNECTORS_ALL,
"edc.oauth.endpoint.audience", AUDIENCE_CONNECTOR,
"edc.oauth.provider.jwks.url", dapsUrl + jwksPath,
"edc.oauth.certificate.alias", CLIENT_CERTIFICATE_ALIAS,
"edc.oauth.private.key.alias", CLIENT_PRIVATE_KEY_ALIAS,
"edc.iam.token.scope", "idsc:IDS_CONNECTOR_ATTRIBUTES_ALL",
"edc.oauth.token.resource.enable", "true"
));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public abstract class Oauth2CredentialsRequest {

private static final String GRANT_TYPE = "grant_type";
private static final String SCOPE = "scope";
private static final String RESOURCE = "resource";

protected String url;
protected final Map<String, String> params = new HashMap<>();
Expand All @@ -44,6 +45,15 @@ public String getGrantType() {
return params.get(GRANT_TYPE);
}

/**
* The audience for which an access token will be requested.
* @return The value of the resource form parameter.
*/
@Nullable
public String getResource() {
return this.params.get(RESOURCE);
}

public Map<String, String> getParams() {
return params;
}
Expand Down Expand Up @@ -80,12 +90,24 @@ public B params(Map<String, String> params) {
return self();
}

/**
* Adds the resource form parameter to the request.
*
* @param targetedAudience The audience for which an access token will be requested.
* @see <a href="https://www.rfc-editor.org/rfc/rfc8707.html">RFC-8707</a>
* @return this builder
*/
public B resource(String targetedAudience) {
return param(RESOURCE, targetedAudience);
}

public abstract B self();

protected T build() {
Objects.requireNonNull(request.url, "url");
Objects.requireNonNull(request.params.get(GRANT_TYPE), GRANT_TYPE);
return request;
}

}
}

0 comments on commit 5788b8a

Please sign in to comment.