diff --git a/AUTHORS.md b/AUTHORS.md index c5b8ce9a..9627fda3 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -16,5 +16,6 @@ The following people have contributed to this repository: * Aggarwal Sahil, Robert Bosch GmbH, sahil.aggarwal@de.bosch.com * Tunahan Cicek, Robert Bosch GmbH, tunahan.cicek@de.bosch.com * Istvan Zoltan Nagy, Robert Bosch GmbH, niy1fe@bosch.com +* Anton Peissinger, Draexlmaier Group, anton.peissinger@draexlmaier.com Please add yourself to this list, if you contribute to the content. diff --git a/CHANGELOG.md b/CHANGELOG.md index 519d3702..070b9c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.5.0-RC1 ### Added - +- AWS Cognito support for authentication ## fixed - Bugfix access rule management for submodelfilter (visibleSemanticIds): Remove condition on `semanticId.keys.type = submodel` because the `semanticId.keys.type` should be `GlobalReference`. This check is not needed. - Implemented mandatory changes in licensing and legal documentation diff --git a/INSTALL.md b/INSTALL.md index 26c2bb5a..d6b5b394 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -48,6 +48,8 @@ The Helm Chart can be configured using the following parameters (incomplete list | `registry.ingress.className` | The `Ingress` class name | `nginx` | | `registry.ingress.annotations` | Annotations to further configure the `Ingress` resource, e.g. for using with `cert-manager`. | | | `registry.tenantId` | TenantId which is the owner of the DTR. | | +| `registry.identityProvider` | Identity provider for the DTR. Possible values are `keycloak` or `cognito`. | | +| `registry.idpInternalClientId` | The client id for the app client in Cognito that as full access to the DTR. | | | `registry.externalSubjectIdWildcardPrefix` | WildcardPrefix to make a specificAssetId visible for everyone. | `PUBLIC_READABLE` | | `registry.externalSubjectIdWildcardAllowedTypes` | List of allowed types that can be made visible to everyone. | `manufacturerPartId,assetLifecyclePhase` | diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java b/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java index 4218ef66..04c9ed34 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/RegistryProperties.java @@ -22,12 +22,13 @@ import java.util.List; -import lombok.Data; +import org.eclipse.tractusx.semantics.registry.security.OAuthSecurityConfig; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import lombok.Data; @Data @@ -64,13 +65,25 @@ public class RegistryProperties { @Data @NotNull public static class Idm { - /** + /** + * The identityProvider that should be used. Allowed are keycloak or cognito + */ + @NotEmpty(message = "identityProvider must not be empty. Allowed is keycloak or cognito") + private String identityProvider = OAuthSecurityConfig.IdentityProvider.KEYCLOAK.name().toLowerCase(); + + /** * The public client id used for the redirect urls. */ @NotEmpty(message = "public client id must not be empty") private String publicClientId; - /** + /** + * The internal client id used for writing operations to registry. Used if + * identity provider is cognito + */ + private String internalClientId; + + /** * The owning tenant id to which this AAS Registry belongs to. */ @NotEmpty(message = "owningTenantId must not be empty") diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java index e5036eb7..d071fc83 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/AuthorizationEvaluator.java @@ -1,6 +1,6 @@ /******************************************************************************* - * Copyright (c) 2021 Robert Bosch Manufacturing Solutions GmbH and others - * Copyright (c) 2021 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Draexlmaier Group * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -22,15 +22,6 @@ import static org.eclipse.tractusx.semantics.registry.security.AuthorizationEvaluator.Roles.*; -import java.util.Collection; -import java.util.Map; - -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; - -import lombok.extern.slf4j.Slf4j; - /** * This class contains methods validating JWT tokens for correctness and ensuring that the JWT token contains a desired role. * The methods are meant to be used in Spring Security expressions for RBAC on API operations. @@ -48,12 +39,11 @@ * the token will be considered invalid. Invalid tokens result in 403. * */ -@Slf4j -public class AuthorizationEvaluator { +public abstract class AuthorizationEvaluator { private final String clientId; - public AuthorizationEvaluator( String clientId ) { + protected AuthorizationEvaluator( String clientId ) { this.clientId = clientId; } @@ -85,34 +75,16 @@ public boolean hasRoleWriteAccessRules() { return containsRole( ROLE_WRITE_ACCESS_RULES ); } - private boolean containsRole( String role ) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if ( !(authentication instanceof JwtAuthenticationToken) ) { - return false; - } - - JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) (authentication); - Map claims = jwtAuthenticationToken.getToken().getClaims(); - - Object resourceAccess = claims.get( "resource_access" ); - if ( !(resourceAccess instanceof Map) ) { - return false; - } + protected abstract boolean containsRole( String role ) ; - Object resource = ((Map) resourceAccess).get( clientId ); - if ( !(resource instanceof Map) ) { - return false; - } - - Object roles = ((Map) resource).get( "roles" ); - if ( !(roles instanceof Collection) ) { - return false; - } - - Collection rolesList = (Collection) roles; - return rolesList.contains( role ); + /** + * get the client id + * @return the client id + */ + protected String getClientId() { + return clientId; } - + /** * Represents the roles defined for the registry. */ diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/CognitoAuthorizationEvaluator.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/CognitoAuthorizationEvaluator.java new file mode 100755 index 00000000..b186bc3c --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/CognitoAuthorizationEvaluator.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Draexlmaier Group + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.registry.security; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.tractusx.semantics.RegistryProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Conito Authenticator We have two client_ids This is necessary because we have + * two clients, one for reading data and one for full access + */ +public final class CognitoAuthorizationEvaluator extends AuthorizationEvaluator { + private static final Logger log = LoggerFactory.getLogger(CognitoAuthorizationEvaluator.class); + + private final String internalClientId; + + public CognitoAuthorizationEvaluator(final RegistryProperties.Idm idm) { + super(idm.getPublicClientId()); + this.internalClientId = idm.getInternalClientId(); + } + + @Override + protected boolean containsRole(final String role) { + CognitoAuthorizationEvaluator.log.debug("Checking if token contains role {}", role); + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!(authentication instanceof JwtAuthenticationToken)) { + return false; + } + + final JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) (authentication); + final Map claims = jwtAuthenticationToken.getToken().getClaims(); + + final Object claimClientId = claims.get("client_id"); + + if (StringUtils.equals(this.internalClientId, (String) claimClientId) || StringUtils.equals(this.getClientId(), (String) claimClientId)) { + final Object scope = claims.get("scope"); + if (scope instanceof final String scopeString) { + final String[] split = StringUtils.split(scopeString, ' '); + for (final String tokenRole : split) { + if (StringUtils.contains(tokenRole, role)) { + CognitoAuthorizationEvaluator.log.debug("Role {} found in token", role); + return true; + } + } + } + } + CognitoAuthorizationEvaluator.log.debug("Role {} NOT found in token", role); + return false; + } +} diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/KeycloakAuthorizationEvaluator.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/KeycloakAuthorizationEvaluator.java new file mode 100755 index 00000000..a8b9d349 --- /dev/null +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/KeycloakAuthorizationEvaluator.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Draexlmaier Group + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.registry.security; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +/** + * Keycloak Authorization Evaluator + */ +public final class KeycloakAuthorizationEvaluator extends AuthorizationEvaluator { + + /** + * Constructor + * @param clientId clientId + */ + public KeycloakAuthorizationEvaluator(String clientId) { + super(clientId); + } + + protected boolean containsRole( String role ) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if ( !(authentication instanceof JwtAuthenticationToken) ) { + return false; + } + + JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) (authentication); + Map claims = jwtAuthenticationToken.getToken().getClaims(); + + Object resourceAccess = claims.get( "resource_access" ); + if ( !(resourceAccess instanceof Map) ) { + return false; + } + + Object resource = ((Map) resourceAccess).get( this.getClientId() ); + if ( !(resource instanceof Map) ) { + return false; + } + + Object roles = ((Map) resource).get( "roles" ); + if ( !(roles instanceof Collection) ) { + return false; + } + + Collection rolesList = (Collection) roles; + return rolesList.contains( role ); + } +} diff --git a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java index 9e5c1f4c..9f18c92d 100644 --- a/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java +++ b/backend/src/main/java/org/eclipse/tractusx/semantics/registry/security/OAuthSecurityConfig.java @@ -91,6 +91,45 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception { @Bean public AuthorizationEvaluator authorizationEvaluator(RegistryProperties registryProperties){ - return new AuthorizationEvaluator(registryProperties.getIdm().getPublicClientId()); + final IdentityProvider provider = IdentityProvider.getIdentityProvider(registryProperties.getIdm().getIdentityProvider()); + return provider.createAuthorizationEvaluator(registryProperties.getIdm()); } + + /** + * enum containing the possible identity providers for the DTR + */ + public enum IdentityProvider { + + KEYCLOAK { + @Override + public AuthorizationEvaluator createAuthorizationEvaluator(RegistryProperties.Idm idmProperties) { + return new KeycloakAuthorizationEvaluator(idmProperties.getPublicClientId()); + } + }, + + COGNITO { + @Override + public AuthorizationEvaluator createAuthorizationEvaluator(RegistryProperties.Idm idmProperties) { + return new CognitoAuthorizationEvaluator(idmProperties); + } + }; + + public abstract AuthorizationEvaluator createAuthorizationEvaluator(RegistryProperties.Idm idmProperties); + + /** + * tries to find the Identity Provider enum element based on a string, ignores + * case!. + * + * @param key the key to search + * @return the key as enum + */ + + public static IdentityProvider getIdentityProvider(final String identityProviderName) { + try { + return IdentityProvider.valueOf(identityProviderName.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Unknown identityProvider: " + identityProviderName, e); + } + } + } } diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/CognitoApiSecurityTest.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/CognitoApiSecurityTest.java new file mode 100755 index 00000000..0d515bc8 --- /dev/null +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/CognitoApiSecurityTest.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Draexlmaier Group + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.semantics.registry; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +@ActiveProfiles(profiles = { "cognito-test" }) +public class CognitoApiSecurityTest extends AbstractAssetAdministrationShellApi { + @Test + public void testWithInvalidAuthenticationTokenConfigurationExpectUnauthorized() throws Exception { + mvc.perform( + MockMvcRequestBuilders + .get( SINGLE_SHELL_BASE_PATH, UUID.randomUUID() ) + .accept( MediaType.APPLICATION_JSON ) + .with( jwtTokenFactory.withoutResourceAccess() ) + ) + .andDo( MockMvcResultHandlers.print() ) + .andExpect( status().isForbidden() ); + + mvc.perform( + MockMvcRequestBuilders + .get( SINGLE_SHELL_BASE_PATH, UUID.randomUUID() ) + .accept( MediaType.APPLICATION_JSON ) + .with( jwtTokenFactory.withoutRoles() ) + ) + .andDo( MockMvcResultHandlers.print() ) + .andExpect( status().isForbidden() ); + } + + @Test + public void testWithAuthenticationTokenConfigurationExpectAuthorized() throws Exception { + // test is only if Cognito auth is working. Shell descriptor does not exist so we expect a 404 not found. + mvc.perform( + MockMvcRequestBuilders + .get( SINGLE_SHELL_BASE_PATH, UUID.randomUUID() ) + .accept( MediaType.APPLICATION_JSON ) + .with( jwtTokenFactory.allRoles() ) + ) + .andDo( MockMvcResultHandlers.print() ) + .andExpect( status().isNotFound() ); + } +} diff --git a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/JwtTokenFactory.java b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/JwtTokenFactory.java index 80fd3c82..2ae25da4 100644 --- a/backend/src/test/java/org/eclipse/tractusx/semantics/registry/JwtTokenFactory.java +++ b/backend/src/test/java/org/eclipse/tractusx/semantics/registry/JwtTokenFactory.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.eclipse.tractusx.semantics.registry.security.AuthorizationEvaluator; import org.springframework.security.core.GrantedAuthority; @@ -158,10 +159,13 @@ public RequestPostProcessor withoutRoles() { } private RequestPostProcessor authenticationWithRoles( String tenantId, List roles ) { + final String scopes = String.join(" ", roles.stream().map(p -> "dtwinreg/" + p).collect(Collectors.toList())); + Jwt jwt = Jwt.withTokenValue( "token" ) .header( "alg", "none" ) .claim( "sub", "user" ) .claim( "resource_access", Map.of( publicClientId, Map.of( "roles", toJsonArray( roles ) ) ) ) + .claim("scope", scopes) .build(); Collection authorities = Collections.emptyList(); return authentication( new JwtAuthenticationToken( jwt, authorities ) ); diff --git a/backend/src/test/resources/application-cognito-test.yml b/backend/src/test/resources/application-cognito-test.yml new file mode 100755 index 00000000..2e68ebb9 --- /dev/null +++ b/backend/src/test/resources/application-cognito-test.yml @@ -0,0 +1,52 @@ +################################################################################ +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# Copyright (c) 2024 Draexlmaier Group +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# 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. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# SPDX-License-Identifier: Apache-2.0 +################################################################################ + +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: + + datasource: + driverClassName: org.h2.Driver + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;CASE_INSENSITIVE_IDENTIFIERS=TRUE + + # Properties for running tests against a postgres instance + # You can start a postgres instance using the docker-compose file located in the backend/postgres/ directory + #datasource: + #url: jdbc:postgresql://localhost:5432/postgres + #driverClassName: org.postgresql.Driver + #username: postgres + #password: example + +registry: + external-subject-id-wildcard-prefix: "PUBLIC_READABLE" + external-subject-id-wildcard-allowed-types: manufacturerPartId,assetLifecyclePhase + idm: + identity-provider: cognito + owning-tenant-id: TENANT_ONE + +logging: + level: + org: + springframework: + jdbc: + core: TRACE \ No newline at end of file diff --git a/charts/registry/Chart.yaml b/charts/registry/Chart.yaml index b6af1d0f..ac4f8f3a 100644 --- a/charts/registry/Chart.yaml +++ b/charts/registry/Chart.yaml @@ -26,7 +26,7 @@ sources: - https://github.com/eclipse-tractusx/sldt-digital-twin-registry type: application -version: 0.4.11 +version: 0.4.12 appVersion: 0.4.3 dependencies: @@ -37,4 +37,4 @@ dependencies: - repository: https://charts.bitnami.com/bitnami name: keycloak version: 16.1.7 - condition: enableKeycloak + condition: enableKeycloak \ No newline at end of file diff --git a/charts/registry/README.md b/charts/registry/README.md index 00bfa19c..d1a4dc41 100644 --- a/charts/registry/README.md +++ b/charts/registry/README.md @@ -62,7 +62,9 @@ helm install registry -n semantics charts/registry | registry.dataSource.url | string | `"jdbc:postgresql://database:5432"` | | | registry.dataSource.user | string | `"default-user"` | | | registry.host | string | `"minikube"` | | +| registry.identityProvider | string | `"keycloak"` | | | registry.idpClientId | string | `"default-client"` | | +| registry.idpInternalClientId | string | `"default-client"` | | | registry.idpIssuerUri | string | `""` | | | registry.image.registry | string | `"docker.io"` | | | registry.image.repository | string | `"tractusx/sldt-digital-twin-registry"` | | diff --git a/charts/registry/templates/registry/registry-secret.yaml b/charts/registry/templates/registry/registry-secret.yaml index 05f4fa27..1b7370fe 100644 --- a/charts/registry/templates/registry/registry-secret.yaml +++ b/charts/registry/templates/registry/registry-secret.yaml @@ -37,6 +37,8 @@ data: {{- end }} REGISTRY_IDM_PUBLIC_CLIENT_ID: {{ .Values.registry.idpClientId | b64enc }} REGISTRY_IDM_OWNING_TENANT_ID: {{ .Values.registry.tenantId | b64enc }} + REGISTRY_IDM_INTERNAL_CLIENT_ID: {{ .Values.registry.idpInternalClientId | b64enc }} + REGISTRY_IDM_IDENTITY_PROVIDER: {{ .Values.registry.identityProvider | b64enc }} REGISTRY_EXTERNAL_SUBJECT_ID_WILDCARD_PREFIX: {{ .Values.registry.externalSubjectIdWildcardPrefix | b64enc }} REGISTRY_EXTERNAL_SUBJECT_ID_WILDCARD_ALLOWED_TYPES: {{ .Values.registry.externalSubjectIdWildcardAllowedTypes | b64enc }} REGISTRY_USE_GRANULAR_ACCESS_CONTROL: {{ .Values.registry.useGranularAccessControl | b64enc }} diff --git a/charts/registry/values.yaml b/charts/registry/values.yaml index 61dcfb22..eb1fae2d 100644 --- a/charts/registry/values.yaml +++ b/charts/registry/values.yaml @@ -47,6 +47,10 @@ registry: idpIssuerUri: "" idpClientId: default-client tenantId: default-tenant + # this field is necessary if cognito is used as identity provider instead of keycloak + idpInternalClientId: default-client + # this field configures the identity provider for the digital twin registry (keycloak or cognito) + identityProvider: keycloak externalSubjectIdWildcardPrefix: PUBLIC_READABLE externalSubjectIdWildcardAllowedTypes: manufacturerPartId,digitalTwinType useGranularAccessControl: "false" diff --git a/docs/COGNITO.md b/docs/COGNITO.md new file mode 100755 index 00000000..ce927b1b --- /dev/null +++ b/docs/COGNITO.md @@ -0,0 +1,133 @@ +# AWS Cognito as Identity Provider +DTR can support Keycloak and AWS Cognito as an Identity Provider for all clients that want to access the registry. +Client must fetch a token and send it together with the request. +Cognito behaves somehow different than Keycloak so some code modifications were necessary to use Cognito as an Identity Provider for DTR. + +## Configuring AWS Cognito +The main difference is that Cognito is not able to attach roles to app clients directly if no user is used for authentication. +For authentication only client id and client secret is used. +This makes it necessary to use a different approach to control the access of the different app clients. + +**Resource Servers and Scopes** +First of all a resource server must be configured within Cognito. Otherwise it is not possible to use custom scopes. +After that the following scopes must be created which correspond to the DTR roles: +- view_digital_twin +- add_digital_twin +- delete_digital_twin +- update_digital_twin +- submodel_access_control +- read_access_rules +- write_access_rules + +If newer versions of DTR have new roles also those must be created as described above. + +The final name of the scope is then the name of the resource server combined with the custom scope e.g. +Resource server name is: `dtwinreg` then scope name for view access is: `dtwinreg/view_digital_twin` + +**App Integration App Clients** +At least two app clients should be created: +- one with ony the view scope attached +- one with all scopes attached for full access + +For additional roles additional app clinets can be created. Or roles can be attached to different clients. +It depends on the granularity of your access management. + +The app clients have a client id and a client secret that can be used together with the scopes to fetch a token. +If somebody wants to request a token with the credentials of the "view app client" and with the scope `add_digital_twin` authentication on Cognito will fail, because the client has not attached that scope. +So it can ge guaranteed that the "view app client" has only view access. + +## Configuring DTR + +**Config File** + +The `idm` subelement has a new attribute called `identity-provider`. +Two values are allowed: +- `keycloak`: This switches DTR to the default behaviour that is already known +- `cognito`: This switches DTR to use cognito as identity provider + +Here is a snippet of an example DTR config file: + + registry: + # This wildcard prefix is used to make specificAssetIds public vor everyone. + # The default-value "PUBLIC_READABLE" is used by all catenaX participants. + external-subject-id-wildcard-prefix: PUBLIC_READABLE + external-subject-id-wildcard-allowed-types: manufacturerPartId,assetLifecyclePhase + idm: + identity-provider: cognito + # is our BPN + owning-tenant-id: + # Keycloak + # public-client-id: default-client + # The claim name of the keycloak claims where the name of the client-id is the value + # tenant-id-claim-name: azp + + # Cognito + # if this id comes then only scope dtwinreg/view_digital_twin is allowed + public-client-id: 16php469rdo51k4sr5ltcmrbsf + # if this id comes then every existing scope is allowed + internal-client-id: 602dd312d9bo9819qdcrthtsmk + +If `cognito` is set as an identity provider the following attributes must be set: + +- `public-client-id`: This is the client id of the app client in Cognito that has the `view_digital_twin` scope attached +- `internal-client-id`: This is the client is of the app client in Cognito that has the edit scopes attached + +**DTR Asset in EDC** +To access the DTR a token must be fetched first. For the management API the token must be fetched before the API will be called. +The token must be then included as a Bearer token in the header (same as for Keycloak tokens) + +To fetch a token a token form Cognito there is a corresponding URL: +`https://xxxx.auth.eu-west-1.amazoncognito.com/oauth2/token` +where `xxxx` is the custom domain name of the Cognito in AWS and the location of Cognito here is `eu-west-1` + +The body of the `POST` request must contain the following values with `x-www-form-urlencoded`encoding: + + grant_type: "client_credentials" + scope: "dtwinreg/" + the DTR role (e.g. dtwinreg/view_digital_twin) Also multiple scopes are possible separated by whitespace (e.g. dtwinreg/view_digital_twin dtwinreg/submodel_access_control) + client_id: "the client id of the app client that has scopes above assigned" + client_secret: "the secret of the app client with the client id above" + +With the following example a token will be returned for view only access to DTR: + + grant_type: "client_credentials" + scope: "dtwinreg/view_digital_twin" + client_id: "the client id of the app client that has scopes above assigned" + client_secret: "the secret of the app client with the client id above" + +When EDC will access the DTR this will be done normally via a special asset that contains necessary information also for fetching the token to access the DTR: +This could look: + + { + "@context": { + "@vocab": "https://w3id.org/edc/v0.0.1/ns/", + "cx-common": "https://w3id.org/catenax/ontology/common#", + "cx-taxo": "https://w3id.org/catenax/taxonomy#", + "dct": "https://purl.org/dc/terms/" + }, + "@id": "urn:name:digital-twin-registry", + "properties": { + "dct:type": { + "@id": "cx-taxo:DigitalTwinRegistry" + }, + "cx-common:version": "3.0", + "type": "data.core.digitalTwinRegistry" + }, + "privateProperties": {}, + "dataAddress": { + "@type": "DataAddress", + "type": "HttpData", + "baseUrl": "base url to the DTR", + "proxyPath": "true", + "proxyBody": "true", + "proxyMethod": "true", + "proxyQueryParams": "true", + "oauth2:clientId": "the client id of the app client that has only view scope", + "oauth2:clientSecretKey": "the name of the secret in the vault where the client secret of the client id above is stored", + "oauth2:tokenUrl": "https://xxxx.auth.eu-west-1.amazoncognito.com/oauth2/token", + "oauth2:scope": "dtwinreg/view_digital_twin" + } + } + +The OAuth properies point to the Cognito service including the id, secret and the scope. + + diff --git a/docs/README.md b/docs/README.md index 20c1027d..90b40c98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -487,6 +487,12 @@ Depending on being a Data Provider or a Data Consumer there are different tokens #### Data Consumer 1. needs an EDR token which is provided between the intercommunication between the EDCs. +#### AWS Cognito as Identity Provider +DTR can support Keycloak and AWS Cognito as an Identity Provider for all clients that want to access the registry. +Client must fetch a token and send it together with the request. +Cognito behaves somehow different than Keycloak so some code modifications were necessary to use Cognito as an Identity Provider for DTR. +Detailed information can be found [here](COGNITO.md) + ### Authentication on behalf of a user The AAS Registry can be accessed on behalf of a user. The token has to be obtained via the OpenID Connect flow. The AAS Registry will validate these tokens. *Support contact* tractusx-dev@eclipse.org