diff --git a/extensions/common/iam/oauth2/oauth2-core/README.md b/extensions/common/iam/oauth2/oauth2-core/README.md index e98bf54e209..51dc5ee866a 100644 --- a/extensions/common/iam/oauth2/oauth2-core/README.md +++ b/extensions/common/iam/oauth2/oauth2-core/README.md @@ -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 diff --git a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java index f28200348b5..1a9d1fbcb96 100644 --- a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java +++ b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java @@ -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() { } @@ -92,6 +95,10 @@ public Long getTokenExpiration() { return tokenExpiration; } + public boolean isTokenResourceEnabled() { + return tokenResourceEnabled; + } + public int getProviderJwksRefresh() { return providerJwksRefresh; } @@ -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; } diff --git a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java index 94aa811778a..3cb935998c7 100644 --- a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java +++ b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java @@ -159,7 +159,8 @@ private Oauth2ServiceImpl createOauth2Service(Oauth2ServiceConfiguration configu jwtDecoratorRegistry, tokenValidationRulesRegistry, tokenValidationService, - providerKeyResolver + providerKeyResolver, + configuration.isTokenResourceEnabled() ); } diff --git a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java index 6c0eb800e38..3f86b3f65c0 100644 --- a/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java +++ b/extensions/common/iam/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java @@ -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; @@ -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 @@ -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 privateKeyIdSupplier, Oauth2Client client, TokenDecoratorRegistry jwtDecoratorRegistry, TokenValidationRulesRegistry tokenValidationRuleRegistry, TokenValidationService tokenValidationService, - PublicKeyResolver publicKeyResolver) { + PublicKeyResolver publicKeyResolver, boolean tokenResourceEnabled) { this.tokenUrl = tokenUrl; this.privateKeySupplier = privateKeyIdSupplier; this.client = client; @@ -75,6 +78,7 @@ public Oauth2ServiceImpl(String tokenUrl, TokenGenerationService tokenGeneration this.tokenGenerationService = tokenGenerationService; this.tokenValidationService = tokenValidationService; this.publicKeyResolver = publicKeyResolver; + this.tokenResourceEnabled = tokenResourceEnabled; } @Override @@ -98,12 +102,16 @@ private Result 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(); } } diff --git a/extensions/common/iam/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java b/extensions/common/iam/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java index ec09c37f048..c30c6837184 100644 --- a/extensions/common/iam/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java +++ b/extensions/common/iam/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java @@ -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(); @@ -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()); } @@ -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"); } diff --git a/spi/common/oauth2-spi/src/main/java/org/eclipse/edc/iam/oauth2/spi/client/Oauth2CredentialsRequest.java b/spi/common/oauth2-spi/src/main/java/org/eclipse/edc/iam/oauth2/spi/client/Oauth2CredentialsRequest.java index 19e02050842..d24486a2f4f 100644 --- a/spi/common/oauth2-spi/src/main/java/org/eclipse/edc/iam/oauth2/spi/client/Oauth2CredentialsRequest.java +++ b/spi/common/oauth2-spi/src/main/java/org/eclipse/edc/iam/oauth2/spi/client/Oauth2CredentialsRequest.java @@ -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 params = new HashMap<>(); @@ -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 getParams() { return params; } @@ -80,6 +91,17 @@ public B params(Map params) { return self(); } + /** + * Adds the resource form parameter to the request. + * + * @param targetedAudience The audience for which an access token will be requested. + * @see RFC-8707 + * @return this builder + */ + public B resource(String targetedAudience) { + return param(RESOURCE, targetedAudience); + } + public abstract B self(); protected T build() { @@ -87,5 +109,6 @@ protected T build() { Objects.requireNonNull(request.params.get(GRANT_TYPE), GRANT_TYPE); return request; } + } }