From 38635e7a4b8f01be2687ac1193dab412f204b774 Mon Sep 17 00:00:00 2001 From: Julian L Date: Mon, 9 Sep 2024 15:33:14 +0200 Subject: [PATCH] refactor: blockchain token validation reworked (#15) --- .../blockchain-catalog-api/build.gradle.kts | 2 +- .../api/BlockchainCatalogApiController.java | 24 +- .../BlockchainSmartContractService.java | 465 ++++++------------ .../model/TokenizedPolicyDefinition.java | 5 +- .../listener/model/TokenziedAsset.java | 114 +---- extensions/helper/build.gradle.kts | 25 + .../ise/extension/helper/HelperExtension.java | 30 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 1 + .../oauth2/oauth2-client/build.gradle.kts | 24 + .../edc/iam/oauth2/Oauth2ClientExtension.java | 47 ++ .../iam/oauth2/client/Oauth2ClientImpl.java | 93 ++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../oauth2/client/Oauth2ClientImplTest.java | 104 ++++ external/oauth2/oauth2-core/README.md | 24 + external/oauth2/oauth2-core/build.gradle.kts | 36 ++ .../oauth2/Oauth2ServiceConfiguration.java | 163 ++++++ ...Oauth2ServiceDefaultServicesExtension.java | 41 ++ .../iam/oauth2/Oauth2ServiceExtension.java | 174 +++++++ .../identity/IdentityProviderKeyResolver.java | 167 +++++++ ...ntityProviderKeyResolverConfiguration.java | 33 ++ .../oauth2/identity/Oauth2ServiceImpl.java | 101 ++++ .../edc/iam/oauth2/jwt/Fingerprint.java | 62 +++ .../eclipse/edc/iam/oauth2/jwt/JwkKey.java | 95 ++++ .../eclipse/edc/iam/oauth2/jwt/JwkKeys.java | 35 ++ ...auth2JwtDecoratorRegistryRegistryImpl.java | 24 + .../oauth2/jwt/X509CertificateDecorator.java | 51 ++ .../rule/Oauth2AudienceValidationRule.java | 49 ++ ...auth2ExpirationIssuedAtValidationRule.java | 62 +++ .../rule/Oauth2NotBeforeValidationRule.java | 54 ++ .../Oauth2ValidationRulesRegistryImpl.java | 33 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 16 + .../oauth2/Oauth2ServiceExtensionTest.java | 72 +++ .../IdentityProviderKeyResolverTest.java | 121 +++++ .../identity/Oauth2ServiceImplTest.java | 271 ++++++++++ .../edc/iam/oauth2/jwt/FingerprintTest.java | 64 +++ .../jwt/X509CertificateDecoratorTest.java | 51 ++ .../Oauth2AudienceValidationRuleTest.java | 68 +++ ...2ExpirationIssuedAtValidationRuleTest.java | 100 ++++ .../Oauth2NotBeforeValidationRuleTest.java | 73 +++ .../oauth2-core/src/test/resources/cert.pem | 13 + .../src/test/resources/jwks_response.json | 64 +++ external/oauth2/oauth2-daps/README.md | 8 + external/oauth2/oauth2-daps/build.gradle.kts | 28 ++ .../edc/iam/oauth2/daps/DapsExtension.java | 67 +++ .../edc/iam/oauth2/daps/DapsJwtDecorator.java | 37 ++ .../iam/oauth2/daps/DapsTokenDecorator.java | 37 ++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 + .../iam/oauth2/daps/DapsIntegrationTest.java | 73 +++ .../iam/oauth2/daps/DapsJwtDecoratorTest.java | 40 ++ .../oauth2/daps/DapsTokenDecoratorTest.java | 38 ++ .../iam/oauth2/daps/annotations/DapsTest.java | 33 ++ .../src/test/resources/config/clients.yml | 11 + .../src/test/resources/config/omejdn.yml | 18 + .../test/resources/config/scope_mapping.yml | 3 + .../src/test/resources/empty-vault.properties | 0 ...6REU6RkI6OTg6MkU6MkQ6RkQ6RTc6ODM6RDc=.cert | 21 + .../src/test/resources/keys/signing_key.pem | 28 ++ .../src/test/resources/keystore.p12 | Bin 0 -> 2477 bytes external/oauth2/oauth2-service/README.md | 7 + .../oauth2/oauth2-service/build.gradle.kts | 24 + gradle/libs.versions.toml | 8 +- launchers/azure/build.gradle.kts | 3 + launchers/edc-tu-berlin/build.gradle.kts | 12 +- launchers/edc-tu-berlin/config.properties | 7 + launchers/edc-tu-berlin/config2.properties | 21 +- settings.gradle.kts | 4 + 66 files changed, 3176 insertions(+), 433 deletions(-) create mode 100644 extensions/helper/build.gradle.kts create mode 100644 extensions/helper/src/main/java/berlin/tu/ise/extension/helper/HelperExtension.java create mode 100644 extensions/helper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 external/oauth2/oauth2-client/build.gradle.kts create mode 100644 external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ClientExtension.java create mode 100644 external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImpl.java create mode 100644 external/oauth2/oauth2-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 external/oauth2/oauth2-client/src/test/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImplTest.java create mode 100644 external/oauth2/oauth2-core/README.md create mode 100644 external/oauth2/oauth2-core/build.gradle.kts create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceDefaultServicesExtension.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolver.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverConfiguration.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Fingerprint.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKey.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKeys.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Oauth2JwtDecoratorRegistryRegistryImpl.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecorator.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRule.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRule.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRule.java create mode 100644 external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ValidationRulesRegistryImpl.java create mode 100644 external/oauth2/oauth2-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtensionTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/FingerprintTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecoratorTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRuleTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRuleTest.java create mode 100644 external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRuleTest.java create mode 100644 external/oauth2/oauth2-core/src/test/resources/cert.pem create mode 100644 external/oauth2/oauth2-core/src/test/resources/jwks_response.json create mode 100644 external/oauth2/oauth2-daps/README.md create mode 100644 external/oauth2/oauth2-daps/build.gradle.kts create mode 100644 external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsExtension.java create mode 100644 external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecorator.java create mode 100644 external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecorator.java create mode 100644 external/oauth2/oauth2-daps/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsIntegrationTest.java create mode 100644 external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecoratorTest.java create mode 100644 external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecoratorTest.java create mode 100644 external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/annotations/DapsTest.java create mode 100644 external/oauth2/oauth2-daps/src/test/resources/config/clients.yml create mode 100644 external/oauth2/oauth2-daps/src/test/resources/config/omejdn.yml create mode 100644 external/oauth2/oauth2-daps/src/test/resources/config/scope_mapping.yml create mode 100644 external/oauth2/oauth2-daps/src/test/resources/empty-vault.properties create mode 100644 external/oauth2/oauth2-daps/src/test/resources/keys/Njg6OTk6MkU6RDQ6MTM6MkQ6RkQ6M0E6NjY6NkI6ODU6REU6RkI6OTg6MkU6MkQ6RkQ6RTc6ODM6RDc=.cert create mode 100644 external/oauth2/oauth2-daps/src/test/resources/keys/signing_key.pem create mode 100644 external/oauth2/oauth2-daps/src/test/resources/keystore.p12 create mode 100644 external/oauth2/oauth2-service/README.md create mode 100644 external/oauth2/oauth2-service/build.gradle.kts diff --git a/extensions/blockchain/blockchain-catalog-api/build.gradle.kts b/extensions/blockchain/blockchain-catalog-api/build.gradle.kts index 5cb9c80..a186207 100644 --- a/extensions/blockchain/blockchain-catalog-api/build.gradle.kts +++ b/extensions/blockchain/blockchain-catalog-api/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation(libs.edc.http) implementation(libs.edc.configuration.filesystem) - implementation(libs.edc.iam.mock) + //implementation(libs.edc.iam.mock) implementation(libs.edc.auth.tokenbased) implementation(libs.edc.management.api) diff --git a/extensions/blockchain/blockchain-catalog-api/src/main/java/berlin/tu/ise/extension/blockchain/catalog/api/BlockchainCatalogApiController.java b/extensions/blockchain/blockchain-catalog-api/src/main/java/berlin/tu/ise/extension/blockchain/catalog/api/BlockchainCatalogApiController.java index eaaf28d..7e4a1e7 100644 --- a/extensions/blockchain/blockchain-catalog-api/src/main/java/berlin/tu/ise/extension/blockchain/catalog/api/BlockchainCatalogApiController.java +++ b/extensions/blockchain/blockchain-catalog-api/src/main/java/berlin/tu/ise/extension/blockchain/catalog/api/BlockchainCatalogApiController.java @@ -131,23 +131,23 @@ public String getCatalog(FederatedCatalogCacheQuery federatedCatalogCacheQuery) // iterate over all sources of contractDefinitionResponseDtoGroupedBySource and fetch all contracts for each source + var extendedDebugging = false; - - monitor.debug("-------------------------------------------------"); + if (extendedDebugging) monitor.debug("-------------------------------------------------"); for (Asset asset : assetList) { - monitor.debug("Asset: " + asset.getId()); - monitor.debug("AssetName: " + asset.getName()); - monitor.debug("AssetProperties: " + asset.getProperties().toString()); - monitor.debug("AssetDataAddress: " + asset.getDataAddress().toString()); + if (extendedDebugging) monitor.debug("Asset: " + asset.getId()); + if (extendedDebugging) monitor.debug("AssetName: " + asset.getName()); + if (extendedDebugging) monitor.debug("AssetProperties: " + asset.getProperties().toString()); + if (extendedDebugging) monitor.debug("AssetDataAddress: " + asset.getDataAddress().toString()); } for (PolicyDefinition policyDefinition : policyDefinitionList) { - monitor.debug("Policy: " + policyDefinition.getId()); - monitor.debug("PolicyTarget: " + policyDefinition.getPolicy().getTarget()); + if (extendedDebugging) monitor.debug("Policy: " + policyDefinition.getId()); + if (extendedDebugging) monitor.debug("PolicyTarget: " + policyDefinition.getPolicy().getTarget()); } for (ContractDefinition contractDefinition : contractDefinitionList) { - monitor.debug("Contract: " + contractDefinition.getId()); - monitor.debug("ContractPolicyId: " + contractDefinition.getContractPolicyId()); - monitor.debug("ContractCriteria: " + contractDefinition.getAccessPolicyId()); + if (extendedDebugging) monitor.debug("Contract: " + contractDefinition.getId()); + if (extendedDebugging) monitor.debug("ContractPolicyId: " + contractDefinition.getContractPolicyId()); + if (extendedDebugging) monitor.debug("ContractCriteria: " + contractDefinition.getAccessPolicyId()); } @@ -155,7 +155,7 @@ public String getCatalog(FederatedCatalogCacheQuery federatedCatalogCacheQuery) assert contractDefinitionList != null; for (ContractDefinition contract : contractDefinitionList) { - monitor.debug(format("[%s] fetching contract %s", this.getClass().getSimpleName(), contract.getId())); + if (extendedDebugging) monitor.debug(format("[%s] fetching contract %s", this.getClass().getSimpleName(), contract.getId())); // TODO: Refactor - connect everything together diff --git a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/BlockchainSmartContractService.java b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/BlockchainSmartContractService.java index 089d0db..b362e1b 100644 --- a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/BlockchainSmartContractService.java +++ b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/BlockchainSmartContractService.java @@ -8,9 +8,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonValue; +import jakarta.json.JsonObject; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractOfferMessage; import org.eclipse.edc.connector.contract.spi.types.offer.ContractDefinition; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; @@ -19,6 +17,8 @@ import org.eclipse.edc.spi.types.domain.asset.Asset; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; +import org.eclipse.edc.validator.spi.ValidationResult; +import org.eclipse.edc.validator.spi.Violation; import org.eclipse.edc.web.spi.exception.InvalidRequestException; import org.eclipse.edc.web.spi.exception.ValidationFailureException; @@ -111,53 +111,6 @@ public ReturnObject sendToSmartContract(String jsonString, String smartContractU return returnObject; } - - - public Asset getAssetWithIdFromSmartContract(String id, String edcInterfaceUrl) { - Asset asset = null; - ObjectMapper mapper = new ObjectMapper(); - - HttpURLConnection c = null; - try { - URL u = new URL(edcInterfaceUrl + "/asset/" + id); - c = (HttpURLConnection) u.openConnection(); - c.setRequestMethod("GET"); - c.setRequestProperty("Content-length", "0"); - c.setUseCaches(false); - c.setAllowUserInteraction(false); - c.connect(); - int status = c.getResponseCode(); - - switch (status) { - case 200: - case 201: - BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - br.close(); - - return mapper.readValue(sb.toString(), TokenziedAsset.class).getTokenDataAsAsset(); - default: - return null; - } - - } catch (IOException ex) { - System.out.println(ex); - } finally { - if (c != null) { - try { - c.disconnect(); - } catch (Exception ex) { - System.out.println(ex); - } - } - } - return null; - } - /** Get all contract definitions from the smart contract. * * @return List of ContractDefinitionResponseDto @@ -180,99 +133,81 @@ public List getAllContractDefinitionsFromSmartContract() { c.connect(); int status = c.getResponseCode(); - switch (status) { - case 200: - case 201: - BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - br.close(); + if (status != 200 && status != 201) { + monitor.warning("Failed to fetch contracts from edc-interface with status code: " + status); + return null; + } - monitor.debug(sb.toString()); - tokenziedContractList = mapper.readValue(sb.toString(), new TypeReference>() {}); + BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + br.close(); - for (TokenizedContract tokenizedContract : tokenziedContractList) { - if (tokenizedContract != null) { + tokenziedContractList = mapper.readValue(sb.toString(), new TypeReference>() {}); - if (tokenizedContract != null && tokenizedContract.getTokenData() != null && tokenizedContract.getTokenData().containsKey("@id")) { - if (!tokenizedContract.getTokenData().containsKey("@context")) { - monitor.warning("TokenizedContractDefinition " + tokenizedContract.getTokenData().getString("@id") + " does not contain @context - Skipping"); - continue; - } - monitor.debug("Validating contract definition: " + tokenizedContract.getTokenData()); - try { - /* - var preTransformedPolicyDefinition = tokenizedContract.getTokenData(); + for (TokenizedContract tokenizedContract : tokenziedContractList) { + if (tokenizedContract == null) { + monitor.warning("TokenizedContractDefinition is null"); + continue; + } - JsonValue policyNode = preTransformedPolicyDefinition.get("edc:policy"); - if (policyNode.getValueType() == JsonValue.ValueType.OBJECT) { - JsonArray policyArray = Json.createArrayBuilder().add(policyNode).build(); - preTransformedPolicyDefinition = Json.createObjectBuilder(preTransformedPolicyDefinition) - .remove("edc:policy") - .add("edc:policy", policyArray) - .build(); - } + JsonObject contractAsExpandedJson = jsonLd.expand(tokenizedContract.getTokenData()).getContent(); - */ + if (contractAsExpandedJson == null) { + monitor.warning("TokenizedContractDefinition does not contain tokenData"); + continue; + } - //validatorRegistry.validate(ContractDefinition.CONTRACT_DEFINITION_TYPE, tokenizedContract.getTokenData()).orElseThrow(ValidationFailureException::new); - //monitor.debug("Faulty Policy detection: " + tokenizedContract.getTokenData().getJsonArray("edc:assetsSelector").getJsonObject(0).getJsonObject("edc:operator").toString()); + if (contractAsExpandedJson.getString("@id") == null) { + monitor.warning("TokenizedContractDefinition does not contain @id"); + continue; + } - if (tokenizedContract.getTokenData().containsKey("edc:assetsSelector") && - tokenizedContract.getTokenData().get("edc:assetsSelector").getValueType() == JsonValue.ValueType.ARRAY && - tokenizedContract.getTokenData().getJsonArray("edc:assetsSelector").size() > 0 && - tokenizedContract.getTokenData().getJsonArray("edc:assetsSelector").getJsonObject(0).containsKey("edc:operator") && - tokenizedContract.getTokenData().getJsonArray("edc:assetsSelector").getJsonObject(0).getString("edc:operator").toString().equals("in")) { - monitor.debug("Contract definition contains 'in' operator instead of '='. Skipping as not supported"); - continue; - } - var jsonContract = jsonLd.expand(tokenizedContract.getTokenData()).getContent(); - monitor.debug("Expanded contract definition: " + jsonContract.toString()); - var contract = transformerRegistry.transform(jsonContract, ContractDefinition.class) - .orElseThrow(InvalidRequestException::new); + monitor.debug("Validating contract definition: " + contractAsExpandedJson.getString("@id")); + try { - contractOfferDtoList.add(contract); - } catch (ValidationFailureException vex) { - monitor.warning("Validation failed for contract definition with message: " + vex.getMessage()); - continue; - } catch (InvalidRequestException irex) { - monitor.warning("Transformation failed for contract definition with message: " + irex.getMessage()); - continue; - } - } else { - monitor.warning("TokenizedContractDefinition is null or does not contain @id"); - } + ValidationResult result = validatorRegistry.validate(ContractDefinition.CONTRACT_DEFINITION_TYPE, contractAsExpandedJson); + if (result.failed()) { + monitor.warning("Validation failed for contract definition with message: " + result.getFailureDetail()); + continue; + } - } - } - monitor.debug("Validation failed for " + (tokenziedContractList.size() - contractOfferDtoList.size()) + " contract definitions and succeeded for " + contractOfferDtoList.size() + " contract definitions"); - return contractOfferDtoList; - default: - return null; + var contract = transformerRegistry.transform(contractAsExpandedJson, ContractDefinition.class) + .orElseThrow(InvalidRequestException::new); + + contractOfferDtoList.add(contract); + + } catch (ValidationFailureException vex) { + monitor.warning("Validation failed for contract definition with message: " + vex.getMessage()); + continue; + } catch (InvalidRequestException irex) { + monitor.warning("Transformation failed for contract definition with message: " + irex.getMessage()); + continue; + } + } + monitor.info("Validation failed for " + (tokenziedContractList.size() - contractOfferDtoList.size()) + " contract definitions and succeeded for " + contractOfferDtoList.size() + " contract definitions"); - } catch (MalformedURLException ex) { - System.out.println(ex); } catch (IOException ex) { - System.out.println(ex); + monitor.warning(ex.getMessage()); } finally { if (c != null) { try { c.disconnect(); } catch (Exception ex) { - System.out.println(ex); + monitor.warning(ex.getMessage()); } } } - return null; + return contractOfferDtoList; } @@ -388,53 +323,16 @@ public HashMap> getAllContractDefinitionsFrom return null; } - /* - public static PolicyDefinition getPolicyWithIdFromSmartContract(String id, String edcInterfaceUrl) { - PolicyDefinition policy = null; - ObjectMapper mapper = new ObjectMapper(); - - HttpURLConnection c = null; - try { - URL u = new URL(edcInterfaceUrl + "/policy/"+id); - c = (HttpURLConnection) u.openConnection(); - c.setRequestMethod("GET"); - c.setRequestProperty("Content-length", "0"); - c.setUseCaches(false); - c.setAllowUserInteraction(false); - c.connect(); - int status = c.getResponseCode(); - - switch (status) { - case 200: - case 201: - BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - br.close(); - - return mapper.readValue(sb.toString(), TokenizedPolicyDefinition.class).getTokenData(); - } - - } catch (MalformedURLException ex) { - System.out.println(ex); - } catch (IOException ex) { - System.out.println(ex); - } finally { - if (c != null) { - try { - c.disconnect(); - } catch (Exception ex) { - System.out.println(ex); - } - } + private ValidationResult isJsonObjectAnValidAsset(JsonObject assetAsJson) { + if (!assetAsJson.containsKey("@id")) { + monitor.warning("TokenizedAsset does not contain @id"); + return ValidationResult.failure(new Violation("TokenizedAsset does not contain @id", "@id", assetAsJson)); } - return null; - } + monitor.debug("Validating asset: " + assetAsJson.getString("@id")); + // It is important to expand the json before validation as otherwise the exact paths in the json does not match the schema + return validatorRegistry.validate(Asset.EDC_ASSET_TYPE, assetAsJson); - */ + } /** Get all policy definitions from the smart contract and group them by source. * @@ -443,7 +341,6 @@ public static PolicyDefinition getPolicyWithIdFromSmartContract(String id, Strin public List getAllAssetsFromSmartContract() { ObjectMapper mapper = new ObjectMapper(); - List tokenziedAssetList; List assetResponseList = new ArrayList<>(); @@ -459,99 +356,69 @@ public List getAllAssetsFromSmartContract() { c.connect(); int status = c.getResponseCode(); - switch (status) { - case 200: - case 201: - BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line).append("\n"); - } - br.close(); + if (status != 200 && status != 201) { + monitor.warning("Failed to fetch assets from edc-interface with status code: " + status); + return null; + } - //monitor.debug("Read from edc-interface: " + sb.toString() + " and going on to map it to a list of TokenizedObjects"); - List assetList = mapper.readValue(sb.toString(), new TypeReference>() {}); - /* - I first tried to use the normal transformer and validator but it didnt work and i dont know why - So I will just transform them manually, but its the inferior solution - */ - monitor.debug("Read " + assetList.size() + " assets from edc-interface and going on to validate them"); - int failedCounter = 0; - for (TokenziedAsset tokenziedAsset : assetList) { + BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + br.close(); - if (tokenziedAsset == null || tokenziedAsset.tokenData == null) { - monitor.warning("TokenizedAsset is null? IPFS file to new?"); - continue; - } - if (!tokenziedAsset.tokenData.containsKey("@id")) { - monitor.debug("TokenizedAsset does not contain @id"); - continue; - } - if (!tokenziedAsset.tokenData.containsKey("@context")) { - monitor.debug("TokenziedAsset " + tokenziedAsset.tokenData.getString("@id") + " does not contain @context - Skipping"); - continue; - } - /* - if(!tokenziedAsset.getTokenData().containsKey("@id")) { - monitor.warning("TokenizedAsset does not contain @id"); - continue; - } - monitor.debug("Validating asset: " + tokenziedAsset.getTokenData()); - try { - validatorRegistry.validate(EDC_ASSET_TYPE, tokenziedAsset.getTokenData()).orElseThrow(ValidationFailureException::new); - } catch (ValidationFailureException vex) { - monitor.warning("Validation failed for asset with message: " + vex.getMessage()); - failedCounter++; - continue; - } catch (ClassCastException cce) { - monitor.warning("We ignore this exception as the validator seems to be buggy: " + cce.getMessage()); - } + List assetList = mapper.readValue(sb.toString(), new TypeReference<>() { + }); - */ - try { - var asset = transformerRegistry.transform(jsonLd.expand(tokenziedAsset.tokenData).getContent(), Asset.class) - .orElseThrow(InvalidRequestException::new); - assetResponseList.add(asset); - } catch (InvalidRequestException irex) { - monitor.warning("Transformation failed for asset " + tokenziedAsset.getTokenData().getString("@id") + " with message: " + irex.getMessage()); - failedCounter++; - continue; - } - /* - try { - var asset = tokenziedAsset.getTokenDataAsAsset(); - assetResponseList.add(asset); - } catch (IllegalArgumentException iae) { - monitor.warning("Transformation failed for asset with message: " + iae.getMessage()); - failedCounter++; - continue; - }*/ - } + monitor.debug("Read " + assetList.size() + " assets from edc-interface and going on to validate them"); + int failedCounter = 0; + for (TokenziedAsset tokenziedAsset : assetList) { - monitor.debug("Validation failed for " + failedCounter + " assets and succeeded for " + assetResponseList.size() + " assets"); + if (tokenziedAsset == null || tokenziedAsset.getTokenData(jsonLd) == null) { + monitor.warning("TokenizedAsset is null? IPFS file to new?"); + failedCounter++; + continue; + } + + var assetAsExpandedJson = tokenziedAsset.getTokenData(jsonLd); + + ValidationResult result = isJsonObjectAnValidAsset(assetAsExpandedJson); + + if (result.failed()) { + monitor.warning("Validation failed for asset with message in result: " + result.getFailureDetail()); + failedCounter++; + continue; + } + + try { + var asset = transformerRegistry.transform(jsonLd.expand(tokenziedAsset.getTokenData(jsonLd)).getContent(), Asset.class) + .orElseThrow(InvalidRequestException::new); + assetResponseList.add(asset); + } catch (InvalidRequestException irex) { + monitor.warning("Transformation failed for asset " + tokenziedAsset.getTokenData(jsonLd).getString("@id") + " with message: " + irex.getMessage()); + failedCounter++; + continue; + } - return assetResponseList; - default: - return null; } + monitor.info("Validation failed for " + failedCounter + " assets and succeeded for " + assetResponseList.size() + " assets"); - } catch (MalformedURLException ex) { - System.out.println(ex); } catch (IOException ex) { - System.out.println(ex); + monitor.warning(ex.getMessage()); } finally { if (c != null) { try { c.disconnect(); } catch (Exception ex) { - System.out.println(ex); + monitor.warning(ex.getMessage()); } } } - return null; + return assetResponseList; } @@ -562,7 +429,7 @@ public List getAllAssetsFromSmartContract() { public List getAllPolicyDefinitionsFromSmartContract() { ObjectMapper mapper = new ObjectMapper(); - List tokenizedPolicyDefinitionList = new ArrayList<>(); + List tokenizedPolicyDefinitionList; List policyDefinitionList = new ArrayList<>(); HttpURLConnection c = null; @@ -576,82 +443,74 @@ public List getAllPolicyDefinitionsFromSmartContract() { c.connect(); int status = c.getResponseCode(); - switch (status) { - case 200: - case 201: - BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = br.readLine()) != null) { - sb.append(line + "\n"); - } - br.close(); + if (status != 200 && status != 201) { + monitor.warning("Failed to fetch policies from edc-interface with status code: " + status); + return null; + } - tokenizedPolicyDefinitionList = mapper.readValue(sb.toString(), new TypeReference>(){}); - monitor.debug("Read policies from edc-interface: " + tokenizedPolicyDefinitionList.size() + " policies and going validate them"); - //monitor.debug("Read policies from edc-interface: " + sb.toString()); - for (TokenizedPolicyDefinition tokenizedPolicyDefinition : tokenizedPolicyDefinitionList) { - if (tokenizedPolicyDefinition != null) { - monitor.debug("Validating policy definition: " + tokenizedPolicyDefinition.getTokenData()); - } - if (tokenizedPolicyDefinition != null && tokenizedPolicyDefinition.getTokenData() != null && - tokenizedPolicyDefinition.getTokenData().containsKey("@id")) { - if (!tokenizedPolicyDefinition.getTokenData().containsKey("@context")) { - monitor.warning("TokenizedPolicyDefinition " + tokenizedPolicyDefinition.getTokenData().getString("@id") + " does not contain @context - Skipping"); - continue; - } - try { - monitor.debug("Going to validate and transform policy definition: " + tokenizedPolicyDefinition.getTokenData().getString("@id")); - var preTransformedPolicyDefinition = tokenizedPolicyDefinition.getTokenData(); - - - JsonValue policyNode = preTransformedPolicyDefinition.get("edc:policy"); - if (policyNode.getValueType() == JsonValue.ValueType.OBJECT) { - JsonArray policyArray = Json.createArrayBuilder().add(policyNode).build(); - preTransformedPolicyDefinition = Json.createObjectBuilder(preTransformedPolicyDefinition) - .remove("edc:policy") - .add("edc:policy", policyArray) - .build(); - } + BufferedReader br = new BufferedReader(new InputStreamReader(c.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + br.close(); + + tokenizedPolicyDefinitionList = mapper.readValue(sb.toString(), new TypeReference>(){}); + monitor.debug("Read policies from edc-interface: " + tokenizedPolicyDefinitionList.size() + " policies and going validate them"); + //monitor.debug("Read policies from edc-interface: " + sb.toString()); + for (TokenizedPolicyDefinition tokenizedPolicyDefinition : tokenizedPolicyDefinitionList) { + if (tokenizedPolicyDefinition == null) { + monitor.warning("TokenizedPolicyDefinition is null"); + continue; + } + JsonObject policyAsExpandedJson = tokenizedPolicyDefinition.getTokenData(jsonLd); - var expandedTokenizedPolicyDefinition = jsonLd.expand(preTransformedPolicyDefinition).getContent(); - validatorRegistry.validate(PolicyDefinition.EDC_POLICY_DEFINITION_TYPE, expandedTokenizedPolicyDefinition).orElseThrow(ValidationFailureException::new); - - var policyDefinition = transformerRegistry.transform(expandedTokenizedPolicyDefinition, PolicyDefinition.class) - .orElseThrow(InvalidRequestException::new); - policyDefinitionList.add(policyDefinition); - } catch (ValidationFailureException vex) { - monitor.warning("Validation failed for policy definition with message: " + vex.getMessage()); - continue; - } catch (InvalidRequestException irex) { - monitor.warning("Transformation failed for policy definition with message: " + irex.getMessage()); - continue; - } - } else { - monitor.debug("TokenizedPolicyDefinition is null or does not contain @id"); - } + if (policyAsExpandedJson == null) { + monitor.warning("TokenizedPolicyDefinition does not contain tokenData"); + continue; + } - } - monitor.debug("Read " + policyDefinitionList.size() + " policy definitions from edc-interface"); - return policyDefinitionList; - default: - return null; + if (policyAsExpandedJson.getString("@id") == null) { + monitor.warning("TokenizedPolicyDefinition does not contain @id"); + continue; + } + monitor.debug("Validating policy definition: " + policyAsExpandedJson.getString("@id")); + + ValidationResult result = validatorRegistry.validate(PolicyDefinition.EDC_POLICY_DEFINITION_TYPE, policyAsExpandedJson); + + if (result.failed()) { + monitor.warning("Validation failed for policy definition with message: " + result.getFailureDetail()); + continue; + } + + try { + monitor.debug("Transforming policy definition: " + policyAsExpandedJson.getString("@id")); + + var policyDefinition = transformerRegistry.transform(policyAsExpandedJson, PolicyDefinition.class) + .orElseThrow(InvalidRequestException::new); + policyDefinitionList.add(policyDefinition); + } catch (ValidationFailureException vex) { + monitor.warning("Validation failed for policy definition with message: " + vex.getMessage()); + continue; + } catch (InvalidRequestException irex) { + monitor.warning("Transformation failed for policy definition with message: " + irex.getMessage()); + continue; + } } + monitor.info("Read " + policyDefinitionList.size() + " policy definitions from edc-interface"); - } catch (MalformedURLException ex) { - System.out.println(ex); } catch (IOException ex) { - System.out.println(ex); - ex.printStackTrace(); + monitor.warning(ex.getMessage()); } finally { if (c != null) { try { c.disconnect(); } catch (Exception ex) { - System.out.println(ex); + monitor.warning(ex.getMessage()); } } } - return null; + return policyDefinitionList; } } diff --git a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenizedPolicyDefinition.java b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenizedPolicyDefinition.java index a33aa39..c3542ca 100644 --- a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenizedPolicyDefinition.java +++ b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenizedPolicyDefinition.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import jakarta.json.JsonObject; import org.eclipse.edc.connector.policy.spi.PolicyDefinition; +import org.eclipse.edc.jsonld.spi.JsonLd; public class TokenizedPolicyDefinition { @SuppressWarnings("CheckStyle") @@ -37,8 +38,8 @@ public void setDecimals(String decimals) { this.decimals = decimals; } - public JsonObject getTokenData() { - return tokenData; + public JsonObject getTokenData(JsonLd jsonLd) { + return jsonLd.expand(tokenData).getContent(); } public void setTokenData(JsonObject tokenData) { diff --git a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenziedAsset.java b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenziedAsset.java index c2dc86f..f8592c4 100644 --- a/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenziedAsset.java +++ b/extensions/blockchain/catalog-listener/src/main/java/berlin/tu/ise/extension/blockchain/catalog/listener/model/TokenziedAsset.java @@ -2,13 +2,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import jakarta.json.Json; -import jakarta.json.JsonArray; import jakarta.json.JsonObject; -import jakarta.json.JsonValue; -import org.eclipse.edc.spi.types.domain.DataAddress; -import org.eclipse.edc.spi.types.domain.HttpDataAddress; -import org.eclipse.edc.spi.types.domain.asset.Asset; +import org.eclipse.edc.jsonld.spi.JsonLd; public class TokenziedAsset { @SuppressWarnings("CheckStyle") @@ -42,111 +37,8 @@ public void setDecimals(String decimals) { this.decimals = decimals; } - /* - Strangely there is some mismatch where the transformer is creating JSON representations of Assets - where dataAddress and properties are objects containing objects but when trying to deserialize them back - into an Asset the dataAddress and properties are exprected to be arrays - */ - public JsonObject getTokenData() { - JsonObject returnTokenData = tokenData; - /* - JsonValue dataAddressNode = returnTokenData.get("https://w3id.org/edc/v0.0.1/ns/dataAddress"); - if (dataAddressNode != null && dataAddressNode.getValueType() == JsonValue.ValueType.OBJECT) { - JsonArray dataAddressArray = Json.createArrayBuilder().add(dataAddressNode).build(); - returnTokenData = Json.createObjectBuilder(returnTokenData) - .remove("https://w3id.org/edc/v0.0.1/ns/dataAddress") - .add("https://w3id.org/edc/v0.0.1/ns/dataAddress", dataAddressArray) - .build(); - } - */ - JsonValue dataAddressTypeNode = returnTokenData.getJsonObject("edc:dataAddress").get("@type"); - if (dataAddressTypeNode != null && dataAddressTypeNode.getValueType() == JsonValue.ValueType.STRING) { - JsonArray dataAddressTypeArray = Json.createArrayBuilder().add(dataAddressTypeNode).build(); - var changes = Json.createObjectBuilder(returnTokenData.getJsonObject("edc:dataAddress")) - .remove("@type") - .add("@type", dataAddressTypeArray) - .build(); - returnTokenData = Json.createObjectBuilder(returnTokenData) - .remove("edc:dataAddress") - .add("edc:dataAddress", changes) - .build(); - } - - - - JsonValue propertiesNode = returnTokenData.get("edc:properties"); - if (propertiesNode != null && propertiesNode.getValueType() == JsonValue.ValueType.OBJECT) { - JsonArray propertiesArray = Json.createArrayBuilder().add(propertiesNode).build(); - returnTokenData = Json.createObjectBuilder(returnTokenData) - .remove("edc:properties") - .add("edc:properties", propertiesArray) - .build(); - } - return returnTokenData; + public JsonObject getTokenData(JsonLd jsonLd) { + return jsonLd.expand(tokenData).getContent(); } - public Asset getTokenDataAsAsset() throws IllegalArgumentException { - var jsonDataAddressTypeObject = tokenData.getJsonObject("edc:dataAddress"); // typo DataAddress vs dataAdress - DataAddress.EDC_DATA_ADDRESS_TYPE); - if (jsonDataAddressTypeObject == null) { - throw new IllegalArgumentException("The token data does not contain a data address type"); - } - if (!jsonDataAddressTypeObject.containsKey(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY)) { - throw new IllegalArgumentException("The token data does not contain a data address type property"); - } - DataAddress transformedDataAddress; - if (jsonDataAddressTypeObject.containsKey("baseUrl")) { - HttpDataAddress.Builder builder = HttpDataAddress.Builder.newInstance() - .baseUrl(jsonDataAddressTypeObject.getString("baseUrl")); - if (jsonDataAddressTypeObject.containsKey("name")) { - builder.name(jsonDataAddressTypeObject.getString("name")); - } else { - builder.name("default"); - } - - transformedDataAddress = builder - .type(jsonDataAddressTypeObject.getString(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY)) - .build(); - } else { - transformedDataAddress = DataAddress.Builder.newInstance() - .type(jsonDataAddressTypeObject.getString(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY)) - .build(); - } - /* - if (!jsonDataAddressTypeObject.containsKey("path")) { - throw new IllegalArgumentException("The token data does not contain a data address type path"); - }*/ - - /* - var transformedDataAddress = HttpDataAddress.Builder.newInstance() - //.type(tokenData.getJsonObject(DataAddress.EDC_DATA_ADDRESS_TYPE).getString("@type")) - .baseUrl(jsonDataAddressTypeObject.getString("baseUrl")) - .path(jsonDataAddressTypeObject.getString("path")) - .type(jsonDataAddressTypeObject.getString(DataAddress.EDC_DATA_ADDRESS_TYPE_PROPERTY)) - .build(); - */ - var assetBuilder = Asset.Builder.newInstance() - .id(tokenData.getString("@id")) - .contentType(tokenData.getString("@type")) - .dataAddress(transformedDataAddress); - - var jsonAssetPropertiesObject = tokenData.getJsonObject(Asset.EDC_ASSET_PROPERTIES); - if (jsonAssetPropertiesObject == null) { - throw new IllegalArgumentException("The token data does not contain an asset properties object"); - } - if (!jsonAssetPropertiesObject.containsKey(Asset.PROPERTY_ID)) { - throw new IllegalArgumentException("The token data does not contain an asset properties id"); - } - if (!jsonAssetPropertiesObject.containsKey(Asset.PROPERTY_NAME)) { - throw new IllegalArgumentException("The token data does not contain an asset properties name"); - } - if (jsonAssetPropertiesObject.containsKey(Asset.PROPERTY_DESCRIPTION)) { - assetBuilder.property(Asset.PROPERTY_DESCRIPTION, jsonAssetPropertiesObject.getString(Asset.PROPERTY_DESCRIPTION)); - } - - var transformedAsset = assetBuilder - .property(Asset.PROPERTY_ID, jsonAssetPropertiesObject.getString(Asset.PROPERTY_ID)) - .property(Asset.PROPERTY_NAME, jsonAssetPropertiesObject.getString(Asset.PROPERTY_NAME)) - .build(); - return transformedAsset; - } } diff --git a/extensions/helper/build.gradle.kts b/extensions/helper/build.gradle.kts new file mode 100644 index 0000000..d7d39f7 --- /dev/null +++ b/extensions/helper/build.gradle.kts @@ -0,0 +1,25 @@ + +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" + id("io.swagger.core.v3.swagger-gradle-plugin") +} + +val groupId: String by project +val edcVersion: String by project + +dependencies { + implementation(libs.edc.vault.filesystem) +} + + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + exclude("**/pom.properties", "**/pom.xml") + mergeServiceFiles() + archiveFileName.set("consumer.jar") +} \ No newline at end of file diff --git a/extensions/helper/src/main/java/berlin/tu/ise/extension/helper/HelperExtension.java b/extensions/helper/src/main/java/berlin/tu/ise/extension/helper/HelperExtension.java new file mode 100644 index 0000000..3afae92 --- /dev/null +++ b/extensions/helper/src/main/java/berlin/tu/ise/extension/helper/HelperExtension.java @@ -0,0 +1,30 @@ +package berlin.tu.ise.extension.helper; + + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +@Extension(value = "TU Berlin Helper Extensions") +public class HelperExtension implements ServiceExtension { + //@Inject + //private FsVaultExtension fsVaultExtension; + + + private ServiceExtensionContext context; + + + @Override + public void initialize(ServiceExtensionContext context) { + + this.context = context; + + var monitor = context.getMonitor(); + + monitor.info("HelperExtensions initialized"); + + + } + + +} diff --git a/extensions/helper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/helper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..ca22343 --- /dev/null +++ b/extensions/helper/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1 @@ +berlin.tu.ise.extension.helper.HelperExtension \ No newline at end of file diff --git a/external/oauth2/oauth2-client/build.gradle.kts b/external/oauth2/oauth2-client/build.gradle.kts new file mode 100644 index 0000000..2130c1b --- /dev/null +++ b/external/oauth2/oauth2-client/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 + * + */ + +plugins { + `java-library` +} + +dependencies { + api(libs.edc.spi.http) + api(libs.edc.spi.oauth2) +} + + diff --git a/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ClientExtension.java b/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ClientExtension.java new file mode 100644 index 0000000..9126f34 --- /dev/null +++ b/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ClientExtension.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 + * + */ + +package org.eclipse.edc.iam.oauth2; + +import org.eclipse.edc.iam.oauth2.client.Oauth2ClientImpl; +import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; + +@Extension(value = Oauth2ClientExtension.NAME) +public class Oauth2ClientExtension implements ServiceExtension { + + public static final String NAME = "OAuth2 Client"; + + @Inject + private EdcHttpClient httpClient; + + @Inject + private TypeManager typeManager; + + @Override + public String name() { + return NAME; + } + + @Provider + public Oauth2Client oauth2Client(ServiceExtensionContext context) { + return new Oauth2ClientImpl(httpClient, typeManager); + } +} diff --git a/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImpl.java b/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImpl.java new file mode 100644 index 0000000..ebf659d --- /dev/null +++ b/external/oauth2/oauth2-client/src/main/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImpl.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 + * + */ + +package org.eclipse.edc.iam.oauth2.client; + +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; +import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client; +import org.eclipse.edc.iam.oauth2.spi.client.Oauth2CredentialsRequest; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.TypeManager; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.eclipse.edc.spi.http.FallbackFactories.retryWhenStatusIsNot; + +public class Oauth2ClientImpl implements Oauth2Client { + + private static final String ACCEPT = "Accept"; + private static final String CONTENT_TYPE = "Content-Type"; + private static final String FORM_URLENCODED = "application/x-www-form-urlencoded"; + private static final String APPLICATION_JSON = "application/json"; + private static final String RESPONSE_ACCESS_TOKEN_CLAIM = "access_token"; + + private final EdcHttpClient httpClient; + private final TypeManager typeManager; + + public Oauth2ClientImpl(EdcHttpClient httpClient, TypeManager typeManager) { + this.httpClient = httpClient; + this.typeManager = typeManager; + } + + @Override + public Result requestToken(Oauth2CredentialsRequest request) { + return httpClient.execute(toRequest(request), List.of(retryWhenStatusIsNot(200)), this::handleResponse); + } + + private Result handleResponse(Response response) { + return getStringBody(response) + .map(it -> typeManager.readValue(it, Map.class)) + .map(it -> it.get(RESPONSE_ACCESS_TOKEN_CLAIM).toString()) + .map(it -> TokenRepresentation.Builder.newInstance().token(it).build()); + } + + private static Request toRequest(Oauth2CredentialsRequest request) { + return new Request.Builder() + .url(request.getUrl()) + .addHeader(ACCEPT, APPLICATION_JSON) + .addHeader(CONTENT_TYPE, FORM_URLENCODED) + .post(createRequestBody(request)) + .build(); + } + + private static FormBody createRequestBody(Oauth2CredentialsRequest request) { + var builder = new FormBody.Builder(); + request.getParams().entrySet().stream() + .filter(entry -> entry.getValue() != null) + .forEach(entry -> builder.add(entry.getKey(), entry.getValue())); + return builder.build(); + } + + @NotNull + private Result getStringBody(Response response) { + try (var body = response.body()) { + if (body != null) { + return Result.success(body.string()); + } else { + return Result.failure("Body is null"); + } + } catch (IOException e) { + return Result.failure("Cannot read response body as String: " + e.getMessage()); + } + + } + +} diff --git a/external/oauth2/oauth2-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/external/oauth2/oauth2-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..6b741a4 --- /dev/null +++ b/external/oauth2/oauth2-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2022 Amadeus +# +# 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 +# +# + +org.eclipse.edc.iam.oauth2.Oauth2ClientExtension diff --git a/external/oauth2/oauth2-client/src/test/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImplTest.java b/external/oauth2/oauth2-client/src/test/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImplTest.java new file mode 100644 index 0000000..53140f6 --- /dev/null +++ b/external/oauth2/oauth2-client/src/test/java/org/eclipse/edc/iam/oauth2/client/Oauth2ClientImplTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 + * + */ + +package org.eclipse.edc.iam.oauth2.client; + +import org.eclipse.edc.iam.oauth2.spi.client.Oauth2CredentialsRequest; +import org.eclipse.edc.iam.oauth2.spi.client.SharedSecretOauth2CredentialsRequest; +import org.eclipse.edc.spi.types.TypeManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.HttpResponse; +import org.mockserver.model.Parameter; +import org.mockserver.model.ParameterBody; +import org.mockserver.model.Parameters; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getFreePort; +import static org.eclipse.edc.junit.testfixtures.TestUtils.testHttpClient; +import static org.mockserver.integration.ClientAndServer.startClientAndServer; +import static org.mockserver.model.MediaType.APPLICATION_JSON; + +class Oauth2ClientImplTest { + + private final int port = getFreePort(); + private final ClientAndServer server = startClientAndServer(port); + private final TypeManager typeManager = new TypeManager(); + + private Oauth2ClientImpl client; + + @BeforeEach + public void setUp() { + client = new Oauth2ClientImpl(testHttpClient(), typeManager); + } + + @Test + void verifyRequestTokenSuccess() { + var request = createRequest(); + + var formParameters = new Parameters( + request.getParams().entrySet().stream() + .map(entry -> Parameter.param(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()) + ); + + var expectedRequest = HttpRequest.request().withBody(new ParameterBody(formParameters)); + var responseBody = typeManager.writeValueAsString(Map.of("access_token", "token")); + server.when(expectedRequest).respond(HttpResponse.response().withBody(responseBody, APPLICATION_JSON)); + + var result = client.requestToken(request); + + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent().getToken()).isEqualTo("token"); + } + + @Test + void verifyFailureIfServerCallFails() { + var request = createRequest(); + server.when(HttpRequest.request()).respond(HttpResponse.response().withStatusCode(400)); + + var result = client.requestToken(request); + + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).startsWith("Server response"); + } + + @Test + void verifyFailureIfServerIsNotReachable() { + server.stop(); + + var request = createRequest(); + + var result = client.requestToken(request); + + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).startsWith("Failed to connect to"); + } + + private Oauth2CredentialsRequest createRequest() { + return SharedSecretOauth2CredentialsRequest.Builder.newInstance() + .url("http://localhost:" + port) + .clientId("clientId") + .clientSecret("clientSecret") + .grantType("client_credentials") + .build(); + } + + +} diff --git a/external/oauth2/oauth2-core/README.md b/external/oauth2/oauth2-core/README.md new file mode 100644 index 0000000..e98bf54 --- /dev/null +++ b/external/oauth2/oauth2-core/README.md @@ -0,0 +1,24 @@ +# OAuth 2 Identity Service + +This extension provides an `IdentityService` implementation based on the OAuth2 protocol for authorization. + +## 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 | + +## Extensions + +### CredentialsRequestAdditionalParametersProvider + +An instance of the `CredentialsRequestAdditionalParametersProvider` service interface can be provided to have the +possibility to enrich the form parameters of the client credentials token request diff --git a/external/oauth2/oauth2-core/build.gradle.kts b/external/oauth2/oauth2-core/build.gradle.kts new file mode 100644 index 0000000..0ce520b --- /dev/null +++ b/external/oauth2/oauth2-core/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + /* + api(project(":spi:common:http-spi")) + api(project(":spi:common:oauth2-spi")) + + + implementation(project(":core:common:jwt-core")) + + + */ + api(libs.edc.spi.http) + api(libs.edc.spi.oauth2) + implementation(project(":external:oauth2:oauth2-client")) + implementation(libs.edc.jwt.core) + implementation(libs.nimbus.jwt) +} + + diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java new file mode 100644 index 0000000..667a71a --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceConfiguration.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2; + +import org.eclipse.edc.iam.oauth2.identity.Oauth2ServiceImpl; +import org.eclipse.edc.spi.iam.PublicKeyResolver; +import org.eclipse.edc.spi.security.CertificateResolver; +import org.eclipse.edc.spi.security.PrivateKeyResolver; + +/** + * Configuration values and dependencies for {@link Oauth2ServiceImpl}. + */ +public class Oauth2ServiceConfiguration { + + 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 Long tokenExpiration; + + private Oauth2ServiceConfiguration() { + + } + + public String getTokenUrl() { + return tokenUrl; + } + + public String getClientId() { + return clientId; + } + + public String getPrivateKeyAlias() { + return privateKeyAlias; + } + + public String getPublicCertificateAlias() { + return publicCertificateAlias; + } + + public String getProviderAudience() { + return providerAudience; + } + + public PrivateKeyResolver getPrivateKeyResolver() { + return privateKeyResolver; + } + + public CertificateResolver getCertificateResolver() { + return certificateResolver; + } + + public PublicKeyResolver getIdentityProviderKeyResolver() { + return identityProviderKeyResolver; + } + + public int getNotBeforeValidationLeeway() { + return notBeforeValidationLeeway; + } + + public String getEndpointAudience() { + return endpointAudience; + } + + public Long getTokenExpiration() { + return tokenExpiration; + } + + public static class Builder { + private final Oauth2ServiceConfiguration configuration = new Oauth2ServiceConfiguration(); + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder tokenUrl(String url) { + configuration.tokenUrl = url; + return this; + } + + public Builder clientId(String clientId) { + configuration.clientId = clientId; + return this; + } + + public Builder privateKeyResolver(PrivateKeyResolver privateKeyResolver) { + configuration.privateKeyResolver = privateKeyResolver; + return this; + } + + /** + * Resolves this runtime's certificate containing its public key. + */ + public Builder certificateResolver(CertificateResolver certificateResolver) { + configuration.certificateResolver = certificateResolver; + return this; + } + + /** + * Resolves the certificate containing the identity provider's public key. + */ + public Builder identityProviderKeyResolver(PublicKeyResolver identityProviderKeyResolver) { + configuration.identityProviderKeyResolver = identityProviderKeyResolver; + return this; + } + + public Builder privateKeyAlias(String privateKeyAlias) { + configuration.privateKeyAlias = privateKeyAlias; + return this; + } + + public Builder publicCertificateAlias(String publicCertificateAlias) { + configuration.publicCertificateAlias = publicCertificateAlias; + return this; + } + + public Builder providerAudience(String providerAudience) { + configuration.providerAudience = providerAudience; + return this; + } + + public Builder notBeforeValidationLeeway(int notBeforeValidationLeeway) { + configuration.notBeforeValidationLeeway = notBeforeValidationLeeway; + return this; + } + + public Builder endpointAudience(String endpointAudience) { + configuration.endpointAudience = endpointAudience; + return this; + } + + public Builder tokenExpiration(long tokenExpiration) { + configuration.tokenExpiration = tokenExpiration; + return this; + } + + public Oauth2ServiceConfiguration build() { + return configuration; + } + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceDefaultServicesExtension.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceDefaultServicesExtension.java new file mode 100644 index 0000000..06e5c45 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceDefaultServicesExtension.java @@ -0,0 +1,41 @@ +/* + * 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.edc.iam.oauth2; + +import org.eclipse.edc.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; + +import java.util.Collections; + +/** + * Provides default service implementations for fallback + * Omitted {@link org.eclipse.edc.runtime.metamodel.annotation.Extension} since this module already contains {@link Oauth2ServiceExtension} + */ +public class Oauth2ServiceDefaultServicesExtension implements ServiceExtension { + + public static final String NAME = "OAuth2 Core Default Services"; + + @Override + public String name() { + return NAME; + } + + @Provider(isDefault = true) + public CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider() { + return parameters -> Collections.emptyMap(); + } + +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java new file mode 100644 index 0000000..948e9f1 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtension.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * Fraunhofer Institute for Software and Systems Engineering - Improvements + * + */ + +package org.eclipse.edc.iam.oauth2; + +import org.eclipse.edc.iam.oauth2.identity.IdentityProviderKeyResolver; +import org.eclipse.edc.iam.oauth2.identity.IdentityProviderKeyResolverConfiguration; +import org.eclipse.edc.iam.oauth2.identity.Oauth2ServiceImpl; +import org.eclipse.edc.iam.oauth2.jwt.Oauth2JwtDecoratorRegistryRegistryImpl; +import org.eclipse.edc.iam.oauth2.jwt.X509CertificateDecorator; +import org.eclipse.edc.iam.oauth2.rule.Oauth2ValidationRulesRegistryImpl; +import org.eclipse.edc.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider; +import org.eclipse.edc.iam.oauth2.spi.Oauth2AssertionDecorator; +import org.eclipse.edc.iam.oauth2.spi.Oauth2JwtDecoratorRegistry; +import org.eclipse.edc.iam.oauth2.spi.Oauth2ValidationRulesRegistry; +import org.eclipse.edc.iam.oauth2.spi.client.Oauth2Client; +import org.eclipse.edc.jwt.TokenGenerationServiceImpl; +import org.eclipse.edc.jwt.TokenValidationServiceImpl; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provides; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.security.CertificateResolver; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; + +import java.security.PrivateKey; +import java.time.Clock; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * Provides OAuth2 client credentials flow support. + */ +@Provides({ IdentityService.class, Oauth2JwtDecoratorRegistry.class, Oauth2ValidationRulesRegistry.class }) +@Extension(value = Oauth2ServiceExtension.NAME) +public class Oauth2ServiceExtension implements ServiceExtension { + + public static final String NAME = "OAuth2 Identity Service"; + private static final int DEFAULT_TOKEN_EXPIRATION = 5; + @Setting + private static final String PROVIDER_JWKS_URL = "edc.oauth.provider.jwks.url"; + @Setting(value = "outgoing tokens 'aud' claim value, by default it's the connector id") + private static final String PROVIDER_AUDIENCE = "edc.oauth.provider.audience"; + @Setting(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"; + + @Setting + private static final String PUBLIC_CERTIFICATE_ALIAS = "edc.oauth.certificate.alias"; + @Setting + private static final String PRIVATE_KEY_ALIAS = "edc.oauth.private.key.alias"; + @Setting + private static final String PROVIDER_JWKS_REFRESH = "edc.oauth.provider.jwks.refresh"; // in minutes + @Setting + private static final String TOKEN_URL = "edc.oauth.token.url"; + @Setting(value = "Token expiration in minutes. By default is 5 minutes") + private static final String TOKEN_EXPIRATION = "edc.oauth.token.expiration"; // in minutes + @Setting + private static final String CLIENT_ID = "edc.oauth.client.id"; + @Setting + private static final String NOT_BEFORE_LEEWAY = "edc.oauth.validation.nbf.leeway"; + private IdentityProviderKeyResolver providerKeyResolver; + + @Inject + private EdcHttpClient httpClient; + + @Inject + private PrivateKeyResolver privateKeyResolver; + + @Inject + private CertificateResolver certificateResolver; + + @Inject + private Clock clock; + + @Inject + private Oauth2Client oauth2Client; + + @Inject + private CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider; + + @Inject + private TypeManager typeManager; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var jwksUrl = context.getSetting(PROVIDER_JWKS_URL, "http://localhost/empty_jwks_url"); + var keyRefreshInterval = context.getSetting(PROVIDER_JWKS_REFRESH, 5); + var identityProviderKeyResolverConfiguration = new IdentityProviderKeyResolverConfiguration(jwksUrl, keyRefreshInterval); + providerKeyResolver = new IdentityProviderKeyResolver(context.getMonitor(), httpClient, typeManager, identityProviderKeyResolverConfiguration); + + var configuration = createConfig(context); + + var certificate = Optional.ofNullable(configuration.getCertificateResolver().resolveCertificate(configuration.getPublicCertificateAlias())) + .orElseThrow(() -> new EdcException("Public certificate not found: " + configuration.getPublicCertificateAlias())); + var jwtDecoratorRegistry = new Oauth2JwtDecoratorRegistryRegistryImpl(); + jwtDecoratorRegistry.register(new Oauth2AssertionDecorator(configuration.getProviderAudience(), configuration.getClientId(), clock, configuration.getTokenExpiration())); + jwtDecoratorRegistry.register(new X509CertificateDecorator(certificate)); + context.registerService(Oauth2JwtDecoratorRegistry.class, jwtDecoratorRegistry); + + var validationRulesRegistry = new Oauth2ValidationRulesRegistryImpl(configuration, clock); + context.registerService(Oauth2ValidationRulesRegistry.class, validationRulesRegistry); + + var privateKeyAlias = configuration.getPrivateKeyAlias(); + var privateKey = configuration.getPrivateKeyResolver().resolvePrivateKey(privateKeyAlias, PrivateKey.class); + + var oauth2Service = new Oauth2ServiceImpl( + configuration, + new TokenGenerationServiceImpl(privateKey), + oauth2Client, + jwtDecoratorRegistry, + new TokenValidationServiceImpl(configuration.getIdentityProviderKeyResolver(), validationRulesRegistry), + credentialsRequestAdditionalParametersProvider + ); + + context.registerService(IdentityService.class, oauth2Service); + } + + @Override + public void start() { + providerKeyResolver.start(); + } + + @Override + public void shutdown() { + providerKeyResolver.stop(); + } + + private Oauth2ServiceConfiguration 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 publicCertificateAlias = context.getConfig().getString(PUBLIC_CERTIFICATE_ALIAS); + var privateKeyAlias = context.getConfig().getString(PRIVATE_KEY_ALIAS); + var clientId = context.getConfig().getString(CLIENT_ID); + var tokenExpiration = context.getSetting(TOKEN_EXPIRATION, DEFAULT_TOKEN_EXPIRATION); + return Oauth2ServiceConfiguration.Builder.newInstance() + .identityProviderKeyResolver(providerKeyResolver) + .tokenUrl(tokenUrl) + .providerAudience(providerAudience) + .endpointAudience(endpointAudience) + .publicCertificateAlias(publicCertificateAlias) + .privateKeyAlias(privateKeyAlias) + .clientId(clientId) + .privateKeyResolver(privateKeyResolver) + .certificateResolver(certificateResolver) + .notBeforeValidationLeeway(context.getSetting(NOT_BEFORE_LEEWAY, 10)) + .tokenExpiration(TimeUnit.MINUTES.toSeconds(tokenExpiration)) + .build(); + } + +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolver.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolver.java new file mode 100644 index 0000000..3292bb1 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolver.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2020 - 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: + * Microsoft Corporation - initial API and implementation + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package org.eclipse.edc.iam.oauth2.identity; + +import okhttp3.Request; +import org.eclipse.edc.iam.oauth2.jwt.JwkKey; +import org.eclipse.edc.iam.oauth2.jwt.JwkKeys; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.iam.PublicKeyResolver; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.types.TypeManager; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.AbstractMap; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyMap; +import static java.util.concurrent.TimeUnit.MINUTES; + +/** + * Resolves public signing certificates for the identity provider. Used to verify JWTs. + * The keys are cached and the resolver must be started calling the `start` method + */ +public class IdentityProviderKeyResolver implements PublicKeyResolver { + private final Monitor monitor; + private static final String RSA = "RSA"; + + private final TypeManager typeManager; + private final IdentityProviderKeyResolverConfiguration configuration; + private final ScheduledExecutorService executorService; + private final AtomicReference> cache = new AtomicReference<>(emptyMap()); // the current key cache, atomic for thread-safety + private final EdcHttpClient httpClient; + private final Predicate isRsa = key -> RSA.equals(key.getKty()); + + public IdentityProviderKeyResolver(Monitor monitor, EdcHttpClient httpClient, TypeManager typeManager, IdentityProviderKeyResolverConfiguration configuration) { + this.monitor = monitor; + this.httpClient = httpClient; + this.typeManager = typeManager; + this.configuration = configuration; + this.executorService = Executors.newSingleThreadScheduledExecutor(); + } + + @Override + public RSAPublicKey resolveKey(String id) { + return cache.get().get(id); + } + + /** + * Start the keys cache refreshing job. + * Throws exception if it's not able to load the cache at startup. + * + */ + public void start() { + var result = refreshKeys(); + if (result.failed()) { + throw new EdcException(String.format("Failed to get keys from %s: %s", configuration.getJwksUrl(), String.join(", " + result.getFailureMessages()))); + } + + executorService.scheduleWithFixedDelay(this::refreshKeys, configuration.getKeyRefreshInterval(), configuration.getKeyRefreshInterval(), MINUTES); + } + + /** + * Stops the cache refresh job. + */ + public void stop() { + executorService.shutdownNow(); + } + + /** + * Get keys from the JWKS provider. Protected for testing purposes. + * + * @return succeed if keys are retrieved correctly, failure otherwise + */ + protected Result> getKeys() { + var request = new Request.Builder().url(configuration.getJwksUrl()).get().build(); + try (var response = httpClient.execute(request)) { + if (response.code() == 200) { + var body = response.body(); + if (body == null) { + var message = "Unable to refresh identity provider keys. An empty response was returned."; + monitor.severe(message); + return Result.failure(message); + } + + var jwsKeys = typeManager.readValue(body.string(), JwkKeys.class); + var keys = jwsKeys.getKeys(); + if (keys == null || keys.isEmpty()) { + var message = "No keys returned from identity provider."; + monitor.warning(message); + return Result.failure(message); + } + + return Result.success(deserializeKeys(keys)); + + } else { + var message = "Unable to refresh identity provider keys. Response code was: " + response.code(); + monitor.severe(message); + return Result.failure(message); + } + } catch (Exception e) { + var message = "Error resolving identity provider keys: " + configuration.getJwksUrl(); + monitor.severe(message, e); + return Result.failure(message); + } + } + + private Result refreshKeys() { + var result = getKeys(); + if (result.succeeded()) { + cache.set(result.getContent()); + } + return result.map(it -> null); + } + + private Map deserializeKeys(List jwkKeys) { + return jwkKeys.stream() + .filter(isRsa) + .map(this::deserializeKey) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Map.Entry deserializeKey(JwkKey key) { + var modulus = unsignedInt(key.getN()); + var exponent = unsignedInt(key.getE()); + var rsaPublicKeySpec = new RSAPublicKeySpec(modulus, exponent); + try { + var keyFactory = KeyFactory.getInstance(RSA); + return new AbstractMap.SimpleEntry<>(key.getKid(), (RSAPublicKey) keyFactory.generatePublic(rsaPublicKeySpec)); + } catch (GeneralSecurityException e) { + monitor.severe("Error parsing identity provider public key, skipping. The kid is: " + key.getKid()); + } + return null; + } + + private BigInteger unsignedInt(String value) { + return new BigInteger(1, Base64.getUrlDecoder().decode(value)); + } + +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverConfiguration.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverConfiguration.java new file mode 100644 index 0000000..b76f9d1 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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) - improvements + * + */ + +package org.eclipse.edc.iam.oauth2.identity; + +public class IdentityProviderKeyResolverConfiguration { + private final String jwksUrl; + private final long keyRefreshInterval; + + public IdentityProviderKeyResolverConfiguration(String jwksUrl, long keyRefreshInterval) { + this.jwksUrl = jwksUrl; + this.keyRefreshInterval = keyRefreshInterval; + } + + public String getJwksUrl() { + return jwksUrl; + } + + public long getKeyRefreshInterval() { + return keyRefreshInterval; + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java new file mode 100644 index 0000000..93eb9bd --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImpl.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 - 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: + * Microsoft Corporation - initial API and implementation + * Fraunhofer Institute for Software and Systems Engineering - Improvements + * Microsoft Corporation - Use IDS Webhook address for JWT audience claim + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package org.eclipse.edc.iam.oauth2.identity; + +import org.eclipse.edc.iam.oauth2.Oauth2ServiceConfiguration; +import org.eclipse.edc.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider; +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.jwt.spi.JwtDecorator; +import org.eclipse.edc.jwt.spi.JwtDecoratorRegistry; +import org.eclipse.edc.jwt.spi.TokenGenerationService; +import org.eclipse.edc.jwt.spi.TokenValidationService; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.NotNull; + +/** + * Implements the OAuth2 client credentials flow and bearer token validation. + */ +public class Oauth2ServiceImpl implements IdentityService { + + private static final String GRANT_TYPE = "client_credentials"; + + private final Oauth2ServiceConfiguration configuration; + private final Oauth2Client client; + private final JwtDecoratorRegistry jwtDecoratorRegistry; + private final TokenGenerationService tokenGenerationService; + private final TokenValidationService tokenValidationService; + private final CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider; + + /** + * Creates a new instance of the OAuth2 Service + * + * @param configuration The configuration + * @param tokenGenerationService Service used to generate the signed tokens + * @param client client for Oauth2 server + * @param jwtDecoratorRegistry Registry containing the decorator for build the JWT + * @param tokenValidationService Service used for token validation + * @param credentialsRequestAdditionalParametersProvider Provides additional form parameters + */ + public Oauth2ServiceImpl(Oauth2ServiceConfiguration configuration, TokenGenerationService tokenGenerationService, + Oauth2Client client, JwtDecoratorRegistry jwtDecoratorRegistry, TokenValidationService tokenValidationService, + CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider) { + this.configuration = configuration; + this.client = client; + this.jwtDecoratorRegistry = jwtDecoratorRegistry; + this.tokenGenerationService = tokenGenerationService; + this.tokenValidationService = tokenValidationService; + this.credentialsRequestAdditionalParametersProvider = credentialsRequestAdditionalParametersProvider; + } + + @Override + public Result obtainClientCredentials(TokenParameters parameters) { + return generateClientAssertion() + .map(assertion -> createRequest(parameters, assertion)) + .compose(client::requestToken); + } + + @Override + public Result verifyJwtToken(TokenRepresentation tokenRepresentation, String audience) { + return tokenValidationService.validate(tokenRepresentation); + } + + @NotNull + private Result generateClientAssertion() { + var decorators = jwtDecoratorRegistry.getAll().toArray(JwtDecorator[]::new); + return tokenGenerationService.generate(decorators) + .map(TokenRepresentation::getToken); + } + + @NotNull + private Oauth2CredentialsRequest createRequest(TokenParameters parameters, String assertion) { + return PrivateKeyOauth2CredentialsRequest.Builder.newInstance() + .url(configuration.getTokenUrl()) + .clientAssertion(assertion) + .scope(parameters.getScope()) + .grantType(GRANT_TYPE) + .params(credentialsRequestAdditionalParametersProvider.provide(parameters)) + .build(); + } + +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Fingerprint.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Fingerprint.java new file mode 100644 index 0000000..00e9920 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Fingerprint.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.eclipse.edc.spi.EdcException; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Produces SHA-1 fingerprints. + */ +public class Fingerprint { + private static final char[] HEX_CODES = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private Fingerprint() { + } + + /** + * Produces a SHA1 fingerprint of the given bytes using HEX encoding. Used for the x5t claim in a JWT. + */ + public static String sha1HexFingerprint(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] data = md.digest(bytes); + char[] encoded = new char[data.length << 1]; + for (int i = 0, encodedPos = 0; i < data.length; i++) { + encoded[encodedPos++] = HEX_CODES[(0xF0 & data[i]) >>> 4]; + encoded[encodedPos++] = HEX_CODES[0x0F & data[i]]; + } + return new String(encoded); + } catch (NoSuchAlgorithmException e) { + throw new EdcException(e); + } + } + + /** + * Produces a SHA1 fingerprint of the given bytes using Base 64 encoding. Used for the x5t claim in a JWT. + */ + public static String sha1Base64Fingerprint(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(bytes); + return Base64.getEncoder().encodeToString(md.digest()); + } catch (NoSuchAlgorithmException e) { + throw new EdcException(e); + } + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKey.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKey.java new file mode 100644 index 0000000..49849fa --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKey.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import java.util.List; + +/** + * Models an identity provider JWK key. + */ +public class JwkKey { + private String kty; + private String use; + private String kid; + private String x5t; + private String nn; + private String ee; + private List x5c; + private String issuer; + + public String getKty() { + return kty; + } + + public void setKty(String kty) { + this.kty = kty; + } + + public String getUse() { + return use; + } + + public void setUse(String use) { + this.use = use; + } + + public String getKid() { + return kid; + } + + public void setKid(String kid) { + this.kid = kid; + } + + public String getX5t() { + return x5t; + } + + public void setX5t(String x5t) { + this.x5t = x5t; + } + + public String getN() { + return nn; + } + + public void setN(String n) { + nn = n; + } + + public String getE() { + return ee; + } + + public void setE(String e) { + ee = e; + } + + public List getX5c() { + return x5c; + } + + public void setX5c(List x5c) { + this.x5c = x5c; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKeys.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKeys.java new file mode 100644 index 0000000..3c7f578 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/JwkKeys.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Models an identity provider JWK keys response. + */ +public class JwkKeys { + private List keys; + + @Nullable + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys; + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Oauth2JwtDecoratorRegistryRegistryImpl.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Oauth2JwtDecoratorRegistryRegistryImpl.java new file mode 100644 index 0000000..f4fa0ef --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/Oauth2JwtDecoratorRegistryRegistryImpl.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.eclipse.edc.iam.oauth2.spi.Oauth2JwtDecoratorRegistry; +import org.eclipse.edc.jwt.JwtDecoratorRegistryImpl; + +/** + * Registry for Oauth2 JWT decorators. + */ +public class Oauth2JwtDecoratorRegistryRegistryImpl extends JwtDecoratorRegistryImpl implements Oauth2JwtDecoratorRegistry { +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecorator.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecorator.java new file mode 100644 index 0000000..8012b30 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecorator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.eclipse.edc.jwt.spi.JwtDecorator; +import org.eclipse.edc.spi.EdcException; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Map; + +import static org.eclipse.edc.iam.oauth2.jwt.Fingerprint.sha1Base64Fingerprint; + +/** + * Creates the 'x5t' header containing the base64url-encoded SHA-1 thumbprint of the DER encoding of the thumbprint of the + * X.509 certificate corresponding to the key used to sign the JWT. This header is requested by some Oauth2 servers. + */ +public class X509CertificateDecorator implements JwtDecorator { + private final byte[] certificate; + + public X509CertificateDecorator(X509Certificate certificate) { + try { + this.certificate = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new EdcException(e); + } + } + + @Override + public Map headers() { + return Map.of("x5t", sha1Base64Fingerprint(certificate)); + } + + @Override + public Map claims() { + return Collections.emptyMap(); + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRule.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRule.java new file mode 100644 index 0000000..6bf8dd5 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRule.java @@ -0,0 +1,49 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +import static org.eclipse.edc.jwt.spi.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 checkRule(@NotNull ClaimToken toVerify, @Nullable Map 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(); + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRule.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRule.java new file mode 100644 index 0000000..8eb574f --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRule.java @@ -0,0 +1,62 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Clock; +import java.util.Map; + +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT; + +/** + * Token validation rule that checks if the token is not expired and if the "issued at" claim is valued correctly + */ +public class Oauth2ExpirationIssuedAtValidationRule implements TokenValidationRule { + + private final Clock clock; + + public Oauth2ExpirationIssuedAtValidationRule(Clock clock) { + this.clock = clock; + } + + @Override + public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { + var now = clock.instant(); + var expires = toVerify.getInstantClaim(EXPIRATION_TIME); + if (expires == null) { + return Result.failure("Required expiration time (exp) claim is missing in token"); + } else if (now.isAfter(expires)) { + return Result.failure("Token has expired (exp)"); + } + + var issuedAt = toVerify.getInstantClaim(ISSUED_AT); + if (issuedAt != null) { + if (issuedAt.isAfter(expires)) { + return Result.failure("Issued at (iat) claim is after expiration time (exp) claim in token"); + } else if (now.isBefore(issuedAt)) { + return Result.failure("Current date/time before issued at (iat) claim in token"); + } + } + + return Result.success(); + } + +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRule.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRule.java new file mode 100644 index 0000000..56a37ae --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRule.java @@ -0,0 +1,54 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Clock; +import java.util.Map; + +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE; + +/** + * Token validation rule that checks if the "not before" claim is valid + */ +public class Oauth2NotBeforeValidationRule implements TokenValidationRule { + private final Clock clock; + private final int notBeforeLeeway; + + public Oauth2NotBeforeValidationRule(Clock clock, int notBeforeLeeway) { + this.clock = clock; + this.notBeforeLeeway = notBeforeLeeway; + } + + @Override + public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { + var now = clock.instant(); + var leewayNow = now.plusSeconds(notBeforeLeeway); + var notBefore = toVerify.getInstantClaim(NOT_BEFORE); + + if (notBefore == null) { + return Result.failure("Required not before (nbf) claim is missing in token"); + } else if (leewayNow.isBefore(notBefore)) { + return Result.failure("Current date/time with leeway before the not before (nbf) claim in token"); + } + + return Result.success(); + } +} diff --git a/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ValidationRulesRegistryImpl.java b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ValidationRulesRegistryImpl.java new file mode 100644 index 0000000..7c56592 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ValidationRulesRegistryImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 implementation + * + */ + +package org.eclipse.edc.iam.oauth2.rule; + +import org.eclipse.edc.iam.oauth2.Oauth2ServiceConfiguration; +import org.eclipse.edc.iam.oauth2.spi.Oauth2ValidationRulesRegistry; +import org.eclipse.edc.jwt.TokenValidationRulesRegistryImpl; + +import java.time.Clock; + +/** + * Registry for Oauth2 validation rules. + */ +public class Oauth2ValidationRulesRegistryImpl extends TokenValidationRulesRegistryImpl implements Oauth2ValidationRulesRegistry { + + public Oauth2ValidationRulesRegistryImpl(Oauth2ServiceConfiguration configuration, Clock clock) { + this.addRule(new Oauth2AudienceValidationRule(configuration.getEndpointAudience())); + this.addRule(new Oauth2NotBeforeValidationRule(clock, configuration.getNotBeforeValidationLeeway())); + this.addRule(new Oauth2ExpirationIssuedAtValidationRule(clock)); + } +} diff --git a/external/oauth2/oauth2-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/external/oauth2/oauth2-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..b6ef0e6 --- /dev/null +++ b/external/oauth2/oauth2-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2020 - 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: +# Microsoft Corporation - initial API and implementation +# +# + +org.eclipse.edc.iam.oauth2.Oauth2ServiceExtension +org.eclipse.edc.iam.oauth2.Oauth2ServiceDefaultServicesExtension diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtensionTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtensionTest.java new file mode 100644 index 0000000..edf9326 --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/Oauth2ServiceExtensionTest.java @@ -0,0 +1,72 @@ +/* + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.security.CertificateResolver; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +class Oauth2ServiceExtensionTest { + + private final CertificateResolver certificateResolver = mock(); + private final PrivateKeyResolver privateKeyResolver = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(CertificateResolver.class, certificateResolver); + context.registerService(PrivateKeyResolver.class, privateKeyResolver); + } + + @Test + void verifyExtensionWithCertificateAlias(Oauth2ServiceExtension extension, ServiceExtensionContext context) throws CertificateEncodingException { + var config = spy(ConfigFactory.fromMap(Map.of( + "edc.oauth.client.id", "id", + "edc.oauth.token.url", "url", + "edc.oauth.certificate.alias", "alias", + "edc.oauth.private.key.alias", "p_alias"))); + when(context.getConfig(any())).thenReturn(config); + var certificate = mock(X509Certificate.class); + var privateKey = mock(PrivateKey.class); + when(privateKey.getAlgorithm()).thenReturn("RSA"); + when(certificate.getEncoded()).thenReturn(new byte[] {}); + when(certificateResolver.resolveCertificate("alias")).thenReturn(certificate); + when(privateKeyResolver.resolvePrivateKey("p_alias", PrivateKey.class)).thenReturn(privateKey); + + extension.initialize(context); + + verify(config, times(1)).getString("edc.oauth.certificate.alias"); + verify(config, never()).getString("edc.oauth.public.key.alias"); + } + +} diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverTest.java new file mode 100644 index 0000000..cf9376f --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/IdentityProviderKeyResolverTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 - 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: + * Microsoft Corporation - initial API and implementation + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package org.eclipse.edc.iam.oauth2.identity; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.eclipse.edc.iam.oauth2.jwt.JwkKeys; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.types.TypeManager; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.junit.testfixtures.TestUtils.testHttpClient; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class IdentityProviderKeyResolverTest { + + private static final String JWKS_URL = "https://test.jwks.url"; + private static final String JWKS_FILE = "jwks_response.json"; + private final Interceptor interceptor = mock(Interceptor.class); + private final TypeManager typeManager = new TypeManager(); + private final EdcHttpClient httpClient = testHttpClient(interceptor); + private JwkKeys testKeys; + private IdentityProviderKeyResolver resolver; + + @BeforeEach + void setUp() { + resolver = new IdentityProviderKeyResolver(mock(Monitor.class), httpClient, typeManager, new IdentityProviderKeyResolverConfiguration(JWKS_URL, 1)); + + try (var stream = getClass().getClassLoader().getResourceAsStream(JWKS_FILE)) { + testKeys = new ObjectMapper().readValue(stream, JwkKeys.class); + } catch (IOException e) { + throw new EdcException("Failed to load keys from file"); + } + } + + @Test + void getKeys_shouldGetUpdatedKeys() throws IOException { + when(interceptor.intercept(any())).thenReturn(response(200, testKeys)); + + var result = resolver.getKeys(); + + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(5).containsKey("nOo3ZDrODXEK1jKWhXslHR_KXEg"); + } + + @Test + void getKeys_shouldReturnFailureIfServerReturnsError() throws IOException { + when(interceptor.intercept(any())).thenReturn(response(500, emptyMap())); + + var result = resolver.getKeys(); + + assertThat(result.failed()).isTrue(); + } + + @Test + void getKeys_shouldReturnFailureIfBodyCannotBeDeserialized() throws IOException { + when(interceptor.intercept(any())).thenReturn(response(200, null)); + + var result = resolver.getKeys(); + + assertThat(result.failed()).isTrue(); + } + + @Test + void getKeys_shouldReturnFailureIfNoKeysAreContainedInTheResult() throws IOException { + when(interceptor.intercept(any())).thenReturn(response(200, Map.of("keys", emptyList()))); + + var result = resolver.getKeys(); + + assertThat(result.failed()).isTrue(); + } + + @Test + void start_shouldThrowExceptionIfItFailsToLoadKeysTheFirstTime() throws IOException { + when(interceptor.intercept(any())).thenReturn(response(500, emptyMap())); + + assertThatThrownBy(() -> resolver.start()).isInstanceOf(EdcException.class); + } + + @NotNull + private Response response(int code, Object body) { + return new Response.Builder() + .code(code) + .body(ResponseBody.create(typeManager.writeValueAsString(body), MediaType.get("application/json"))) + .message("Test message") + .protocol(Protocol.HTTP_1_1) + .request(new Request.Builder().url("http://test.some.url").build()) + .build(); + } +} diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java new file mode 100644 index 0000000..1c3a157 --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/identity/Oauth2ServiceImplTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2020 - 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: + * Microsoft Corporation - initial API and implementation + * Fraunhofer Institute for Software and Systems Engineering - Improvements + * Microsoft Corporation - Use IDS Webhook address for JWT audience claim + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements + * + */ + +package org.eclipse.edc.iam.oauth2.identity; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.iam.oauth2.Oauth2ServiceConfiguration; +import org.eclipse.edc.iam.oauth2.rule.Oauth2ValidationRulesRegistryImpl; +import org.eclipse.edc.iam.oauth2.spi.CredentialsRequestAdditionalParametersProvider; +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.jwt.JwtDecoratorRegistryImpl; +import org.eclipse.edc.jwt.TokenValidationServiceImpl; +import org.eclipse.edc.jwt.spi.JwtDecorator; +import org.eclipse.edc.jwt.spi.TokenGenerationService; +import org.eclipse.edc.spi.iam.PublicKeyResolver; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.eclipse.edc.spi.iam.TokenRepresentation; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.security.CertificateResolver; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.time.Clock; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class Oauth2ServiceImplTest { + + private static final String CLIENT_ID = "client-test"; + private static final String PRIVATE_KEY_ALIAS = "pk-test"; + private static final String PUBLIC_CERTIFICATE_ALIAS = "cert-test"; + private static final String PROVIDER_AUDIENCE = "audience-test"; + private static final String ENDPOINT_AUDIENCE = "endpoint-audience-test"; + private static final String OAUTH2_SERVER_URL = "http://oauth2-server.com"; + + private final Instant now = Instant.now(); + private final Oauth2Client client = mock(Oauth2Client.class); + private final TokenGenerationService tokenGenerationService = mock(TokenGenerationService.class); + private final CredentialsRequestAdditionalParametersProvider credentialsRequestAdditionalParametersProvider = mock(CredentialsRequestAdditionalParametersProvider.class); + private final JwtDecorator jwtDecorator = mock(JwtDecorator.class); + private Oauth2ServiceImpl authService; + private JWSSigner jwsSigner; + + @BeforeEach + void setUp() throws JOSEException { + var testKey = testKey(); + + jwsSigner = new RSASSASigner(testKey.toPrivateKey()); + var publicKeyResolverMock = mock(PublicKeyResolver.class); + var privateKeyResolverMock = mock(PrivateKeyResolver.class); + var certificateResolverMock = mock(CertificateResolver.class); + when(publicKeyResolverMock.resolveKey(anyString())).thenReturn(testKey.toPublicKey()); + var configuration = Oauth2ServiceConfiguration.Builder.newInstance() + .tokenUrl(OAUTH2_SERVER_URL) + .clientId(CLIENT_ID) + .privateKeyAlias(PRIVATE_KEY_ALIAS) + .publicCertificateAlias(PUBLIC_CERTIFICATE_ALIAS) + .providerAudience(PROVIDER_AUDIENCE) + .endpointAudience(ENDPOINT_AUDIENCE) + .privateKeyResolver(privateKeyResolverMock) + .certificateResolver(certificateResolverMock) + .identityProviderKeyResolver(publicKeyResolverMock) + .build(); + + var clock = Clock.fixed(now, UTC); + var validationRulesRegistry = new Oauth2ValidationRulesRegistryImpl(configuration, clock); + var tokenValidationService = new TokenValidationServiceImpl(publicKeyResolverMock, validationRulesRegistry); + + var jwtDecoratorRegistry = new JwtDecoratorRegistryImpl(); + jwtDecoratorRegistry.register(jwtDecorator); + + authService = new Oauth2ServiceImpl(configuration, tokenGenerationService, client, jwtDecoratorRegistry, tokenValidationService, credentialsRequestAdditionalParametersProvider); + } + + @Test + void obtainClientCredentials() { + when(credentialsRequestAdditionalParametersProvider.provide(any())).thenReturn(emptyMap()); + when(tokenGenerationService.generate(any())).thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token("assertionToken").build())); + + var tokenParameters = TokenParameters.Builder.newInstance() + .audience("audience") + .scope("scope") + .build(); + + when(client.requestToken(any())).thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token("accessToken").build())); + + var result = authService.obtainClientCredentials(tokenParameters); + + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent().getToken()).isEqualTo("accessToken"); + var captor = ArgumentCaptor.forClass(Oauth2CredentialsRequest.class); + verify(client).requestToken(captor.capture()); + var captured = captor.getValue(); + assertThat(captured).isNotNull() + .isInstanceOf(PrivateKeyOauth2CredentialsRequest.class); + var capturedRequest = (PrivateKeyOauth2CredentialsRequest) captured; + assertThat(capturedRequest.getGrantType()).isEqualTo("client_credentials"); + assertThat(capturedRequest.getScope()).isEqualTo("scope"); + assertThat(capturedRequest.getClientAssertion()).isEqualTo("assertionToken"); + assertThat(capturedRequest.getClientAssertionType()).isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + } + + @Test + void obtainClientCredentials_addsAdditionalFormParameters() { + when(credentialsRequestAdditionalParametersProvider.provide(any())).thenReturn(Map.of("parameterKey", "parameterValue")); + when(tokenGenerationService.generate(any())).thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token("assertionToken").build())); + + var tokenParameters = TokenParameters.Builder.newInstance() + .audience("audience") + .scope("scope") + .build(); + + when(client.requestToken(any())).thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token("accessToken").build())); + + var result = authService.obtainClientCredentials(tokenParameters); + + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent().getToken()).isEqualTo("accessToken"); + var captor = ArgumentCaptor.forClass(Oauth2CredentialsRequest.class); + verify(client).requestToken(captor.capture()); + var captured = captor.getValue(); + assertThat(captured).isNotNull() + .isInstanceOf(PrivateKeyOauth2CredentialsRequest.class); + var capturedRequest = (PrivateKeyOauth2CredentialsRequest) captured; + assertThat(capturedRequest.getGrantType()).isEqualTo("client_credentials"); + assertThat(capturedRequest.getScope()).isEqualTo("scope"); + assertThat(capturedRequest.getClientAssertion()).isEqualTo("assertionToken"); + assertThat(capturedRequest.getParams()).containsEntry("parameterKey", "parameterValue"); + } + + @Test + void obtainClientCredentials_verifyReturnsFailureIfOauth2ClientFails() { + when(credentialsRequestAdditionalParametersProvider.provide(any())).thenReturn(emptyMap()); + when(tokenGenerationService.generate(any())).thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token("assertionToken").build())); + + var tokenParameters = TokenParameters.Builder.newInstance() + .audience("audience") + .scope("scope") + .build(); + + when(client.requestToken(any())).thenReturn(Result.failure("test error")); + + var result = authService.obtainClientCredentials(tokenParameters); + + assertThat(result.failed()).isTrue(); + assertThat(result.getFailureDetail()).contains("test error"); + } + + @Test + void verifyNoAudienceToken() { + var jwt = createJwt(null, Date.from(now.minusSeconds(1000)), Date.from(now.plusSeconds(1000))); + + var result = authService.verifyJwtToken(jwt, ENDPOINT_AUDIENCE); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).isNotEmpty(); + } + + @Test + void verifyInvalidAudienceToken() { + var jwt = createJwt("different.audience", Date.from(now.minusSeconds(1000)), Date.from(now.plusSeconds(1000))); + + var result = authService.verifyJwtToken(jwt, ENDPOINT_AUDIENCE); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).isNotEmpty(); + } + + @Test + void verifyInvalidAttemptUseNotBeforeToken() { + var jwt = createJwt(PROVIDER_AUDIENCE, Date.from(now.plusSeconds(1000)), Date.from(now.plusSeconds(1000))); + + var result = authService.verifyJwtToken(jwt, ENDPOINT_AUDIENCE); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).isNotEmpty(); + } + + @Test + void verifyExpiredToken() { + var jwt = createJwt(PROVIDER_AUDIENCE, Date.from(now.minusSeconds(1000)), Date.from(now.minusSeconds(1000))); + + var result = authService.verifyJwtToken(jwt, ENDPOINT_AUDIENCE); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).isNotEmpty(); + } + + @Test + void verifyValidJwt() { + var jwt = createJwt(ENDPOINT_AUDIENCE, Date.from(now.minusSeconds(1000)), new Date(System.currentTimeMillis() + 1000000)); + + var result = authService.verifyJwtToken(jwt, ENDPOINT_AUDIENCE); + + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent().getClaims()).hasSize(3).containsKeys(AUDIENCE, NOT_BEFORE, EXPIRATION_TIME); + } + + private PrivateKeyOauth2CredentialsRequest createRequest(Map additional) { + return PrivateKeyOauth2CredentialsRequest.Builder.newInstance() + .grantType("client_credentials") + .clientAssertion("assertion-token") + .scope("scope") + .params(additional) + .build(); + } + + private RSAKey testKey() throws JOSEException { + return new RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key + .keyID(UUID.randomUUID().toString()) // give the key a unique ID + .generate(); + } + + private TokenRepresentation createJwt(String aud, Date nbf, Date exp) { + var claimsSet = new JWTClaimsSet.Builder() + .audience(aud) + .notBeforeTime(nbf) + .expirationTime(exp).build(); + var header = new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("an-id").build(); + + try { + var jwt = new SignedJWT(header, claimsSet); + jwt.sign(jwsSigner); + return TokenRepresentation.Builder.newInstance().token(jwt.serialize()).build(); + } catch (JOSEException e) { + throw new AssertionError(e); + } + } +} diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/FingerprintTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/FingerprintTest.java new file mode 100644 index 0000000..987b664 --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/FingerprintTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FingerprintTest { + X509Certificate certificate; + + @Test + void verifySha1HexFingerprint() throws Exception { + String fingerprint = Fingerprint.sha1HexFingerprint(certificate.getEncoded()); + assertEquals("e1b2b8e5ec19d345a4f9afec8694f0d9c0aa25cf", fingerprint); // expected SHA1 fingerprint of the test cert + } + + @Test + void verifySha1Base64Fingerprint() throws Exception { + String fingerprint = Fingerprint.sha1Base64Fingerprint(certificate.getEncoded()); + assertEquals("4bK45ewZ00Wk+a/shpTw2cCqJc8=", fingerprint); // expected SHA1 fingerprint of the test cert + } + + @BeforeEach + void setUp() throws CertificateException { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + certificate = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(X509_CERTIFICATE.getBytes()))); + } + + // X.509 cert with header and footer stripped (required by CertificateFactory) + static final String X509_CERTIFICATE = "MIICVjCCAb8CAg37MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG" + + "A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE" + + "MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl" + + "YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw" + + "ODIyMDUyNzIzWhcNMTcwODIxMDUyNzIzWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE" + + "CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs" + + "ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMYBBrx5PlP0WNI/ZdzD" + + "+6Pktmurn+F2kQYbtc7XQh8/LTBvCo+P6iZoLEmUA9e7EXLRxgU1CVqeAi7QcAn9" + + "MwBlc8ksFJHB0rtf9pmf8Oza9E0Bynlq/4/Kb1x+d+AyhL7oK9tQwB24uHOueHi1" + + "C/iVv8CSWKiYe6hzN1txYe8rAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAASPdjigJ" + + "kXCqKWpnZ/Oc75EUcMi6HztaW8abUMlYXPIgkV2F7YanHOB7K4f7OOLjiz8DTPFf" + + "jC9UeuErhaA/zzWi8ewMTFZW/WshOrm3fNvcMrMLKtH534JKvcdMg6qIdjTFINIr" + + "evnAhf0cwULaebn+lMs8Pdl7y37+sfluVok="; + +} diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecoratorTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecoratorTest.java new file mode 100644 index 0000000..1570943 --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/jwt/X509CertificateDecoratorTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 implementation + * + */ + +package org.eclipse.edc.iam.oauth2.jwt; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +class X509CertificateDecoratorTest { + + private static final String TEST_CERT_FILE = "cert.pem"; + private static final String HEADER = "-----BEGIN CERTIFICATE-----"; + private static final String FOOTER = "-----END CERTIFICATE-----"; + + @Test + void verifyDecorator() throws CertificateException, IOException { + var certificate = createCertificate(); + var decorator = new X509CertificateDecorator(certificate); + + assertThat(decorator.claims()).isEmpty(); + assertThat(decorator.headers()).containsOnlyKeys("x5t"); + } + + private static X509Certificate createCertificate() throws CertificateException, IOException { + var classloader = Thread.currentThread().getContextClassLoader(); + var pem = new String(Objects.requireNonNull(classloader.getResourceAsStream(TEST_CERT_FILE)).readAllBytes()); + var encoded = pem.replace(HEADER, "").replaceAll(System.lineSeparator(), "").replace(FOOTER, ""); + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + return (X509Certificate) fact.generateCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encoded.getBytes()))); + } +} \ No newline at end of file diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRuleTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRuleTest.java new file mode 100644 index 0000000..bf93c07 --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2AudienceValidationRuleTest.java @@ -0,0 +1,68 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE; + +class Oauth2AudienceValidationRuleTest { + + private final String endpointAudience = "test-audience"; + private final TokenValidationRule rule = new Oauth2AudienceValidationRule(endpointAudience); + + @Test + void validAudience() { + var token = ClaimToken.Builder.newInstance() + .claim(AUDIENCE, List.of(endpointAudience)) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + void validationKoBecauseAudienceNotRespected() { + var token = ClaimToken.Builder.newInstance() + .claim(AUDIENCE, List.of("fake-audience")) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Token audience (aud) claim did not contain connector audience: test-audience"); + } + + @Test + void validationKoBecauseAudienceNotProvided() { + var token = ClaimToken.Builder.newInstance() + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Required audience (aud) claim is missing in token"); + } + +} \ No newline at end of file diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRuleTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRuleTest.java new file mode 100644 index 0000000..023cb2f --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2ExpirationIssuedAtValidationRuleTest.java @@ -0,0 +1,100 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT; + +class Oauth2ExpirationIssuedAtValidationRuleTest { + + private final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private final Clock clock = Clock.fixed(now, UTC); + private final TokenValidationRule rule = new Oauth2ExpirationIssuedAtValidationRule(clock); + + @Test + void validationOk() { + var token = ClaimToken.Builder.newInstance() + .claim(EXPIRATION_TIME, Date.from(now.plusSeconds(600))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + void validationKoBecauseExpirationTimeNotRespected() { + var token = ClaimToken.Builder.newInstance() + .claim(EXPIRATION_TIME, Date.from(now.minusSeconds(10))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Token has expired (exp)"); + } + + @Test + void validationKoBecauseExpirationTimeNotProvided() { + var token = ClaimToken.Builder.newInstance().build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Required expiration time (exp) claim is missing in token"); + } + + @Test + void validationKoBecauseIssuedAtAfterExpires() { + var token = ClaimToken.Builder.newInstance() + .claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60))) + .claim(ISSUED_AT, Date.from(now.plusSeconds(65))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1).contains("Issued at (iat) claim is after expiration time (exp) claim in token"); + } + + @Test + void validationKoBecauseIssuedAtInFuture() { + var token = ClaimToken.Builder.newInstance() + .claim(EXPIRATION_TIME, Date.from(now.plusSeconds(60))) + .claim(ISSUED_AT, Date.from(now.plusSeconds(10))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1).contains("Current date/time before issued at (iat) claim in token"); + } + + +} \ No newline at end of file diff --git a/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRuleTest.java b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRuleTest.java new file mode 100644 index 0000000..312cf4a --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/java/org/eclipse/edc/iam/oauth2/rule/Oauth2NotBeforeValidationRuleTest.java @@ -0,0 +1,73 @@ +/* + * 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.edc.iam.oauth2.rule; + +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static java.time.ZoneOffset.UTC; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.NOT_BEFORE; + +class Oauth2NotBeforeValidationRuleTest { + + private final int notBeforeLeeway = 20; + private final Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private final Clock clock = Clock.fixed(now, UTC); + private final TokenValidationRule rule = new Oauth2NotBeforeValidationRule(clock, notBeforeLeeway); + + @Test + void validNotBefore() { + var token = ClaimToken.Builder.newInstance() + .claim(NOT_BEFORE, Date.from(now.plusSeconds(notBeforeLeeway))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + void validationKoBecauseNotBeforeTimeNotRespected() { + var token = ClaimToken.Builder.newInstance() + .claim(NOT_BEFORE, Date.from(now.plusSeconds(notBeforeLeeway + 1))) + .build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Current date/time with leeway before the not before (nbf) claim in token"); + } + + @Test + void validationKoBecauseNotBeforeTimeNotProvided() { + var token = ClaimToken.Builder.newInstance().build(); + + var result = rule.checkRule(token, emptyMap()); + + assertThat(result.succeeded()).isFalse(); + assertThat(result.getFailureMessages()).hasSize(1) + .contains("Required not before (nbf) claim is missing in token"); + } + +} \ No newline at end of file diff --git a/external/oauth2/oauth2-core/src/test/resources/cert.pem b/external/oauth2/oauth2-core/src/test/resources/cert.pem new file mode 100644 index 0000000..a1a70cc --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/resources/cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7DCCAVWgAwIBAgIGAYHYExlWMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMM +BFRlc3QwHhcNMjIwNzA3MDk1MjE5WhcNMzIwNzA0MDk1MjE5WjAPMQ0wCwYDVQQD +DARUZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHggAW6QF9E2N2y37U +xcacS+LFZwyVIFJkd/Zbe4JF0N+qqPGgRO8kMYiXb12geuJ+lxobLQkOlsnztkfX +htVLkYoruSthMQORC/fZDhP1eV5KpR0LVACoQmLJBTbKE2tOJh5HODxvzhiV+Bi5 +DAWOhmkA1dYo1UFg8ORt/YzOvQIDAQABo1MwUTAdBgNVHQ4EFgQUwxs2XndsvlwH +4JqFpqMXF9mEDVAwHwYDVR0jBBgwFoAUwxs2XndsvlwH4JqFpqMXF9mEDVAwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCajJWQ7x7TEZvCRXWr3J43 +WFoOZBjhKwDtNySoeF/mJvxEcWzeCfxvqO/zQ16+vMj/+1kW7+eex8dSYBfRtb83 +MjOtKQYd4PU5uH4QqFFyJ3oH72ZItTAikfuRcrV0Gu7lsLSkLjglUFAREr8aI0QC +0SDLUMXw7nNsSJ/s2yIiVw== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/external/oauth2/oauth2-core/src/test/resources/jwks_response.json b/external/oauth2/oauth2-core/src/test/resources/jwks_response.json new file mode 100644 index 0000000..70a222c --- /dev/null +++ b/external/oauth2/oauth2-core/src/test/resources/jwks_response.json @@ -0,0 +1,64 @@ +{ + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg", + "x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg", + "n": "oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ", + "e": "AQAB", + "x5c": [ + "MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "l3sQ-50cCH4xBVZLHTGwnSR7680", + "x5t": "l3sQ-50cCH4xBVZLHTGwnSR7680", + "n": "sfsXMXWuO-dniLaIELa3Pyqz9Y_rWff_AVrCAnFSdPHa8__Pmkbt_yq-6Z3u1o4gjRpKWnrjxIh8zDn1Z1RS26nkKcNg5xfWxR2K8CPbSbY8gMrp_4pZn7tgrEmoLMkwfgYaVC-4MiFEo1P2gd9mCdgIICaNeYkG1bIPTnaqquTM5KfT971MpuOVOdM1ysiejdcNDvEb7v284PYZkw2imwqiBY3FR0sVG7jgKUotFvhd7TR5WsA20GS_6ZIkUUlLUbG_rXWGl0YjZLS_Uf4q8Hbo7u-7MaFn8B69F6YaFdDlXm_A0SpedVFWQFGzMsp43_6vEzjfrFDJVAYkwb6xUQ", + "e": "AQAB", + "x5c": [ + "MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "DqUu8gf-nAgcyjP3-SuplNAXAnc", + "x5t": "DqUu8gf-nAgcyjP3-SuplNAXAnc", + "n": "1n7-nWSLeuWQzBRlYSbS8RjvWvkQeD7QL9fOWaGXbW73VNGH0YipZisPClFv6GzwfWECTWQp19WFe_lASka5-KEWkQVzCbEMaaafOIs7hC61P5cGgw7dhuW4s7f6ZYGZEzQ4F5rHE-YNRbvD51qirPNzKHk3nji1wrh0YtbPPIf--NbI98bCwLLh9avedOmqESzWOGECEMXv8LSM-B9SKg_4QuBtyBwwIakTuqo84swTBM5w8PdhpWZZDtPgH87Wz-_WjWvk99AjXl7l8pWPQJiKNujt_ck3NDFpzaLEppodhUsID0ptRA008eCU6l8T-ux19wZmb_yBnHcV3pFWhQ", + "e": "AQAB", + "x5c": [ + "MIIC8TCCAdmgAwIBAgIQYVk/tJ1e4phISvVrAALNKTANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMjAxMjIxMDAwMDAwWhcNMjUxMjIxMDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWfv6dZIt65ZDMFGVhJtLxGO9a+RB4PtAv185ZoZdtbvdU0YfRiKlmKw8KUW/obPB9YQJNZCnX1YV7+UBKRrn4oRaRBXMJsQxppp84izuELrU/lwaDDt2G5bizt/plgZkTNDgXmscT5g1Fu8PnWqKs83MoeTeeOLXCuHRi1s88h/741sj3xsLAsuH1q9506aoRLNY4YQIQxe/wtIz4H1IqD/hC4G3IHDAhqRO6qjzizBMEznDw92GlZlkO0+AfztbP79aNa+T30CNeXuXylY9AmIo26O39yTc0MWnNosSmmh2FSwgPSm1EDTTx4JTqXxP67HX3BmZv/IGcdxXekVaFAgMBAAGjITAfMB0GA1UdDgQWBBQ2r//lgTPcKughDkzmCtRlw+P9SzANBgkqhkiG9w0BAQsFAAOCAQEAsFdRyczNWh/qpYvcIZbDvWYzlrmFZc6blcUzns9zf7sUWtQZrZPu5DbetV2Gr2r3qtMDKXCUaR+pqoy3I2zxTX3x8bTNhZD9YAgAFlTLNSydTaK5RHyB/5kr6B7ZJeNIk3PRVhRGt6ybCJSjV/VYVkLR5fdLP+5GhvBESobAR/d0ntriTzp7/tLMb/oXx7w5Hu1m3I8rpMocoXfH2SH1GLmMXj6Mx1dtwCDYM6bsb3fhWRz9O9OMR6QNiTnq8q9wn1QzBAnRcswYzT1LKKBPNFSasCvLYOCPOZCL+W8N8jqa9ZRYNYKWXzmiSptgBEM24t3m5FUWzWqoLu9pIcnkPQ==" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "1LTMzakihiRla_8z2BEJVXeWMqo", + "x5t": "1LTMzakihiRla_8z2BEJVXeWMqo", + "n": "3sKcJSD4cHwTY5jYm5lNEzqk3wON1CaARO5EoWIQt5u-X-ZnW61CiRZpWpfhKwRYU153td5R8p-AJDWT-NcEJ0MHU3KiuIEPmbgJpS7qkyURuHRucDM2lO4L4XfIlvizQrlyJnJcd09uLErZEO9PcvKiDHoois2B4fGj7CsAe5UZgExJvACDlsQSku2JUyDmZUZP2_u_gCuqNJM5o0hW7FKRI3MFoYCsqSEmHnnumuJ2jF0RHDRWQpodhlAR6uKLoiWHqHO3aG7scxYMj5cMzkpe1Kq_Dm5yyHkMCSJ_JaRhwymFfV_SWkqd3n-WVZT0ADLEq0RNi9tqZ43noUnO_w", + "e": "AQAB", + "x5c": [ + "MIIDYDCCAkigAwIBAgIJAIB4jVVJ3BeuMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0xNjA0MDUxNDQzMzVaFw0yMTA0MDQxNDQzMzVaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN7CnCUg+HB8E2OY2JuZTRM6pN8DjdQmgETuRKFiELebvl/mZ1utQokWaVqX4SsEWFNed7XeUfKfgCQ1k/jXBCdDB1NyoriBD5m4CaUu6pMlEbh0bnAzNpTuC+F3yJb4s0K5ciZyXHdPbixK2RDvT3Lyogx6KIrNgeHxo+wrAHuVGYBMSbwAg5bEEpLtiVMg5mVGT9v7v4ArqjSTOaNIVuxSkSNzBaGArKkhJh557pridoxdERw0VkKaHYZQEerii6Ilh6hzt2hu7HMWDI+XDM5KXtSqvw5ucsh5DAkifyWkYcMphX1f0lpKnd5/llWU9AAyxKtETYvbameN56FJzv8CAwEAAaOBijCBhzAdBgNVHQ4EFgQU9IdLLpbC2S8Wn1MCXsdtFac9SRYwWQYDVR0jBFIwUIAU9IdLLpbC2S8Wn1MCXsdtFac9SRahLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAIB4jVVJ3BeuMAsGA1UdDwQEAwIBxjANBgkqhkiG9w0BAQsFAAOCAQEAXk0sQAib0PGqvwELTlflQEKS++vqpWYPW/2gCVCn5shbyP1J7z1nT8kE/ZDVdl3LvGgTMfdDHaRF5ie5NjkTHmVOKbbHaWpTwUFbYAFBJGnx+s/9XSdmNmW9GlUjdpd6lCZxsI6888r0ptBgKINRRrkwMlq3jD1U0kv4JlsIhafUIOqGi4+hIDXBlY0F/HJPfUU75N885/r4CCxKhmfh3PBM35XOch/NGC67fLjqLN+TIWLoxnvil9m3jRjqOA9u50JUeDGZABIYIMcAdLpI2lcfru4wXcYXuQul22nAR7yOyGKNOKULoOTE4t4AeGRqCogXSxZgaTgKSBhvhE+MGg==" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "xP_zn6I1YkXcUUmlBoPuXTGsaxk", + "x5t": "xP_zn6I1YkXcUUmlBoPuXTGsaxk", + "n": "2pWatafeb3mB0A73-Z-URwrubwDldWvivRu19GNC61MBOb3fZ4I4lyhUhNuS7aJRPJIFB6zl-HFx1nHpGg74BHe0z9skODHYZEACd2iKBIet55DdduIe1CXsZ9keyEmNaGv3XS4OW_7IDM0j5wR9OHugUifkH3PQIcFvTYanHmXojTmgjIOWoz7y0okpyN9-FbZRzdfx-ej-njaj5gR8r69muwO5wlTbIG20V40R6zYh-QODMUpayy7jDGFGw5vjFH9Ca0tLZcNQq__JKE_mp-0fODOAQobOrBUoASFkyCd95BVW7KJrndvW7ofRWaCTuZZOy5SnU4asbjMrgxFZFw", + "e": "AQAB", + "x5c": [ + "MIIDYDCCAkigAwIBAgIJAJzCyTLC+DjJMA0GCSqGSIb3DQEBCwUAMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTAeFw0xNjA3MTMyMDMyMTFaFw0yMTA3MTIyMDMyMTFaMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANqVmrWn3m95gdAO9/mflEcK7m8A5XVr4r0btfRjQutTATm932eCOJcoVITbku2iUTySBQes5fhxcdZx6RoO+AR3tM/bJDgx2GRAAndoigSHreeQ3XbiHtQl7GfZHshJjWhr910uDlv+yAzNI+cEfTh7oFIn5B9z0CHBb02Gpx5l6I05oIyDlqM+8tKJKcjffhW2Uc3X8fno/p42o+YEfK+vZrsDucJU2yBttFeNEes2IfkDgzFKWssu4wxhRsOb4xR/QmtLS2XDUKv/yShP5qftHzgzgEKGzqwVKAEhZMgnfeQVVuyia53b1u6H0Vmgk7mWTsuUp1OGrG4zK4MRWRcCAwEAAaOBijCBhzAdBgNVHQ4EFgQU11z579/IePwuc4WBdN4L0ljG4CUwWQYDVR0jBFIwUIAU11z579/IePwuc4WBdN4L0ljG4CWhLaQrMCkxJzAlBgNVBAMTHkxpdmUgSUQgU1RTIFNpZ25pbmcgUHVibGljIEtleYIJAJzCyTLC+DjJMAsGA1UdDwQEAwIBxjANBgkqhkiG9w0BAQsFAAOCAQEAiASLEpQseGNahE+9f9PQgmX3VgjJerNjXr1zXWXDJfFE31DxgsxddjcIgoBL9lwegOHHvwpzK1ecgH45xcJ0Z/40OgY8NITqXbQRfdgLrEGJCoyOQEbjb5PW5k2aOdn7LBxvDsH6Y8ax26v+EFMPh3G+xheh6bfoIRSK1b+44PfoDZoJ9NfJibOZ4Cq+wt/yOvpMYQDB/9CNo18wmA3RCLYjf2nAc7RO0PDYHSIq5QDWV+1awmXDKgIdRpYPpRtn9KFXQkpCeEc/lDTG+o6n7nC40wyjioyR6QmHGvNkMR4VfSoTKCTnFATyDpI1bqU2K7KNjUEsCYfwybFB8d6mjQ==" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + } + ] +} \ No newline at end of file diff --git a/external/oauth2/oauth2-daps/README.md b/external/oauth2/oauth2-daps/README.md new file mode 100644 index 0000000..565ab5f --- /dev/null +++ b/external/oauth2/oauth2-daps/README.md @@ -0,0 +1,8 @@ +# Dynamic Attribute Provisioning Service (DAPS) + +## How to run integration tests +Run omejdn server: +``` +export DAPS_RESOURCES=$PWD/extensions/common/iam/oauth2/oauth2-daps/src/test/resources +docker run --rm -p 4567:4567 -v $DAPS_RESOURCES/config:/opt/config -v $DAPS_RESOURCES/keys:/opt/keys ghcr.io/fraunhofer-aisec/omejdn-server:1.4.2 +``` \ No newline at end of file diff --git a/external/oauth2/oauth2-daps/build.gradle.kts b/external/oauth2/oauth2-daps/build.gradle.kts new file mode 100644 index 0000000..452cbb1 --- /dev/null +++ b/external/oauth2/oauth2-daps/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020, 2021 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: + * Microsoft Corporation - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":spi:common:core-spi")) + api(project(":spi:common:oauth2-spi")) + + testImplementation(project(":extensions:common:vault:vault-filesystem")) + testImplementation(project(":extensions:common:iam:oauth2:oauth2-core")) + testImplementation(project(":core:common:junit")) +} + + diff --git a/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsExtension.java b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsExtension.java new file mode 100644 index 0000000..792d981 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsExtension.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, 2021 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 + * + */ + +package org.eclipse.edc.iam.oauth2.daps; + +import org.eclipse.edc.iam.oauth2.spi.Oauth2JwtDecoratorRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import static java.lang.String.format; + +/** + * Provides specialization of Oauth2 extension to interact with DAPS instance + * + * @deprecated This DAPS specific extension will be deleted and be replaced by configuration values + */ +@Extension(value = DapsExtension.NAME) +@Deprecated(since = "0.1.0") +public class DapsExtension implements ServiceExtension { + + public static final String NAME = "DAPS"; + public static final String DEFAULT_TOKEN_SCOPE = "idsc:IDS_CONNECTOR_ATTRIBUTES_ALL"; + @Setting(value = "The value of the scope claim that is passed to DAPS to obtain a DAT", defaultValue = DEFAULT_TOKEN_SCOPE) + public static final String DAPS_TOKEN_SCOPE = "edc.iam.token.scope"; + + @Inject + private Oauth2JwtDecoratorRegistry jwtDecoratorRegistry; + + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + jwtDecoratorRegistry.register(new DapsJwtDecorator()); + } + + @Provider + public TokenDecorator createDapsTokenDecorator(ServiceExtensionContext context) { + var scope = context.getSetting(DAPS_TOKEN_SCOPE, null); + if (scope == null) { + context.getMonitor().warning(() -> format("The config value '%s' was not supplied, falling back to the default '%s'. " + + "Please be aware that this default will be removed in future releases", DAPS_TOKEN_SCOPE, DEFAULT_TOKEN_SCOPE)); + scope = DEFAULT_TOKEN_SCOPE; + } + + return new DapsTokenDecorator(scope); + } +} diff --git a/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecorator.java b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecorator.java new file mode 100644 index 0000000..bd9d30d --- /dev/null +++ b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecorator.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020, 2021 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 + * + */ + +package org.eclipse.edc.iam.oauth2.daps; + +import org.eclipse.edc.jwt.spi.JwtDecorator; + +import java.util.Map; + +import static java.util.Collections.emptyMap; + +public class DapsJwtDecorator implements JwtDecorator { + + @Override + public Map claims() { + return Map.of( + "@context", "https://w3id.org/idsa/contexts/context.jsonld", + "@type", "ids:DatRequestToken" + ); + } + + @Override + public Map headers() { + return emptyMap(); + } +} diff --git a/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecorator.java b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecorator.java new file mode 100644 index 0000000..7340afd --- /dev/null +++ b/external/oauth2/oauth2-daps/src/main/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecorator.java @@ -0,0 +1,37 @@ +/* + * 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.iam.oauth2.daps; + +import org.eclipse.edc.spi.iam.TokenDecorator; +import org.eclipse.edc.spi.iam.TokenParameters; + +/** + * Token decorator that sets the {@code scope} claim on the token that is used on DSP request egress + * + * @deprecated In future releases this will be replaced by a config value "edc.iam.token.scope" + */ +@Deprecated(since = "0.1.0") +public class DapsTokenDecorator implements TokenDecorator { + private final String scope; + + public DapsTokenDecorator(String configuredScope) { + this.scope = configuredScope; + } + + @Override + public TokenParameters.Builder decorate(TokenParameters.Builder tokenParametersBuilder) { + return tokenParametersBuilder.scope(scope); + } +} diff --git a/external/oauth2/oauth2-daps/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/external/oauth2/oauth2-daps/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..07e9fb8 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2020, 2021 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: +# Microsoft Corporation - initial API and implementation +# +# + +org.eclipse.edc.iam.oauth2.daps.DapsExtension diff --git a/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsIntegrationTest.java b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsIntegrationTest.java new file mode 100644 index 0000000..ae72ff2 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsIntegrationTest.java @@ -0,0 +1,73 @@ +/* + * 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.iam.oauth2.daps.annotations.DapsTest; +import org.eclipse.edc.junit.extensions.EdcExtension; +import org.eclipse.edc.spi.iam.IdentityService; +import org.eclipse.edc.spi.iam.TokenParameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(EdcExtension.class) +@DapsTest +class DapsIntegrationTest { + + private static final String AUDIENCE_IDS_CONNECTORS_ALL = "idsc:IDS_CONNECTORS_ALL"; + 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_KEY_ALIAS = "1"; + private static final String CLIENT_KEYSTORE_PASSWORD = "1234"; + private static final String DAPS_URL = "http://localhost:4567"; + + private final Map configuration = Map.of( + "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.certificate.alias", CLIENT_KEYSTORE_KEY_ALIAS, + "edc.oauth.private.key.alias", CLIENT_KEYSTORE_KEY_ALIAS + ); + + @Test + void retrieveTokenAndValidate(IdentityService identityService) { + var tokenParameters = TokenParameters.Builder.newInstance() + .scope("idsc:IDS_CONNECTOR_ATTRIBUTES_ALL") + .audience("audience") + .build(); + var tokenResult = identityService.obtainClientCredentials(tokenParameters); + + assertThat(tokenResult.succeeded()).withFailMessage(tokenResult::getFailureDetail).isTrue(); + + var verificationResult = identityService.verifyJwtToken(tokenResult.getContent(), "audience"); + + assertThat(verificationResult.succeeded()).withFailMessage(verificationResult::getFailureDetail).isTrue(); + } + + @BeforeEach + protected void before(EdcExtension extension) { + System.setProperty("edc.vault", "src/test/resources/empty-vault.properties"); + System.setProperty("edc.keystore", "src/test/resources/keystore.p12"); + System.setProperty("edc.keystore.password", CLIENT_KEYSTORE_PASSWORD); + extension.setConfiguration(configuration); + } + +} diff --git a/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecoratorTest.java b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecoratorTest.java new file mode 100644 index 0000000..ae228c0 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsJwtDecoratorTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, 2021 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 + * + */ + +package org.eclipse.edc.iam.oauth2.daps; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DapsJwtDecoratorTest { + + private final DapsJwtDecorator decorator = new DapsJwtDecorator(); + + @Test + void claims() { + var result = decorator.claims(); + + assertThat(result) + .hasFieldOrPropertyWithValue("@context", "https://w3id.org/idsa/contexts/context.jsonld") + .hasFieldOrPropertyWithValue("@type", "ids:DatRequestToken"); + } + + @Test + void headers() { + var result = decorator.headers(); + + assertThat(result).isNotNull().isEmpty(); + } +} \ No newline at end of file diff --git a/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecoratorTest.java b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecoratorTest.java new file mode 100644 index 0000000..89cabb5 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/DapsTokenDecoratorTest.java @@ -0,0 +1,38 @@ +/* + * 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.iam.oauth2.daps; + +import org.eclipse.edc.spi.iam.TokenParameters; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DapsTokenDecoratorTest { + + private DapsTokenDecorator decorator; + + @Test + void decorate() { + decorator = new DapsTokenDecorator("test-scope"); + var bldr = TokenParameters.Builder.newInstance() + .audience("test-audience"); + + var result = decorator.decorate(bldr).build(); + + assertThat(result.getAudience()).isEqualTo("test-audience"); + assertThat(result.getScope()).isEqualTo("test-scope"); + } + +} \ No newline at end of file diff --git a/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/annotations/DapsTest.java b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/annotations/DapsTest.java new file mode 100644 index 0000000..e6148f6 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/java/org/eclipse/edc/iam/oauth2/daps/annotations/DapsTest.java @@ -0,0 +1,33 @@ +/* + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.edc.iam.oauth2.daps.annotations; + +import org.eclipse.edc.junit.annotations.IntegrationTest; +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Composite annotation for Daps integration testing. It applies specific Junit Tag. + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@IntegrationTest +@Tag("DapsIntegrationTest") +public @interface DapsTest { +} diff --git a/external/oauth2/oauth2-daps/src/test/resources/config/clients.yml b/external/oauth2/oauth2-daps/src/test/resources/config/clients.yml new file mode 100644 index 0000000..ea39874 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/resources/config/clients.yml @@ -0,0 +1,11 @@ +--- +- client_id: 68:99:2E:D4:13:2D:FD:3A:66:6B:85:DE:FB:98:2E:2D:FD:E7:83:D7 + name: test client + redirect_uri: + allowed_scopes: + - omejdn:write + - openid + - idsc:IDS_CONNECTOR_ATTRIBUTES_ALL + attributes: + - key: omejdn + - value: admin diff --git a/external/oauth2/oauth2-daps/src/test/resources/config/omejdn.yml b/external/oauth2/oauth2-daps/src/test/resources/config/omejdn.yml new file mode 100644 index 0000000..14d91db --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/resources/config/omejdn.yml @@ -0,0 +1,18 @@ +--- +host: http://localhost:4567 +path_prefix: "/opt" +bind_to: 0.0.0.0 +allow_origin: "*" +app_env: debug +accept_audience: idsc:IDS_CONNECTORS_ALL +token: + expiration: 3600 + signing_key: keys/signing_key.pem + algorithm: RS256 + audience: idsc:IDS_CONNECTORS_ALL + issuer: http://localhost:4567 +id_token: + expiration: 3600 + signing_key: keys/signing_key.pem + algorithm: RS256 + issuer: http://localhost:4567 diff --git a/external/oauth2/oauth2-daps/src/test/resources/config/scope_mapping.yml b/external/oauth2/oauth2-daps/src/test/resources/config/scope_mapping.yml new file mode 100644 index 0000000..ea8209b --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/resources/config/scope_mapping.yml @@ -0,0 +1,3 @@ +--- +idsc:IDS_CONNECTOR_ATTRIBUTES_ALL: + - omejdn \ No newline at end of file diff --git a/external/oauth2/oauth2-daps/src/test/resources/empty-vault.properties b/external/oauth2/oauth2-daps/src/test/resources/empty-vault.properties new file mode 100644 index 0000000..e69de29 diff --git a/external/oauth2/oauth2-daps/src/test/resources/keys/Njg6OTk6MkU6RDQ6MTM6MkQ6RkQ6M0E6NjY6NkI6ODU6REU6RkI6OTg6MkU6MkQ6RkQ6RTc6ODM6RDc=.cert b/external/oauth2/oauth2-daps/src/test/resources/keys/Njg6OTk6MkU6RDQ6MTM6MkQ6RkQ6M0E6NjY6NkI6ODU6REU6RkI6OTg6MkU6MkQ6RkQ6RTc6ODM6RDc=.cert new file mode 100644 index 0000000..4b9fc86 --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/resources/keys/Njg6OTk6MkU6RDQ6MTM6MkQ6RkQ6M0E6NjY6NkI6ODU6REU6RkI6OTg6MkU6MkQ6RkQ6RTc6ODM6RDc=.cert @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUKLpU2zUcd6PlJQ90Jt2WbdG/kxgwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTExMjIwODA1NTJaFw0zMTEx +MjAwODA1NTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDq4RvLUrLw6tfY5/8Wz6QmG93Gyagbwvr4Y5nXXfMf +lKwAyV6FbxIXp2KnguuXq4wEAss0D3CHbRoSG6Wwur46gigDSaFZIqKKog1dXXaa +2GopTyptAUTedLPD2k5ZeyrU6kRqdOkOj0N1IqrqqrBSSs57zIFz7U86TUEx13+x +FrzpfkiToSPACpvHX4TSs+6bLOnImfqlGghh4lmq22RgRoUFqGa0IrLY1tARsr7g +lcxKWt1VdnudXGA32HL3QIAfTvhitbw4R3068s+wswCpkW98MjHogSR+6x3YvQI/ +gkiyZn5/5jWxrlbOwaFMB5xNkuSd5UnE4PAiaDVUNNprAgMBAAGjUzBRMB0GA1Ud +DgQWBBRomS7UEy39OmZrhd77mC4t/eeD1zAfBgNVHSMEGDAWgBRomS7UEy39OmZr +hd77mC4t/eeD1zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj +KgRXg+6rJih1ENc7y2xKKoGS8GvLCAKp9KmRer3nycaqR/2YmOqAatyFKscW4FCh +s/WIel/L+8SEJSsaAXnfcjk5R18qbxFGY74z25Pbdxskq0WEWrxCVDL4jrPOSFIw +9rY4Ym6rtaPUrelcXpvNuaCSDDVlZt9R7BGncwU0sZCIHtxKnMQklnNhQ2ppRq/e +loVCvuYHS6aTG+QSj5Fejqmazgagf94yRdhQuO0HSCjU/PFyUmthCUGVGGGjcjfT +QookwrHG0TIlXkCCgVcQF+7W6g8MnxJD7JxFDM0LfmKjzx1AstY/Hv6W0JHBaSUm +96sClTHVKuOjc8ox80Oo +-----END CERTIFICATE----- diff --git a/external/oauth2/oauth2-daps/src/test/resources/keys/signing_key.pem b/external/oauth2/oauth2-daps/src/test/resources/keys/signing_key.pem new file mode 100644 index 0000000..b3235be --- /dev/null +++ b/external/oauth2/oauth2-daps/src/test/resources/keys/signing_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCS/2Dh4nzop3NC +J1wQpyHrExTHoWJYEmy6nBMsDQ7f2umSICf/KBIqWcsKycwxQPRS66hQXi+aMD1n +fnEFLanFZXlD8UFEXrn5pQwkCMX4hGL8iHpyDK2K/WRWzI8y46mnJ72zDC/eO/+3 +0Ah5O72hccHIAG9E70nekMClNTo29vbJLBNaRpCD2NyYBzIbRdnLpDS91OUzcxE+ +70lwmCIE4ADmIB3DVxOnFXP3bJeCc1KmAL1S4ubi5SaA7GkS6aqHC+Pa/NsCGJTW +foRj2Wc8w3X2Y+MjOeGSAQ8ErwTdEOWens+FtScGUphiFUUhS07ghcfTs/Lz6kVN +Oz1QNWepAgMBAAECggEAVY261l0ac9IZm/QKekq7y/RkELgV48p9a7Kw2d+Tu6fO +b1S10qSAxhNSwCmo5TW3vZcYdAYNUIEpC9Ykc24bNB8WD/wXD3LObMSpp0NP7Y8n +iXDpSv2j95P41SfjZCvqrrXLi6zZI0/qShITDHQ/rvnlXcEbAZT/ekDnToAHLLt4 +IaPiu89JeXOeeaBovOTK54TzKbqPVCTNtks6tHu8wLYcfe/OxVV7q1aJStFNOnbL +V0F1WQlVPRIhHQ8G0iGmwPTNoi80AO9KzSiVxBlWGW3CXZhB89246+Fs2gbJ/70N +lBM9xj2ApuiULFLeh6Ew9wmJetB9B+Ailyi6PADfcQKBgQDCT8F78Tu++IRiyyls +fd3cdQ7Fd3/fjVoYrR79alvarU95XkJjLtyoRWRLJgP0cowwUz2J//HPbPbQeAWR +LlUVpq1DLNlA5H2ES6J41jTcxBoE+7CQ8UM5eIcm3DdWk6RfbXDPWOfwuvLu7xW9 +knPHIYn7rNlz9LKAxfhOS2y07QKBgQDBqkiHE4zJVnZSovJOk38QvUPnirK8TMWs +qTDE8TSBTN+5idZa0XHNQEs2g5PD6rosql7Dpz+FeWlumRDOEqHTA9K9p3t2TTxh +HKqIxgGFma1XlkVVq8LFUw10xrPGszRyXzc1wc90WNQwGrojsWUglmG/68Bh4TBo +njFcLzbCLQKBgFCIca6GyrZZlbTEcwSuHfey5E5fOrZShVbY2ZE6NZuqXNf2gxlM +YNO0/t5OgTEdEJEuzsCVPYk0pg68z8HeLBFvJTxEKD7G9GaSWmIulXYyKH6MOh+4 +fp4hIBKxDpZpVqTeXPTy6h5RvUHeAWqyeh27/s46U13Fuv24DzOT+xf5AoGBALbf +kh8jAdV5NL/xqHc0Zk8rOXziBscyg5L4LNo7nkXejoBIPUaC8kBLzvoKIzVkaCsX +Meb0/lGOhVVvamP9ShvVR2HZTgc3BaX6CLqgpv0+UWYcuxob2A62z0UPAOHHhOXf +LWYwvjHyU2OdSVm9AG5WMrWk64RBvZF8l8Whu8Z9AoGAPbzzGiyUkcVXus6syQV1 +yVSIJdpEQ9hysW6crBoMowe5Jg4XvqBSR3jCOaK565KtJPjHZitOwgt8y+cnaKv1 +IVa9osoClWQ6jMvT5PqGVIDcIvT/9ZsC5UbLLTWPpzxin4FmzGPp8rIbR+Uj7jh8 +OCJAmcrl1M+5fA35/Tihpjw= +-----END PRIVATE KEY----- diff --git a/external/oauth2/oauth2-daps/src/test/resources/keystore.p12 b/external/oauth2/oauth2-daps/src/test/resources/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..9c6de06e27a1ac0c4faeb8c19c88a382102d3080 GIT binary patch literal 2477 zcmY+^XEYm(8VB%%$Uy8-wW>Ca#NMlRjYjRgVsE82i`cb-qKed3DnZeR8hu+z&9>gx ze9amyO3m86?K$_}_udcBdCvL&&w0K*KNJ>LM**ZnVPSz_m{_uY@((x=0xZMAoIzNa z{jb;>g$2?7Q-RB{AOH$Wc}@ZRy$=6rfG937^}jFB0>LOc5HyDFWzTfV7Ci+86cC4{ z9H^`#xD$`-4hkNHFpxU%6}Jl(Wbc>uR+y4^hcZjE3$9o`77^cP9%q|^-?t|#_KP`k zVG8-IwxG3&+zjS*1qWEW10}62ttu6BqGk%qFj+ov zznwdVmv$<6kUzPrR-gLB7_hE8mUh0CY!tGx8N=z)n4**}Ih(*Lo3Yib`++n(#=g;$ zc06XT*9nS`y*nkdw>8>Oi%ckZ(Y!?uf15jtsgd3DhNp+S8X};}dY(K{`1>Tl1uE>B zmtg4gQ$qQ8k9eYP@$67|Fn53GR3x;oD*Fr1KwsSTz=I;ANNG-_xS^s}>G%jYop7Ia zX*B6XYtHl`6Q^I@9@7O%(`Y^qxEsb8lIAldyvDimkgzA4yhISPA%wcMl74ohI1F~k zZg~-l8Xj=r?*-R5|5%OB(QVfo5i#Zx3!jfj|G8G4JmEjG=DkLB%@vojDafxAR_d}* zX4TzDk;Uy7c6CBqFfBWxR%TfiY(WY4ZfKaz^*kxmqt2i*m9Q-?D3)mD`U>?=ND=6V@xI!(jGVz;yIg+Vl6!o(`*P+#?vc$tYh@}6SpHY zKL>5gD;14HMP?@3hA3)!waqI=)H!Wl7vi$!VFZxA(}GvEDv6*9Bj- zghk5t)M_Elim~QU?`X3DI3L8SE4j#1n2_=8Dx2GCg9OQ1nR|eZ7&J^D9 zcr!dAqm5ymeo&Z54&Ulfy|NP4Gpdld76H&U6DmTFD<<=W*Ec6*Kp0)%Yb`yRv`L>> zl6%96OCBQMurtP%JkP~&a(mr7Y%jq`4b$U?aakn2Tm z)gUt#ilTBsZ>|fOZ3W6Z8;h0bpksYY$G7Y#ES2W}JboFLN(zLflK2&i{Td-K&3_o6 z22%X;V*3{%8~>*RFjMFZlWHol7atpf-P8fdu0_cwg)9gp7ueVmaVR9Rf(h)6`2 z*fy1ReppyBJ|Mi>Y{gxi+C}~~?0EQ2R>{5}DroO)hns$4W+#hAG$5uX4BuSGTLcoo zW;XA2j{+z|EjQDZQ)OqEb<-GBLcBn5fzTNyu_H$04y+K+I(!*v*V;OJ6-oP1nqyCE zJnhGIC)cou&4NNA5Sa`hPrX4BuXfHXDiPBs0kMUsm=IEX2#e+&&2P2gp^n>?WW zHAmWTZHVBN;jPoow+ZQ3Jf6<`Q5}Did^cKKL?~1k-r+~H*K{j-m8qQrDE>;Y| zVnKMiFS_pyEhVJ1=?-(7D5*rY+WB4YpSV|>uXNQ$LsLWWWL6$6~Mi=Mzkcp5vJm8ltzxPO8}XECn$NZ%l{c|h3rfK+2di>kKw z0PM(m{)b52Tj8`Ho&JW=p<7=qF2Mm9W~oYF)76st<|TEDx)&uznnN2ZGe60wJ8#w? zyJOt_7U4A=v%ZiLZuIE&2<1MXPU30Z6oesvmzyG}?ZiuI-wRy_aY`ai-c&Jx?d~jR ze{|_2aK#rNVp9ETqU!Db4oZ=>C0Z$-=f8aYwYQ_F|9O*+ylvzX`S2(#$7k zJv($R$QeC-v!G-$$KHMCn@bzJl8ZhP?^N-P3x{5i&rXi6G(>Eix;{k **_NOTE:_** Unless you are sure of what you are doing, you do not want to implement your own Oauth2 Client in most cases. +> This BOM should thus be considered as the standard and recommended approach of embedding the Oauth2 Identity Service into your runtime. \ No newline at end of file diff --git a/external/oauth2/oauth2-service/build.gradle.kts b/external/oauth2/oauth2-service/build.gradle.kts new file mode 100644 index 0000000..94c5625 --- /dev/null +++ b/external/oauth2/oauth2-service/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 Amadeus + * + * 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 + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(project(":external:oauth2:oauth2-client")) + implementation(project(":external:oauth2:oauth2-core")) +} + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65e1fd1..f6e155c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ restAssured = "5.3.1" rsApi = "3.1.0" shadow = "7.1.2" postgres = "42.6.0" +nimbus = "9.31" retrofit = "2.11.0" mockito = "5.12.0" @@ -41,6 +42,7 @@ edc-data-plane-util = { module = "org.eclipse.edc:data-plane-util", version.ref edc-dsp = { module = "org.eclipse.edc:dsp", version.ref = "edc" } edc-http = { module = "org.eclipse.edc:http", version.ref = "edc" } edc-iam-mock = { module = "org.eclipse.edc:iam-mock", version.ref = "edc" } +edc-iam-oauth2 = { module = "org.eclipse.edc:oauth2-service", version.ref = "edc" } edc-jersey-micrometer = { module = "org.eclipse.edc:jersey-micrometer", version.ref = "edc" } edc-jetty-micrometer = { module = "org.eclipse.edc:jetty-micrometer", version.ref = "edc" } edc-control-plane-store-sql-asset = { module = "org.eclipse.edc:asset-index-sql", version.ref = "edc" } @@ -52,7 +54,10 @@ edc-control-plane-store-sql-transferProcess = { module = "org.eclipse.edc:transf edc-transaction-local = { module = "org.eclipse.edc:transaction-local", version.ref = "edc" } edc-sql-pool = { module = "org.eclipse.edc:sql-pool-apache-commons", version.ref = "edc" } edc-spi-fcc = { module = "org.eclipse.edc:federated-catalog-spi", version.ref = "edc" } +edc-spi-oauth2 = { module = "org.eclipse.edc:oauth2-spi", version.ref = "edc" } +edc-spi-http = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" } +edc-jwt-core = { module = "org.eclipse.edc:jwt-core", version.ref = "edc" } edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } edc-management-api = { module = "org.eclipse.edc:management-api", version.ref = "edc" } edc-micrometer-core = { module = "org.eclipse.edc:micrometer-core", version.ref = "edc" } @@ -77,8 +82,9 @@ opentelemetry-annotations = { module = "io.opentelemetry:opentelemetry-extension opentelemetry = "io.opentelemetry.javaagent:opentelemetry-javaagent:2.0.0" opentelemetry-exporter-jaeger = { module = "io.opentelemetry:opentelemetry-exporter-jaeger", version = "1.34.1" } restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } +nimbus-jwt = { module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } [plugins] -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } \ No newline at end of file +shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } diff --git a/launchers/azure/build.gradle.kts b/launchers/azure/build.gradle.kts index 8b2ae26..120cdf2 100644 --- a/launchers/azure/build.gradle.kts +++ b/launchers/azure/build.gradle.kts @@ -57,6 +57,9 @@ dependencies { implementation(libs.edc.data.plane.core) implementation(libs.edc.data.plane.http) + implementation(libs.opentelemetry.exporter.jaeger) + implementation(libs.edc.api.observability) + implementation(libs.edc.monitor.jdk.logger) } diff --git a/launchers/edc-tu-berlin/build.gradle.kts b/launchers/edc-tu-berlin/build.gradle.kts index ba1defd..3dd8540 100644 --- a/launchers/edc-tu-berlin/build.gradle.kts +++ b/launchers/edc-tu-berlin/build.gradle.kts @@ -25,6 +25,8 @@ val edcVersion: String by project dependencies { + + implementation(project(":extensions:blockchain:catalog-listener")) implementation(project(":extensions:blockchain:logger")) implementation(project(":extensions:blockchain:blockchain-catalog-api")) @@ -36,7 +38,6 @@ dependencies { implementation(libs.edc.dsp) implementation(libs.edc.management.api) implementation(libs.edc.data.plane.selector.core) - implementation(libs.edc.iam.mock) implementation(libs.edc.transaction.local) @@ -45,7 +46,6 @@ dependencies { implementation(libs.edc.control.plane.core) implementation(libs.edc.dsp) implementation(libs.edc.vault.filesystem) - implementation(libs.edc.iam.mock) implementation(libs.edc.management.api) implementation(libs.edc.transfer.data.plane) @@ -68,6 +68,14 @@ dependencies { implementation(libs.opentelemetry.exporter.jaeger) implementation(libs.edc.api.observability) //runtimeOnly(libs.edc.monitor.jdk.logger) + + implementation(project(":extensions:helper")) + // enable / disable oauth2 - currently not working as expected + //implementation(project(":external:oauth2:oauth2-service")) + //implementation(libs.edc.iam.oauth2) + implementation(libs.edc.iam.mock) + + } application { diff --git a/launchers/edc-tu-berlin/config.properties b/launchers/edc-tu-berlin/config.properties index 2aba0df..2f4406f 100644 --- a/launchers/edc-tu-berlin/config.properties +++ b/launchers/edc-tu-berlin/config.properties @@ -30,6 +30,7 @@ edc.dataplane.token.validation.endpoint=http://localhost:8183/control/token web.http.public.port=8185 web.http.public.path=/public + edc.receiver.http.endpoint=http://localhost:4000/receiver/urn:connector:provider/callback edc.keystore=certs/cert.pfx @@ -49,3 +50,9 @@ edc.datasource.asset.url=jdbc:postgresql://localhost:5433/connector?user=connect edc.datasource.policy.url=jdbc:postgresql://localhost:5433/connector?user=connector&password=password +edc.oauth.token.url=https://ssi-to-oidc-bridge-hydra.gxfs.gx4fm.org/oauth2/token +edc.oauth.certificate.alias=1 +edc.oauth.private.key.alias=1 +edc.oauth.client.id=1bb53adb-61d0-441e-91d5-7aa62531593f +edc.oauth.provider.jwks.url=https://ssi-to-oidc-bridge-hydra.gxfs.gx4fm.org/.well-known/jwks.json + diff --git a/launchers/edc-tu-berlin/config2.properties b/launchers/edc-tu-berlin/config2.properties index ff235ae..b2e3f5b 100644 --- a/launchers/edc-tu-berlin/config2.properties +++ b/launchers/edc-tu-berlin/config2.properties @@ -30,4 +30,23 @@ web.http.public.port=9295 web.http.public.path=/public edc.dataplane.token.validation.endpoint=http://localhost:9193/control/token -edc.receiver.http.endpoint=http://localhost:4000/receiver/urn:connector:provider/callback \ No newline at end of file +edc.receiver.http.endpoint=http://localhost:4000/receiver/urn:connector:provider/callback + + +edc.datasource.asset.name=default +edc.datasource.policy.name=default +edc.datasource.contractdefinition.name=default +edc.datasource.contractnegotiation.name=default +edc.datasource.transferprocess.name=default +edc.datasource.default.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password +edc.datasource.contractdefinition.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password +edc.datasource.contractnegotiation.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password +edc.datasource.transferprocess.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password +edc.datasource.asset.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password +edc.datasource.policy.url=jdbc:postgresql://localhost:5434/connector?user=connector&password=password + +edc.oauth.token.url=https://ssi-to-oidc-bridge-hydra.gxfs.gx4fm.org/oauth2/token +edc.oauth.certificate.alias=1 +edc.oauth.private.key.alias=1 +edc.oauth.client.id=ffa15c1e-9752-4251-bade-40637e854779 +edc.oauth.provider.jwks.url=https://ssi-to-oidc-bridge-hydra.gxfs.gx4fm.org/.well-known/jwks.json diff --git a/settings.gradle.kts b/settings.gradle.kts index cff6438..f65472a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,8 +39,12 @@ include("launchers:azure") include("extensions:blockchain:blockchain-catalog-api") include("extensions:blockchain:catalog-listener") include("extensions:blockchain:logger") +include("extensions:helper") include("extensions:transfer:http-push:provider-push-http-backend-service") include("extensions:claim-compliance-provider-integration") +include("external:oauth2:oauth2-service") +include("external:oauth2:oauth2-core") +include("external:oauth2:oauth2-client")