Skip to content

Commit

Permalink
feat: add scope parameter to oauth2 provisioner (#2407)
Browse files Browse the repository at this point in the history
feat(oauth2-provisioner): add scope parameter
  • Loading branch information
ndr-brt authored Jan 13, 2023
1 parent bcf7c87 commit cc5b348
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ private static Request toRequest(Oauth2CredentialsRequest request) {

private static FormBody createRequestBody(Oauth2CredentialsRequest request) {
var builder = new FormBody.Builder();
request.getParams().forEach(builder::add);
request.getParams().entrySet().stream()
.filter(entry -> entry.getValue() != null)
.forEach(entry -> builder.add(entry.getKey(), entry.getValue()));
return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ The extension works for all the `HttpData` addresses that contain the "oauth2" p
It supports [both types of client credential](https://connect2id.com/products/server/docs/guides/oauth-client-authentication#credential-types):
shared secret and private-key based.

### Common properties

- `oauth2:tokenUrl`: the url where the token will be requested
- `oauth2:scope`: (optional) the requested scope

### Private-key based client credential

This type of client credential is used when the `HttpData` address contains the `oauth2:privateKeyName` property. This type of client
credential is considered as more secured as described [here](https://connect2id.com/products/server/docs/guides/oauth-client-authentication#private-key-auth-is-more-secure).
The mandatory for working with type of client credentials are:

- `oauth2:clientId`: the client id
- `oauth2:tokenUrl`: the url where the token will be requested
- `oauth2:privateKeyName`: the name of the private key used to sign the JWT sent to the Oauth2 server
`oauth2:validity`: the validity of the JWT token sent to the Oauth2 server (in seconds)

Expand All @@ -46,6 +49,5 @@ This type of client credential is used when the `HttpData` address DOES not cont
The mandatory for working with type of client credentials are:

- `oauth2:clientId`: the client id
- `oauth2:tokenUrl`: the url where the token will be requested
- `oauth2:clientSecret`: shared secret for authenticating to the Oauth2 server

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2023 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.edc.connector.provision.oauth2;

import org.eclipse.edc.iam.oauth2.spi.Oauth2AssertionDecorator;
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.SharedSecretOauth2CredentialsRequest;
import org.eclipse.edc.jwt.TokenGenerationServiceImpl;
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.jetbrains.annotations.NotNull;

import java.security.PrivateKey;
import java.time.Clock;

/**
* Factory class that provides methods to build {@link Oauth2CredentialsRequest} instances
*/
public class Oauth2CredentialsRequestFactory {

private static final String GRANT_CLIENT_CREDENTIALS = "client_credentials";
private final PrivateKeyResolver privateKeyResolver;
private final Clock clock;

public Oauth2CredentialsRequestFactory(PrivateKeyResolver privateKeyResolver, Clock clock) {
this.privateKeyResolver = privateKeyResolver;
this.clock = clock;
}

/**
* Create an {@link Oauth2CredentialsRequest} given a {@link Oauth2ResourceDefinition}
*
* @param resourceDefinition the resource definition
* @return a {@link Result} containing the {@link Oauth2CredentialsRequest} object
*/
public Result<Oauth2CredentialsRequest> create(Oauth2ResourceDefinition resourceDefinition) {
var keySecret = resourceDefinition.getPrivateKeyName();
return keySecret != null
? createPrivateKeyBasedRequest(keySecret, resourceDefinition)
: createSharedSecretRequest(resourceDefinition);
}

@NotNull
private Result<Oauth2CredentialsRequest> createPrivateKeyBasedRequest(String pkSecret, Oauth2ResourceDefinition resourceDefinition) {
return createAssertion(pkSecret, resourceDefinition)
.map(assertion -> PrivateKeyOauth2CredentialsRequest.Builder.newInstance()
.clientAssertion(assertion.getToken())
.url(resourceDefinition.getTokenUrl())
.grantType(GRANT_CLIENT_CREDENTIALS)
.scope(resourceDefinition.getScope())
.build());
}

@NotNull
private Result<Oauth2CredentialsRequest> createSharedSecretRequest(Oauth2ResourceDefinition resourceDefinition) {
return Result.success(SharedSecretOauth2CredentialsRequest.Builder.newInstance()
.url(resourceDefinition.getTokenUrl())
.grantType(GRANT_CLIENT_CREDENTIALS)
.clientId(resourceDefinition.getClientId())
.clientSecret(resourceDefinition.getClientSecret())
.scope(resourceDefinition.getScope())
.build());
}

@NotNull
private Result<TokenRepresentation> createAssertion(String pkSecret, Oauth2ResourceDefinition resourceDefinition) {
var privateKey = privateKeyResolver.resolvePrivateKey(pkSecret, PrivateKey.class);
if (privateKey == null) {
return Result.failure("Failed to resolve private key with alias: " + pkSecret);
}
var decorator = new Oauth2AssertionDecorator(resourceDefinition.getTokenUrl(), resourceDefinition.getClientId(), clock, resourceDefinition.getValidity());
var service = new TokenGenerationServiceImpl(privateKey);
return service.generate(decorator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ public interface Oauth2DataAddressSchema {
String TOKEN_URL = "oauth2:tokenUrl";
String VALIDITY = "oauth2:validity";
String PRIVATE_KEY_NAME = "oauth2:privateKeyName";
String SCOPE = "oauth2:scope";
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public void initialize(ServiceExtensionContext context) {
resourceManifestGenerator.registerGenerator(new Oauth2ProviderResourceDefinitionGenerator());
resourceManifestGenerator.registerGenerator(new Oauth2ConsumerResourceDefinitionGenerator());

provisionManager.register(new Oauth2Provisioner(client, privateKeyResolver, clock));
var requestFactory = new Oauth2CredentialsRequestFactory(privateKeyResolver, clock);
provisionManager.register(new Oauth2Provisioner(client, requestFactory));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,11 @@
import org.eclipse.edc.connector.transfer.spi.types.ProvisionResponse;
import org.eclipse.edc.connector.transfer.spi.types.ProvisionedResource;
import org.eclipse.edc.connector.transfer.spi.types.ResourceDefinition;
import org.eclipse.edc.iam.oauth2.spi.Oauth2AssertionDecorator;
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.SharedSecretOauth2CredentialsRequest;
import org.eclipse.edc.jwt.TokenGenerationServiceImpl;
import org.eclipse.edc.policy.model.Policy;
import org.eclipse.edc.spi.iam.TokenRepresentation;
import org.eclipse.edc.spi.response.StatusResult;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.eclipse.edc.spi.types.domain.HttpDataAddress;
import org.jetbrains.annotations.NotNull;

import java.security.PrivateKey;
import java.time.Clock;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

Expand All @@ -46,16 +35,12 @@
*/
class Oauth2Provisioner implements Provisioner<Oauth2ResourceDefinition, Oauth2ProvisionedResource> {

private static final String GRANT_CLIENT_CREDENTIALS = "client_credentials";

private final Oauth2Client client;
private final PrivateKeyResolver privateKeyResolver;
private final Clock clock;
private final Oauth2CredentialsRequestFactory requestFactory;

Oauth2Provisioner(Oauth2Client client, PrivateKeyResolver privateKeyResolver, Clock clock) {
Oauth2Provisioner(Oauth2Client client, Oauth2CredentialsRequestFactory requestFactory) {
this.client = client;
this.privateKeyResolver = privateKeyResolver;
this.clock = clock;
this.requestFactory = requestFactory;
}

@Override
Expand All @@ -70,7 +55,7 @@ public boolean canDeprovision(ProvisionedResource resourceDefinition) {

@Override
public CompletableFuture<StatusResult<ProvisionResponse>> provision(Oauth2ResourceDefinition resourceDefinition, Policy policy) {
var request = createRequest(resourceDefinition);
var request = requestFactory.create(resourceDefinition);
if (request.failed()) {
return completedFuture(StatusResult.failure(FATAL_ERROR, request.getFailureDetail()));
}
Expand Down Expand Up @@ -109,39 +94,4 @@ public CompletableFuture<StatusResult<DeprovisionedResource>> deprovision(Oauth2
return completedFuture(StatusResult.success(deprovisionedResource));
}

@NotNull
private Result<Oauth2CredentialsRequest> createRequest(Oauth2ResourceDefinition rd) {
var keySecret = rd.getPrivateKeyName();
return keySecret != null ? createPrivateKeyBasedRequest(keySecret, rd) : createSharedSecretRequest(rd);
}

@NotNull
private Result<Oauth2CredentialsRequest> createPrivateKeyBasedRequest(String pkSecret, Oauth2ResourceDefinition rd) {
return createAssertion(pkSecret, rd)
.map(assertion -> PrivateKeyOauth2CredentialsRequest.Builder.newInstance()
.clientAssertion(assertion.getToken())
.url(rd.getTokenUrl())
.grantType(GRANT_CLIENT_CREDENTIALS)
.build());
}

@NotNull
private Result<Oauth2CredentialsRequest> createSharedSecretRequest(Oauth2ResourceDefinition rd) {
return Result.success(SharedSecretOauth2CredentialsRequest.Builder.newInstance()
.url(rd.getTokenUrl())
.grantType(GRANT_CLIENT_CREDENTIALS)
.clientId(rd.getClientId())
.clientSecret(rd.getClientSecret())
.build());
}

private Result<TokenRepresentation> createAssertion(String pkSecret, Oauth2ResourceDefinition rd) {
var privateKey = privateKeyResolver.resolvePrivateKey(pkSecret, PrivateKey.class);
if (privateKey == null) {
return Result.failure("Failed to resolve private key with alias: " + pkSecret);
}
var decorator = new Oauth2AssertionDecorator(rd.getTokenUrl(), rd.getClientId(), clock, rd.getValidity());
var service = new TokenGenerationServiceImpl(privateKey);
return service.generate(decorator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.CLIENT_ID;
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.CLIENT_SECRET;
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.PRIVATE_KEY_NAME;
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.SCOPE;
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.TOKEN_URL;
import static org.eclipse.edc.connector.provision.oauth2.Oauth2DataAddressSchema.VALIDITY;

Expand Down Expand Up @@ -70,6 +71,11 @@ public String getPrivateKeyName() {
return dataAddress.getProperty(PRIVATE_KEY_NAME);
}

@Nullable
public String getScope() {
return dataAddress.getProperty(SCOPE);
}

public long getValidity() {
return Optional.ofNullable(dataAddress.getProperty(VALIDITY))
.map(Long::parseLong)
Expand Down
Loading

0 comments on commit cc5b348

Please sign in to comment.