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: Add resource parameter to the OAuth2 token request to follow RFC-8707 #4680

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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
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,16 @@ 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 +91,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;
}

}
}
Loading