diff --git a/connect/camunda7-remote/all/pom.xml b/connect/camunda7-remote/all/pom.xml index 82f2f1b89..66afc3f20 100644 --- a/connect/camunda7-remote/all/pom.xml +++ b/connect/camunda7-remote/all/pom.xml @@ -39,11 +39,6 @@ element-templates-c7 ${project.version} - - io.miragon.miranum.connect - oauth-camunda7-remote - ${project.version} - diff --git a/connect/camunda7-remote/oauth/src/main/java/io/miragon/miranum/connect/Camunda7oAuthAutoConfiguration.java b/connect/camunda7-remote/oauth/src/main/java/io/miragon/miranum/connect/Camunda7oAuthAutoConfiguration.java index 4207e0693..148d5659d 100644 --- a/connect/camunda7-remote/oauth/src/main/java/io/miragon/miranum/connect/Camunda7oAuthAutoConfiguration.java +++ b/connect/camunda7-remote/oauth/src/main/java/io/miragon/miranum/connect/Camunda7oAuthAutoConfiguration.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -19,19 +20,26 @@ import org.springframework.web.client.RestTemplate; import java.io.IOException; +import java.time.Instant; +import java.util.concurrent.locks.ReentrantLock; @Configuration +@Profile("!no-security") public class Camunda7oAuthAutoConfiguration { - @Value("${miranum.security.auth.server.url}") + @Value("${spring.security.oauth2.client.provider.keycloak.token-uri}") private String authServerUrl; - @Value("${miranum.security.client.clientId}") + @Value("${spring.security.oauth2.client.registration.keycloak.client-id}") private String clientId; - @Value("${miranum.security.client.clientSecret}") + @Value("${spring.security.oauth2.client.registration.keycloak.client-secret}") private String clientSecret; + private String cachedToken; + private Instant tokenExpiry; + private final ReentrantLock lock = new ReentrantLock(); + @Bean public ClientRequestInterceptor interceptor() { return context -> context.addHeader("Authorization", this.getAccessToken()); @@ -51,17 +59,31 @@ public Response intercept(final Interceptor.Chain chain) throws IOException { } public String getAccessToken() { + lock.lock(); + try { + if (cachedToken == null || Instant.now().isAfter(tokenExpiry)) { + fetchAndCacheToken(); + } + return "Bearer " + cachedToken; + } finally { + lock.unlock(); + } + } + + private void fetchAndCacheToken() { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setBasicAuth(this.clientId, this.clientSecret); final MultiValueMap map = new LinkedMultiValueMap<>(); map.add("grant_type", "client_credentials"); - map.add("client_id", this.clientId); - map.add("client_secret", this.clientSecret); final HttpEntity> request = new HttpEntity<>(map, headers); final ResponseEntity response = new RestTemplate().postForEntity(this.authServerUrl, request, JsonNode.class); - return "Bearer " + response.getBody().get("access_token").asText(); - } -} + JsonNode body = response.getBody(); + this.cachedToken = body.get("access_token").asText(); + int expiresIn = body.get("expires_in").asInt(); + this.tokenExpiry = Instant.now().plusSeconds(expiresIn - 10); // refresh token 10 sec before it actually expires + } +} \ No newline at end of file diff --git a/examples/inquiry-integration-service/inquiry.http b/examples/inquiry-integration-service/inquiry.http index 2c35e40dc..c42bf6753 100644 --- a/examples/inquiry-integration-service/inquiry.http +++ b/examples/inquiry-integration-service/inquiry.http @@ -1,10 +1,10 @@ ### Get Access Token -POST http://keycloak:9090/auth/realms/miranum/protocol/openid-connect/token +POST http://keycloak:9090/auth/realms/miragon/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = password & client_secret = s3creT & -client_id = miranum & +client_id = inquiry & username = alex.admin@example.com & password = test @@ -30,8 +30,8 @@ Authorization: Bearer {{ access_token }} %} -### Get Group Tasks for group1 -GET http://localhost:8083/rest/task/group/group1 +### Get Group Tasks for sales-department +GET http://localhost:8083/rest/task/group/sales-department Content-Type: application/json Authorization: Bearer {{ access_token }} @@ -41,13 +41,13 @@ Authorization: Bearer {{ access_token }} %} -### Claim Task for user alex.admin +### Claim Task for user Alex Admin POST http://localhost:8083/rest/task/{{group_task_id}}/assign Content-Type: application/json Authorization: Bearer {{ access_token }} { - "assignee": "alex.admin" + "assignee": "alex.admin@example.com" } @@ -63,7 +63,7 @@ Authorization: Bearer {{ access_token }} } -### Get assigned Tasks for user alex.admin +### Get assigned Tasks for user Alex Admin GET http://localhost:8083/rest/task/user Content-Type: application/json Authorization: Bearer {{ access_token }} diff --git a/examples/inquiry-integration-service/local.env b/examples/inquiry-integration-service/local.env index a54919dd8..1ae93f877 100644 --- a/examples/inquiry-integration-service/local.env +++ b/examples/inquiry-integration-service/local.env @@ -1,11 +1,11 @@ MIRANUM_PROCESS_INTEGRATION_EXAMPLE_PORT=8084 -WORKER_BACKOFF_STRATEGIE_DISABLED=true +WORKER_BACKOFF_STRATEGIE_DISABLED=false # # SSO Settings / Keycloak # SSO_BASE_URL=http://keycloak:9090/auth -SSO_REALM=miranum +SSO_REALM=miragon SSO_ISSUER_URL=${SSO_BASE_URL}/realms/${SSO_REALM} -SSO_WORKER_CLIENT_ID=miranum-worker -SSO_WORKER_CLIENT_SECRET=s3creT +SSO_INQUIRY_WORKER_CLIENT_ID=inquiry-worker +SSO_INQUIRY_WORKER_CLIENT_SECRET=s3creT diff --git a/examples/inquiry-integration-service/pom.xml b/examples/inquiry-integration-service/pom.xml index b64a7a2f4..0330fd3a1 100644 --- a/examples/inquiry-integration-service/pom.xml +++ b/examples/inquiry-integration-service/pom.xml @@ -75,6 +75,11 @@ spring-security-starter ${miranum-platform.version} + + io.miragon.miranum.connect + oauth-camunda7-remote + ${miranum-connect.version} + io.miragon.miranum.connect connect-camunda7-remote-all diff --git a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SecurityConfiguration.java b/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/SecurityConfiguration.java similarity index 77% rename from platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SecurityConfiguration.java rename to examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/SecurityConfiguration.java index a27ac419f..cbc725866 100644 --- a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SecurityConfiguration.java +++ b/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/SecurityConfiguration.java @@ -1,5 +1,7 @@ -package io.miragon.miranum.platform.security; +package io.miragon.miranum.inquiry; +import io.miragon.miranum.platform.security.JwtAuthenticationConverter; +import io.miragon.miranum.platform.security.SpringSecurityProperties; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,37 +16,26 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.web.SecurityFilterChain; -import static io.miragon.miranum.platform.security.SecurityConfiguration.SECURITY; - /** * The central class for configuration of all security aspects. */ +@Profile("!no-security") @Configuration -@Profile(SECURITY) @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) @RequiredArgsConstructor +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class SecurityConfiguration { - /** - * Activates security. - */ - public static final String SECURITY = "!no-security"; private final SpringSecurityProperties springSecurityProperties; @Bean public SecurityFilterChain configure(final HttpSecurity http, Converter converter) throws Exception { http.authorizeHttpRequests(auth -> - auth.requestMatchers(springSecurityProperties.getPermittedUrls()) + auth.requestMatchers(springSecurityProperties.getPermittedUrls().toArray(new String[0])) .permitAll() .anyRequest() - .authenticated() - ) - .oauth2ResourceServer(oauth2 -> - oauth2.jwt(jwt -> - jwt.jwtAuthenticationConverter(converter) - ) - ) + .authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(converter))) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); return http.build(); @@ -58,3 +49,4 @@ public Converter jwtAuthenticationConverter() } + diff --git a/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/adapter/in/rest/InquiryCreateController.java b/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/adapter/in/rest/InquiryCreateController.java index 9030c3beb..4bf15bc47 100644 --- a/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/adapter/in/rest/InquiryCreateController.java +++ b/examples/inquiry-integration-service/src/main/java/io/miragon/miranum/inquiry/adapter/in/rest/InquiryCreateController.java @@ -5,7 +5,6 @@ import io.miragon.miranum.inquiry.application.port.in.InquiryReceived; import io.miragon.miranum.inquiry.application.port.in.model.NewInquiryCommand; import io.miragon.miranum.inquiry.domain.InquiryId; -import io.miragon.miranum.platform.security.authentication.UserAuthenticationProvider; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; diff --git a/examples/inquiry-integration-service/src/main/resources/application.yml b/examples/inquiry-integration-service/src/main/resources/application.yml index 78a9afcdb..7b44a0c23 100644 --- a/examples/inquiry-integration-service/src/main/resources/application.yml +++ b/examples/inquiry-integration-service/src/main/resources/application.yml @@ -14,15 +14,14 @@ spring: name: '@project.artifactId@' miranum: - # override default security settings security: - auth: - server: - url: ${SSO_BASE_URL}/realms/${SSO_REALM}/protocol/openid-connect/token + server: + base-url: ${SSO_BASE_URL} + realm: ${SSO_REALM} + user-name-attribute: email client: - enabled: true - clientId: ${SSO_WORKER_CLIENT_ID} - clientSecret: ${SSO_WORKER_CLIENT_SECRET} + clientId: ${SSO_INQUIRY_WORKER_CLIENT_ID} + clientSecret: ${SSO_INQUIRY_WORKER_CLIENT_SECRET} permittedUrls: - "/error" - "/actuator/**" diff --git a/examples/inquiry-integration-service/src/main/resources/processes/bpmn/inquery-process.bpmn b/examples/inquiry-integration-service/src/main/resources/processes/bpmn/inquery-process.bpmn index ebdeed201..4e560f11b 100644 --- a/examples/inquiry-integration-service/src/main/resources/processes/bpmn/inquery-process.bpmn +++ b/examples/inquiry-integration-service/src/main/resources/processes/bpmn/inquery-process.bpmn @@ -5,7 +5,7 @@ Flow_0mci6qe - + check-resources @@ -29,7 +29,7 @@ Flow_066p5i1 - + create-offer diff --git a/examples/single-deployment-unit-example/single-deployment-stack/.env b/examples/single-deployment-unit-example/single-deployment-stack/.env index 4a3555169..b2ccdf82c 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/.env +++ b/examples/single-deployment-unit-example/single-deployment-stack/.env @@ -27,7 +27,7 @@ ENGINE_DATASOURCE_USER=${LOCAL_DATASOURCE_USERNAME} ENGINE_DATASOURCE_PASSWORD=${LOCAL_DATASOURCE_PASSWORD} ENGINE_DATASOURCE_URL=${LOCAL_JDBC_BASE_URL}${ENGINE_DATASOURCE_DB} ENGINE_SCHEMA_URL=http://localhost:8081/schema-registry -ENGINE_DATABASE_PLATFORM=io.miragon.miranum.platform.example.shared.configuration.NoToastPostgresSQLDialect +ENGINE_DATABASE_PLATFORM=io.miragon.miranum.platform.example.shared.database.NoToastPostgresSQLDialect # # DB Settings for Schema Service # @@ -41,15 +41,20 @@ SCHEMA_DATASOURCE_DRIVERCLASSNAME=${LOCAL_DATASOURCE_DRIVERCLASSNAME} # # SSO Settings / Keycloak # +SSO_REALM=miragon SSO_BASE_URL=http://keycloak:9090/auth -SSO_REALM=miranum SSO_ISSUER_URL=${SSO_BASE_URL}/realms/${SSO_REALM} -SSO_ENGINE_CLIENT_ID=miranum -SSO_ENGINE_CLIENT_SECRET=s3creT SSO_DATASOURCE_USERNAME=${LOCAL_DATASOURCE_USERNAME} SSO_DATASOURCE_PASSWORD=${LOCAL_DATASOURCE_PASSWORD} -SSO_WORKER_CLIENT_ID=miranum-worker -SSO_WORKER_CLIENT_SECRET=s3creT +SSO_ENGINE_CLIENT_ID=engine +SSO_ENGINE_CLIENT_SECRET=s3creT +SSO_ENGINE_WEBAPPS_REQUIRED_ROLE=admin +SSO_ENGINE_WORKER_REQUIRED_ROLE=worker +SSO_INQUIRY_CLIENT_ID=inquiry +SSO_INQUIRY_CLIENT_SECRET=s3creT +SSO_INQUIRY_WORKER_CLIENT_ID=inquiry-worker +SSO_INQUIRY_WORKER_CLIENT_SECRET=s3creT + # # Camunda Cockpit diff --git a/examples/single-deployment-unit-example/single-deployment-stack/docker-compose.yml b/examples/single-deployment-unit-example/single-deployment-stack/docker-compose.yml index d819c25eb..4d430f483 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/docker-compose.yml +++ b/examples/single-deployment-unit-example/single-deployment-stack/docker-compose.yml @@ -1,5 +1,4 @@ # Use this only in dev environments. It's not intended for production usage. -version: '3.9' services: keycloak: @@ -12,7 +11,7 @@ services: - '9090:9090' command: 'start-dev --http-relative-path /auth --http-port=9090' healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:9090/auth/realms/miranum/.well-known/openid-configuration" ] + test: [ "CMD", "curl", "-f", "http://keycloak:9090/auth/health" ] interval: 5s timeout: 2s retries: 30 @@ -21,11 +20,12 @@ services: environment: KC_HOSTNAME: keycloak # this hostname must be resolved to 127.0.0.1 locally. Add it to your hosts file. KC_HOSTNAME_STRICT: 'false' + KC_HEALTH_ENABLED: 'true' KC_DB: ${LOCAL_KEYCLOAK_DB_VENDOR} - KC_TRANSACTION_XA_ENABLED: 'false' KC_DB_URL: ${SSO_DATASOURCE_URL} KC_DB_USERNAME: ${SSO_DATASOURCE_USERNAME} KC_DB_PASSWORD: ${SSO_DATASOURCE_PASSWORD} + KC_TRANSACTION_XA_ENABLED: 'false' KEYCLOAK_ADMIN: ${SSO_ADMIN:-admin} KEYCLOAK_ADMIN_PASSWORD: ${SSO_ADMIN_PASSWORD:-admin} networks: diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/01_initial-realm.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/01_initial-realm.yml index c620c4815..8e90b3792 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/01_initial-realm.yml +++ b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/01_initial-realm.yml @@ -1,8 +1,5 @@ id: initial_realm -author: Miranum +author: Miragon changes: - addRealm: name: ${SSO_REALM} - - addGroup: - realm: ${SSO_REALM} - name: group1 diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/02_engine.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/02_engine.yml index 1a977f633..f4c4d96d9 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/02_engine.yml +++ b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/02_engine.yml @@ -1,5 +1,5 @@ id: engine -author: Miranum +author: Miragon realm: ${SSO_REALM} changes: - addSimpleClient: @@ -11,28 +11,28 @@ changes: clientId: ${SSO_ENGINE_CLIENT_ID} webOrigins: - "*" - - addRole: - clientId: ${SSO_ENGINE_CLIENT_ID} - clientRole: true - name: clientrole_deployer - description: Can deploy, assigns BACKEND_DEPLOY_RESOURCE authority on other stages then local. - - addRole: + - addClientMapper: clientId: ${SSO_ENGINE_CLIENT_ID} - clientRole: true - name: clientrole_task_importer - description: Can import user task from the engine to Polyflow. - - addRole: - clientId: ${SSO_ENGINE_CLIENT_ID} - clientRole: true - name: admin - description: Role Admin + name: userClientRole + protocolMapper: oidc-usermodel-client-role-mapper + config: + access.token.claim: true + id.token.claim: true + userinfo.token.claim: true + jsonType.label: String + multivalued: true + claim.name: "roles" + + # Camunda Webapp users will need this role - addRole: clientId: ${SSO_ENGINE_CLIENT_ID} clientRole: true - name: office - description: Office office + name: ${SSO_ENGINE_WEBAPPS_REQUIRED_ROLE} + description: Administrates the camunda webapps + + # Engine Worker (Service Accounts) will need this role - addRole: clientId: ${SSO_ENGINE_CLIENT_ID} clientRole: true - name: group1 - description: Group 1 + name: ${SSO_ENGINE_WORKER_REQUIRED_ROLE} + description: Allows workers to work on service tasks diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_engine_protocol_mapper.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_engine_protocol_mapper.yml deleted file mode 100644 index 7b6adc5a4..000000000 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_engine_protocol_mapper.yml +++ /dev/null @@ -1,26 +0,0 @@ -id: engine_protocol_mapper -author: Miranum -realm: ${SSO_REALM} -changes: - # maps user attribute user_name to a claim user_name - - addClientUserAttributeMapper: - clientId: ${SSO_ENGINE_CLIENT_ID} - name: user_name - userAttribute: user_name - claimName: user_name - addToUserInfo: true - addToAccessToken: true - # Maps user client roles of current client prefixed by "ROLE_" into a claim "user_roles" - - addClientMapper: - clientId: ${SSO_ENGINE_CLIENT_ID} - name: userClientRole - protocolMapper: oidc-usermodel-client-role-mapper - config: - access.token.claim: true - id.token.claim: true - userinfo.token.claim: true - jsonType.label: String - multivalued: true - claim.name: "roles" - usermodel.clientRoleMapping.clientId: ${SSO_ENGINE_CLIENT_ID} - usermodel.clientRoleMapping.rolePrefix: "ROLE_" diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_inquiry.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_inquiry.yml new file mode 100644 index 000000000..2b06b2669 --- /dev/null +++ b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/03_inquiry.yml @@ -0,0 +1,39 @@ +id: engine +author: Miragon +realm: ${SSO_REALM} +changes: + + # Service Account for Inquiry Backend + - addSimpleClient: + clientId: ${SSO_INQUIRY_WORKER_CLIENT_ID} + secret: ${SSO_INQUIRY_WORKER_CLIENT_SECRET} + publicClient: false + redirectUris: + - '*' + - updateClient: + clientId: ${SSO_INQUIRY_WORKER_CLIENT_ID} + serviceAccountsEnabled: true + webOrigins: + - "*" + - assignRoleToClient: + clientId: ${SSO_INQUIRY_WORKER_CLIENT_ID} + role: ${SSO_ENGINE_WORKER_REQUIRED_ROLE} + roleClientId: ${SSO_ENGINE_CLIENT_ID} + + # Inquiry Frontend users will be verified by this client + - addSimpleClient: + clientId: ${SSO_INQUIRY_CLIENT_ID} + secret: ${SSO_ENGINE_CLIENT_SECRET} + redirectUris: + - '*' + - updateClient: + clientId: ${SSO_INQUIRY_CLIENT_ID} + webOrigins: + - "*" + + - addRole: + clientId: ${SSO_INQUIRY_CLIENT_ID} + clientRole: true + name: sales-department + description: Sales Department + diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/04_users.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/04_users.yml index 6239d3be9..c8d09be24 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/04_users.yml +++ b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/04_users.yml @@ -1,7 +1,8 @@ id: users -author: Miranum +author: Miragon realm: ${SSO_REALM} changes: + - addUser: name: alex firstName: Alex @@ -9,21 +10,15 @@ changes: enabled: true emailVerified: true email: alex.admin@example.com - attributes: - user_name: - - alex.admin clientRoles: - client: ${SSO_ENGINE_CLIENT_ID} - role: clientrole_deployer - - client: ${SSO_ENGINE_CLIENT_ID} - role: clientrole_task_importer - - client: ${SSO_ENGINE_CLIENT_ID} - role: admin - - client: ${SSO_ENGINE_CLIENT_ID} - role: group1 + role: ${SSO_ENGINE_WEBAPPS_REQUIRED_ROLE} + - client: ${SSO_INQUIRY_CLIENT_ID} + role: sales-department - updateUserPassword: name: alex password: "test" + - addUser: name: oliver firstName: Oliver @@ -31,17 +26,13 @@ changes: enabled: true emailVerified: true email: oliver.office@example.com - attributes: - user_name: - - oliver.office clientRoles: - - client: ${SSO_ENGINE_CLIENT_ID} - role: office - - client: ${SSO_ENGINE_CLIENT_ID} - role: group1 + - client: ${SSO_INQUIRY_CLIENT_ID} + role: sales-department - updateUserPassword: name: oliver password: "test" + - addUser: name: olga firstName: Olga @@ -49,14 +40,9 @@ changes: enabled: true emailVerified: true email: olga.office@example.com - attributes: - user_name: - - olga.office clientRoles: - - client: ${SSO_ENGINE_CLIENT_ID} - role: office - - client: ${SSO_ENGINE_CLIENT_ID} - role: group1 + - client: ${SSO_INQUIRY_CLIENT_ID} + role: sales-department - updateUserPassword: name: olga password: "test" diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/05_worker.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/05_worker.yml deleted file mode 100644 index d7ce5bbe2..000000000 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/05_worker.yml +++ /dev/null @@ -1,15 +0,0 @@ -id: worker -author: Miranum -realm: ${SSO_REALM} -changes: - - addSimpleClient: - clientId: ${SSO_WORKER_CLIENT_ID} - secret: ${SSO_WORKER_CLIENT_SECRET} - publicClient: false - redirectUris: - - '*' - - updateClient: - clientId: ${SSO_WORKER_CLIENT_ID} - serviceAccountsEnabled: true - webOrigins: - - "*" diff --git a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/keycloak-changelog.yml b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/keycloak-changelog.yml index a28f85c7f..944e335ae 100644 --- a/examples/single-deployment-unit-example/single-deployment-stack/keycloak/keycloak-changelog.yml +++ b/examples/single-deployment-unit-example/single-deployment-stack/keycloak/keycloak-changelog.yml @@ -4,6 +4,5 @@ includes: - path: 01_initial-realm.yml - path: 02_engine.yml - - path: 03_engine_protocol_mapper.yml + - path: 03_inquiry.yml - path: 04_users.yml - - path: 05_worker.yml diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaAuthenticationFilterConfiguration.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaAuthenticationFilterConfiguration.java new file mode 100644 index 000000000..e551440fa --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaAuthenticationFilterConfiguration.java @@ -0,0 +1,36 @@ + + +package io.miragon.miranum.platform.example.engine.sso; + +import io.miragon.miranum.platform.example.engine.sso.rest.ServiceAccountAuthenticationProvider; +import org.camunda.bpm.engine.IdentityService; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Camunda ENGINE Security configuration. + * Adds the filter retrieving currently logged-in user and setting Camunda authorization to it for all REST requests. + */ +@Configuration +public class CamundaAuthenticationFilterConfiguration { + + @Bean + public FilterRegistrationBean statelessUserAuthenticationFilter( + IdentityService identityService, + ServiceAccountAuthenticationProvider userProvider + + ) { + final FilterRegistrationBean filterRegistration = new FilterRegistrationBean<>(); + filterRegistration.setFilter(new CamundaUserAuthenticationFilter( + identityService, + userProvider + )); + filterRegistration.setOrder(102); // make sure the filter is registered after the Spring Security Filter Chain + // install the filter on all protected URLs to propagate the identity from the token to Camunda and Identity Service. + filterRegistration.addUrlPatterns( + "/engine-rest/*" // camunda rest api should be protected + ); + return filterRegistration; + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaUserAuthenticationFilter.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaUserAuthenticationFilter.java new file mode 100644 index 000000000..d6204ee5c --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/CamundaUserAuthenticationFilter.java @@ -0,0 +1,41 @@ +package io.miragon.miranum.platform.example.engine.sso; + +import io.miragon.miranum.platform.example.engine.sso.rest.ServiceAccountAuthenticationProvider; +import jakarta.servlet.*; +import org.camunda.bpm.engine.IdentityService; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * Camunda ENGINE configuration. + * Filter to set authentication / authorization information. + * This information is used for restrict access to resources. + */ +class CamundaUserAuthenticationFilter implements Filter { + + private final IdentityService identityService; + private final ServiceAccountAuthenticationProvider userAuthenticationProvider; + + public CamundaUserAuthenticationFilter( + final IdentityService identityService, + final ServiceAccountAuthenticationProvider userAuthenticationProvider + ) { + this.identityService = identityService; + this.userAuthenticationProvider = userAuthenticationProvider; + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { + final String userId = userAuthenticationProvider.getLoggedInUser(); + final List roles = new ArrayList<>(userAuthenticationProvider.getLoggedInUserRoles()); + try { + identityService.setAuthentication(userId, roles, userAuthenticationProvider.getTenant()); + chain.doFilter(request, response); + } finally { + identityService.clearAuthentication(); + } + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/GrantedAuthoritiesExtractor.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/GrantedAuthoritiesExtractor.java new file mode 100644 index 000000000..f32952ff5 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/GrantedAuthoritiesExtractor.java @@ -0,0 +1,93 @@ +package io.miragon.miranum.platform.example.engine.sso; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import java.security.Principal; +import java.util.*; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +@Component +public class GrantedAuthoritiesExtractor implements Converter { + + public static final String SPRING_ROLE_PREFIX = "ROLE_"; + private static final String ROLE_DECLARATIONS = "roles"; + private static final String REALM_ROLES_CLAIM = "realm_access"; + private static final String CLIENTS_CLAIM = "resource_access"; + private static final String CLIENT_ROLE_SEPARATOR = ":"; + + @Value("${spring.security.oauth2.client.provider.keycloak.user-name-attribute}") + private String principalClaimName; + + public static List getClientAuthorities(ClaimAccessor jwt) { + // retrieve client roles of all clients + final List clientAuthorities = new ArrayList<>(); + Map clientClaims = jwt.getClaimAsMap(CLIENTS_CLAIM); + if (clientClaims != null) { + clientClaims.forEach((client, claims) -> clientAuthorities.addAll(extractRoles(client, (Map) claims))); + } + return clientAuthorities; + } + + static List extractRoles(String client, Map clientObject) { + final Collection clientRoles = (Collection) clientObject.get(ROLE_DECLARATIONS); + if (clientRoles != null) { + return clientRoles + .stream() + .map(role -> client + CLIENT_ROLE_SEPARATOR + role) + .collect(toList()); + } else { + return Collections.emptyList(); + } + } + + public static List extractRoles(Principal principal) { + if (principal instanceof Authentication) { + return ((Authentication) principal).getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(role -> StringUtils.removeStart(role, SPRING_ROLE_PREFIX)) + .collect(toList()); + } + return emptyList(); + } + + @Override + public final AbstractAuthenticationToken convert(Jwt jwt) { + Collection authorities = this.extractAuthorities(jwt); + + String principalClaimValue = jwt.getClaimAsString(this.principalClaimName); + return new JwtAuthenticationToken(jwt, authorities, principalClaimValue); + } + + protected Collection extractAuthorities(Jwt jwt) { + + // Retrieve client roles of all clients + final Collection clientAuthorities = getClientAuthorities(jwt); + + // Retrieve realm roles + final Map realmAccess = jwt.getClaimAsMap(REALM_ROLES_CLAIM); + + Collection realmAuthorities = Collections.emptyList(); + if (realmAccess != null && realmAccess.containsKey(ROLE_DECLARATIONS)) { + realmAuthorities = (Collection) realmAccess.get(ROLE_DECLARATIONS); + } + + return Stream.concat(realmAuthorities.stream(), clientAuthorities.stream()) + .map(s -> SPRING_ROLE_PREFIX + s) + .map(SimpleGrantedAuthority::new) + .collect(toList()); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOAuth2UserService.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOAuth2UserService.java new file mode 100644 index 000000000..c0c14b047 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOAuth2UserService.java @@ -0,0 +1,42 @@ +package io.miragon.miranum.platform.example.engine.sso; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class TokenParsingOAuth2UserService implements OAuth2UserService { + private final GrantedAuthoritiesExtractor grantedAuthoritiesExtractor; + private final ConcurrentHashMap jwtDecoders = new ConcurrentHashMap<>(); + + public TokenParsingOAuth2UserService(GrantedAuthoritiesExtractor grantedAuthoritiesExtractor) { + this.grantedAuthoritiesExtractor = grantedAuthoritiesExtractor; + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + + ClientRegistration clientRegistration = userRequest.getClientRegistration(); + JwtDecoder jwtDecoder = jwtDecoders.computeIfAbsent( + clientRegistration.getRegistrationId(), + ignored -> NimbusJwtDecoder.withJwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri()).build() + ); + Jwt jwt = jwtDecoder.decode(userRequest.getAccessToken().getTokenValue()); + JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) grantedAuthoritiesExtractor.convert(jwt); + + return new DefaultOAuth2User( + authenticationToken.getAuthorities(), + authenticationToken.getTokenAttributes(), + clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName() + ); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOidcUserService.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOidcUserService.java new file mode 100644 index 000000000..ba50f109e --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/TokenParsingOidcUserService.java @@ -0,0 +1,28 @@ +package io.miragon.miranum.platform.example.engine.sso; + +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; + + +public class TokenParsingOidcUserService implements OAuth2UserService { + + private final TokenParsingOAuth2UserService delegate; + + public TokenParsingOidcUserService(TokenParsingOAuth2UserService delegate) { + this.delegate = delegate; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) { + OAuth2User oAuth2User = delegate.loadUser(userRequest); + return new DefaultOidcUser( + oAuth2User.getAuthorities(), + userRequest.getIdToken(), + new OidcUserInfo(oAuth2User.getAttributes()), + userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthenticationInitializer.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthenticationInitializer.java new file mode 100644 index 000000000..9585b6611 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthenticationInitializer.java @@ -0,0 +1,50 @@ +package io.miragon.miranum.platform.example.engine.sso.init; + +import org.camunda.bpm.engine.AuthorizationService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class AuthenticationInitializer { + + public AuthenticationInitializer( + @Value("${camunda.sso.webapps-role}") String webappsRole, + @Value("${camunda.sso.worker-role}") String workerRole, + AuthorizationService authorizationService + ) { + + // User with admin rights + AuthorizationHelper.setupGroupAppPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupProcessDefinitionPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupProcessInstancePermissions(authorizationService, webappsRole); + AuthorizationHelper.setupUserTaskPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupHistoricTaskPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupHistoricProcessInstancePermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupBatchPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupDashboardPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupReportPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupOpLogPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupDeploymentPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupDecisionRequirementPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupDecisionPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupSystemPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupAuthorizationPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupGroupPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupGroupMembershipPermissions(authorizationService, webappsRole); + AuthorizationHelper.setupGroupUserPermissions(authorizationService, webappsRole); + + // Worker + AuthorizationHelper.setupGroupAppPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupProcessDefinitionPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupProcessInstancePermissions(authorizationService, workerRole); + AuthorizationHelper.setupUserTaskPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupHistoricTaskPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupHistoricProcessInstancePermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupDashboardPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupDecisionRequirementPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupDecisionPermissions(authorizationService, workerRole); + AuthorizationHelper.setupGroupDeploymentPermissions(authorizationService, workerRole); + + } + +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthorizationHelper.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthorizationHelper.java new file mode 100644 index 000000000..a33da3bcb --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/init/AuthorizationHelper.java @@ -0,0 +1,256 @@ +package io.miragon.miranum.platform.example.engine.sso.init; + +import lombok.extern.slf4j.Slf4j; +import org.camunda.bpm.engine.AuthorizationService; +import org.camunda.bpm.engine.authorization.Authorization; +import org.camunda.bpm.engine.authorization.Permissions; +import org.camunda.bpm.engine.authorization.Resources; + +@Slf4j +public class AuthorizationHelper { + + public static void setupGroupAppPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.APPLICATION)) { + return; + } + log.info("Setting up Web App Permissions for group '{}'", groupId); + Authorization appAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + appAuth.setGroupId(groupId); + appAuth.addPermission(Permissions.ACCESS); + appAuth.setResource(Resources.APPLICATION); + appAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(appAuth); + } + + public static void setupUserTaskPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.TASK)) { + return; + } + log.info("Setting up Camunda Task Permissions for group '{}'", groupId); + Authorization taskAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + taskAuth.setGroupId(groupId); + taskAuth.addPermission(Permissions.ALL); + taskAuth.setResource(Resources.TASK); + taskAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(taskAuth); + } + + public static void setupGroupProcessDefinitionPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.PROCESS_DEFINITION)) { + return; + } + log.info("Setting up Camunda Process Definition Permissions for group '{}'", groupId); + Authorization processDefinitionAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + processDefinitionAuth.setGroupId(groupId); + processDefinitionAuth.addPermission(Permissions.ALL); + processDefinitionAuth.setResource(Resources.PROCESS_DEFINITION); + processDefinitionAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(processDefinitionAuth); + } + + public static void setupGroupHistoricProcessInstancePermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.HISTORIC_PROCESS_INSTANCE)) { + return; + } + log.info("Setting up Camunda Historic Process Instance Permissions for group '{}'", groupId); + Authorization historicProcessInstanceAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + historicProcessInstanceAuth.setGroupId(groupId); + historicProcessInstanceAuth.addPermission(Permissions.ALL); + historicProcessInstanceAuth.setResource(Resources.HISTORIC_PROCESS_INSTANCE); + historicProcessInstanceAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(historicProcessInstanceAuth); + } + + public static void setupGroupDashboardPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.DASHBOARD)) { + return; + } + log.info("Setting up Camunda Dashboard Permissions for groupId '{}'", groupId); + Authorization dashboardAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + dashboardAuth.setGroupId(groupId); + dashboardAuth.addPermission(Permissions.ALL); + dashboardAuth.setResource(Resources.DASHBOARD); + dashboardAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(dashboardAuth); + } + + public static void setupGroupDecisionRequirementPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.DECISION_REQUIREMENTS_DEFINITION)) { + return; + } + log.info("Setting up Camunda DRD Permissions for group '{}'", groupId); + Authorization decisionRequirementsAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + decisionRequirementsAuth.setGroupId(groupId); + decisionRequirementsAuth.addPermission(Permissions.ALL); + decisionRequirementsAuth.setResource(Resources.DECISION_REQUIREMENTS_DEFINITION); + decisionRequirementsAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(decisionRequirementsAuth); + } + + public static void setupGroupDecisionPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.DECISION_DEFINITION)) { + return; + } + log.info("Setting up Camunda Decision Permissions for group '{}'", groupId); + Authorization decisionAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + decisionAuth.setGroupId(groupId); + decisionAuth.addPermission(Permissions.ALL); + decisionAuth.setResource(Resources.DECISION_DEFINITION); + decisionAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(decisionAuth); + } + + public static void setupGroupHistoricTaskPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.HISTORIC_TASK)) { + return; + } + log.info("Setting up Camunda Historic Task Permissions for group '{}'", groupId); + Authorization historicTaskAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + historicTaskAuth.setGroupId(groupId); + historicTaskAuth.addPermission(Permissions.ALL); + historicTaskAuth.setResource(Resources.HISTORIC_TASK); + historicTaskAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(historicTaskAuth); + } + + public static void setupGroupProcessInstancePermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.PROCESS_INSTANCE)) { + return; + } + log.info("Setting up Camunda Process Instance Permissions for user '{}'", groupId); + Authorization processInstanceAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + processInstanceAuth.setGroupId(groupId); + processInstanceAuth.addPermission(Permissions.ALL); + processInstanceAuth.setResource(Resources.PROCESS_INSTANCE); + processInstanceAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(processInstanceAuth); + } + + public static void setupGroupDeploymentPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.DEPLOYMENT)) { + return; + } + log.info("Setting up Camunda Deployment Permissions for user '{}'", groupId); + Authorization deploymentAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + deploymentAuth.setGroupId(groupId); + deploymentAuth.addPermission(Permissions.ALL); + deploymentAuth.setResource(Resources.DEPLOYMENT); + deploymentAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(deploymentAuth); + } + + public static void setupGroupBatchPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.BATCH)) { + return; + } + log.info("Setting up Batch Permissions for user '{}'", groupId); + Authorization batchAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + batchAuth.setGroupId(groupId); + batchAuth.addPermission(Permissions.CREATE); + batchAuth.addPermission(Permissions.READ); + batchAuth.addPermission(Permissions.DELETE); + batchAuth.addPermission(Permissions.UPDATE); + batchAuth.setResource(Resources.BATCH); + batchAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(batchAuth); + } + + public static void setupGroupReportPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.REPORT)) { + return; + } + log.info("Setting up Camunda Report Permissions for user '{}'", groupId); + Authorization reportAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + reportAuth.setGroupId(groupId); + reportAuth.addPermission(Permissions.ALL); + reportAuth.setResource(Resources.REPORT); + reportAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(reportAuth); + } + + public static void setupGroupOpLogPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.REPORT)) { + return; + } + if (authorizationService.createAuthorizationQuery().groupIdIn(groupId).resourceType(Resources.OPERATION_LOG_CATEGORY).count() != 0L) { + return; + } + log.info("Setting up Camunda Operation Log Permissions for user '{}'", groupId); + Authorization opLogAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + opLogAuth.setGroupId(groupId); + opLogAuth.addPermission(Permissions.ALL); + opLogAuth.setResource(Resources.OPERATION_LOG_CATEGORY); + opLogAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(opLogAuth); + } + + public static void setupGroupSystemPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.SYSTEM)) { + return; + } + log.info("Setting up Camunda System Permissions for user '{}'", groupId); + Authorization systemAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + systemAuth.setGroupId(groupId); + systemAuth.addPermission(Permissions.ALL); + systemAuth.setResource(Resources.SYSTEM); + systemAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(systemAuth); + } + + public static void setupGroupAuthorizationPermissions(AuthorizationService authorizationService, String groupId) { + if (existsByGroupIdAndResourceType(authorizationService, groupId, Resources.AUTHORIZATION)) { + return; + } + log.info("Setting up Authorization Permissions for group '{}'", groupId); + Authorization authorizationAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + authorizationAuth.setGroupId(groupId); + authorizationAuth.addPermission(Permissions.ALL); + authorizationAuth.setResource(Resources.AUTHORIZATION); + authorizationAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(authorizationAuth); + } + + public static void setupGroupGroupPermissions(AuthorizationService authorizationService, String groupId) { + if (authorizationService.createAuthorizationQuery().groupIdIn(groupId).resourceType(Resources.GROUP).count() != 0L) { + return; + } + log.info("Setting up Group Permissions for group '{}'", groupId); + Authorization groupAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + groupAuth.setGroupId(groupId); + groupAuth.addPermission(Permissions.ALL); + groupAuth.setResource(Resources.GROUP); + groupAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(groupAuth); + } + + public static void setupGroupGroupMembershipPermissions(AuthorizationService authorizationService, String groupId) { + if (authorizationService.createAuthorizationQuery().groupIdIn(groupId).resourceType(Resources.GROUP_MEMBERSHIP).count() != 0L) { + return; + } + log.info("Setting up Group Membership Permissions for group '{}'", groupId); + Authorization groupMembershipAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + groupMembershipAuth.setGroupId(groupId); + groupMembershipAuth.addPermission(Permissions.ALL); + groupMembershipAuth.setResource(Resources.GROUP_MEMBERSHIP); + groupMembershipAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(groupMembershipAuth); + } + + public static void setupGroupUserPermissions(AuthorizationService authorizationService, String groupId) { + if (authorizationService.createAuthorizationQuery().groupIdIn(groupId).resourceType(Resources.USER).count() != 0L) { + return; + } + log.info("Setting up User Permissions for group '{}'", groupId); + Authorization userAuth = authorizationService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT); + userAuth.setGroupId(groupId); + userAuth.addPermission(Permissions.ALL); + userAuth.setResource(Resources.USER); + userAuth.setResourceId(Authorization.ANY); + authorizationService.saveAuthorization(userAuth); + } + + private static boolean existsByGroupIdAndResourceType(AuthorizationService authorizationService, String groupId, Resources resources) { + return authorizationService.createAuthorizationQuery().groupIdIn(groupId).resourceType(resources).count() != 0L; + } +} + diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/RestExceptionHandler.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/RestExceptionHandler.java new file mode 100644 index 000000000..fe4167a1b --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/RestExceptionHandler.java @@ -0,0 +1,38 @@ +package io.miragon.miranum.platform.example.engine.sso.rest; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import org.camunda.bpm.engine.rest.dto.ExceptionDto; +import org.camunda.bpm.engine.rest.exception.ExceptionHandlerHelper; +import org.camunda.bpm.engine.rest.exception.RestException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.camunda.commons.utils.StringUtil.getStackTrace; + + +@Provider +public class RestExceptionHandler extends org.camunda.bpm.engine.rest.exception.RestExceptionHandler { + + private static final Logger LOGGER = Logger.getLogger(RestExceptionHandler.class.getSimpleName()); + + @Override + public Response toResponse(RestException exception) { + Response.Status responseStatus = ExceptionHandlerHelper.getInstance().getStatus(exception); + ExceptionDto exceptionDto = ExceptionHandlerHelper.getInstance().fromException(exception); + + if (responseStatus == Response.Status.INTERNAL_SERVER_ERROR) { + LOGGER.log(Level.WARNING, getStackTrace(exception)); + } else if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, getStackTrace(exception)); + } + + return Response + .status(responseStatus) + .entity(exceptionDto) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/ServiceAccountAuthenticationProvider.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/ServiceAccountAuthenticationProvider.java new file mode 100644 index 000000000..1d48d6c03 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/rest/ServiceAccountAuthenticationProvider.java @@ -0,0 +1,48 @@ + + +package io.miragon.miranum.platform.example.engine.sso.rest; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; + +import java.util.*; + +import static io.miragon.miranum.platform.example.engine.sso.GrantedAuthoritiesExtractor.SPRING_ROLE_PREFIX; + +/** + * User authentication provider. + * Extracts the username from the token. + */ +@Component +public class ServiceAccountAuthenticationProvider { + + public static final String NAME_UNAUTHENTICATED = "unauthenticated"; + private final static String SERVICE_ACCOUNT_CLIENT_NAME_CLAIM = "clientId"; + + public String getLoggedInUser() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof AbstractAuthenticationToken && authentication.getPrincipal() instanceof Jwt jwt) { + String clientId = (String) jwt.getClaims().get(SERVICE_ACCOUNT_CLIENT_NAME_CLAIM); + return Objects.isNull(clientId) ? NAME_UNAUTHENTICATED : clientId; + } + return NAME_UNAUTHENTICATED; + } + + public List getTenant() { + return List.of("default"); + } + + public Set getLoggedInUserRoles() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final List extractedRoles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(role -> StringUtils.removeStart(role, SPRING_ROLE_PREFIX)) + .toList(); + return new HashSet<>(extractedRoles); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthContainerBasedAuthenticationProvider.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthContainerBasedAuthenticationProvider.java new file mode 100644 index 000000000..dba78a6dc --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthContainerBasedAuthenticationProvider.java @@ -0,0 +1,53 @@ +package io.miragon.miranum.platform.example.engine.sso.webapp; + +import io.miragon.miranum.platform.example.engine.sso.GrantedAuthoritiesExtractor; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.camunda.bpm.engine.ProcessEngine; +import org.camunda.bpm.engine.rest.security.auth.AuthenticationProvider; +import org.camunda.bpm.engine.rest.security.auth.AuthenticationResult; + +import java.security.Principal; +import java.util.List; + + +/** + * Camunda WEBAPP configuration. + * Similar to camunda's {@link org.camunda.bpm.engine.rest.security.auth.impl.ContainerBasedAuthenticationProvider} but also adds SSO roles to the authentication result. + */ +public class OAuthContainerBasedAuthenticationProvider implements AuthenticationProvider { + + public static List getTenants(Principal principal) { + return List.of("default"); + } + + @Override + public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) { + Principal principal = request.getUserPrincipal(); + + if (principal == null) { + return AuthenticationResult.unsuccessful(); + } + + String name = principal.getName(); + if (name == null || name.isEmpty()) { + return AuthenticationResult.unsuccessful(); + } + + AuthenticationResult result = AuthenticationResult.successful(name); + result.setGroups(GrantedAuthoritiesExtractor.extractRoles(principal)); + + final List tenants = getTenants(principal); + + if (!tenants.isEmpty()) { + result.setTenants(tenants); + } + + return result; + } + + @Override + public void augmentResponseByAuthenticationChallenge(HttpServletResponse response, ProcessEngine engine) { + // noop + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthIdentityServiceProvider.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthIdentityServiceProvider.java new file mode 100644 index 000000000..614f396d0 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthIdentityServiceProvider.java @@ -0,0 +1,399 @@ +package io.miragon.miranum.platform.example.engine.sso.webapp; + +import org.apache.commons.lang3.StringUtils; +import org.camunda.bpm.engine.identity.*; +import org.camunda.bpm.engine.impl.GroupQueryImpl; +import org.camunda.bpm.engine.impl.Page; +import org.camunda.bpm.engine.impl.TenantQueryImpl; +import org.camunda.bpm.engine.impl.UserQueryImpl; +import org.camunda.bpm.engine.impl.identity.ReadOnlyIdentityProvider; +import org.camunda.bpm.engine.impl.interceptor.CommandContext; +import org.camunda.bpm.engine.impl.persistence.AbstractManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static io.miragon.miranum.platform.example.engine.sso.GrantedAuthoritiesExtractor.SPRING_ROLE_PREFIX; +import static java.util.stream.Collectors.toList; + +/** + * Camunda WEBAPP configuration. + */ +public class OAuthIdentityServiceProvider extends AbstractManager implements ReadOnlyIdentityProvider { + + @Value("${spring.security.oauth2.client.provider.keycloak.user-name-attribute}") + private String principalClaimName; + + /* + * USER IMPLEMENTATION + */ + @Override + public User findUserById(String userId) { + return createUserQuery().userId(userId).singleResult(); + } + + @Override + public UserQuery createUserQuery() { + return new OAuthUserQueryImpl(this); + } + + @Override + public UserQuery createUserQuery(CommandContext commandContext) { + return new OAuthUserQueryImpl(this); + } + + @Override + public NativeUserQuery createNativeUserQuery() { + return null; + } + + @Override + public boolean checkPassword(String userId, String password) { + return false; + } + + /* + * GROUP IMPLEMENTATION + */ + + @Override + public Group findGroupById(String groupId) { + return createGroupQuery().groupId(groupId).singleResult(); + } + + @Override + public GroupQuery createGroupQuery() { + return new OAuthGroupQueryImpl(this); + } + + @Override + public GroupQuery createGroupQuery(CommandContext commandContext) { + return new OAuthGroupQueryImpl(this); + } + + /* + * TENANT IMPLEMENTATION + */ + + @Override + public Tenant findTenantById(String tenantId) { + return createTenantQuery().tenantId(tenantId).singleResult(); + } + + @Override + public TenantQuery createTenantQuery() { + return new OAuthTenantQueryImpl(this); + } + + @Override + public TenantQuery createTenantQuery(CommandContext commandContext) { + return createTenantQuery(); + } + + /* + * HELPER + */ + + private DefaultOAuth2User getAuthorizedUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof DefaultOAuth2User) { + return ((DefaultOAuth2User) authentication.getPrincipal()); + } else { + return null; + } + } + + private List getAuthorizedUserTenants() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof DefaultOAuth2User) { + return OAuthContainerBasedAuthenticationProvider.getTenants(authentication); + } else { + return Collections.emptyList(); + } + } + + private User single(OAuthUserQueryImpl oAuthUserQuery) { + + DefaultOAuth2User user = getAuthorizedUser(); + if (!Objects.isNull(user)) { + Map claims = user.getAttributes(); + String userId = (String) claims.get(principalClaimName); + return new OAuthUser( + userId, + (String) claims.getOrDefault("given_name", userId), + (String) claims.getOrDefault("family_name", userId), + (String) claims.getOrDefault("email", userId) + ); + } + return null; + } + + private Group single(OAuthGroupQueryImpl oAuthGroupQuery) { + return list(oAuthGroupQuery) + .stream() + .filter(group -> group.getId().equals(oAuthGroupQuery.getId())) + .findFirst().orElse(null); + } + + private Tenant single(TenantQueryImpl tenantQuery) { + return getAuthorizedUserTenants().stream() + .filter(tenant -> tenant.equals(tenantQuery.getId())) + .map(OAuthTenant::new) + .findFirst().orElse(null); + } + + private List list(OAuthGroupQueryImpl oAuthGroupQuery) { + DefaultOAuth2User user = getAuthorizedUser(); + if (user != null) { + return user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(role -> StringUtils.removeStart(role, SPRING_ROLE_PREFIX)) + .map(role -> new OAuthGroup(role, role, "oauth")) + .collect(toList()); + } + return Collections.emptyList(); + } + + private List list(OAuthUserQueryImpl oAuthUserQuery) { + return Collections.emptyList(); + } + + private List list(OAuthTenantQueryImpl oAuthUserQuery) { + return getAuthorizedUserTenants().stream() + .map(OAuthTenant::new) + .collect(toList()); + } + + private long count(OAuthUserQueryImpl oAuthUserQuery) { + return list(oAuthUserQuery).size(); + } + + private long count(OAuthGroupQueryImpl oAuthGroupQuery) { + return list(oAuthGroupQuery).size(); + } + + private long count(OAuthTenantQueryImpl oAuthTenantQuery) { + return list(oAuthTenantQuery).size(); + } + + static class OAuthUserQueryImpl extends UserQueryImpl { + + private final OAuthIdentityServiceProvider oAuthIdentityServiceProvider; + + public OAuthUserQueryImpl(OAuthIdentityServiceProvider oAuthIdentityServiceProvider) { + this.oAuthIdentityServiceProvider = oAuthIdentityServiceProvider; + } + + @Override + public long executeCount(CommandContext commandContext) { + return oAuthIdentityServiceProvider.count(this); + } + + @Override + public List executeList(CommandContext commandContext, Page page) { + return oAuthIdentityServiceProvider.list(this); + } + + @Override + public User singleResult() { + return oAuthIdentityServiceProvider.single(this); + } + } + + static class OAuthGroupQueryImpl extends GroupQueryImpl { + + private final OAuthIdentityServiceProvider oAuthIdentityServiceProvider; + + public OAuthGroupQueryImpl(OAuthIdentityServiceProvider oAuthIdentityServiceProvider) { + this.oAuthIdentityServiceProvider = oAuthIdentityServiceProvider; + } + + @Override + public long executeCount(CommandContext commandContext) { + return oAuthIdentityServiceProvider.count(this); + } + + @Override + public List executeList(CommandContext commandContext, Page page) { + return oAuthIdentityServiceProvider.list(this); + } + + @Override + public Group singleResult() { + return oAuthIdentityServiceProvider.single(this); + } + } + + static class OAuthTenantQueryImpl extends TenantQueryImpl { + + private final OAuthIdentityServiceProvider oAuthIdentityServiceProvider; + + public OAuthTenantQueryImpl(OAuthIdentityServiceProvider oAuthIdentityServiceProvider) { + this.oAuthIdentityServiceProvider = oAuthIdentityServiceProvider; + } + + @Override + public long executeCount(CommandContext commandContext) { + return oAuthIdentityServiceProvider.count(this); + } + + @Override + public List executeList(CommandContext commandContext, Page page) { + return oAuthIdentityServiceProvider.list(this); + } + + @Override + public Tenant singleResult() { + return oAuthIdentityServiceProvider.single(this); + } + } + + static class OAuthTenant implements Tenant { + + private final String id; + private final String name; + + OAuthTenant(String tenant) { + this.id = tenant; + this.name = tenant; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public void setId(String id) { + throw new UnsupportedOperationException("Can't set tenant id"); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + throw new UnsupportedOperationException("Can't set tenant name"); + } + } + + static class OAuthUser implements User { + + private final String id; + private final String firstName; + private final String lastName; + private final String emailAddress; + + OAuthUser(String id, String firstName, String lastName, String emailAddress) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.emailAddress = emailAddress; + } + + @Override + public String getId() { + return id; + } + + @Override + public void setId(String s) { + throw new UnsupportedOperationException("Can't change user attributes"); + } + + @Override + public String getFirstName() { + return firstName; + } + + @Override + public void setFirstName(String s) { + throw new UnsupportedOperationException("Can't change user attributes"); + } + + @Override + public String getLastName() { + return lastName; + } + + @Override + public void setLastName(String s) { + throw new UnsupportedOperationException("Can't change user attributes"); + } + + @Override + public String getEmail() { + return emailAddress; + } + + @Override + public void setEmail(String s) { + throw new UnsupportedOperationException("Can't change user attributes"); + } + + @Override + public String getPassword() { + throw new UnsupportedOperationException("Can't read user's password"); + } + + @Override + public void setPassword(String s) { + throw new UnsupportedOperationException("Can't change user attributes"); + } + } + + static class OAuthGroup implements Group { + + private final String id; + private final String name; + private final String type; + + OAuthGroup(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public void setId(String id) { + throw new UnsupportedOperationException("Can't set group id"); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + throw new UnsupportedOperationException("Can't set group name"); + } + + @Override + public String getType() { + return this.type; + } + + @Override + public void setType(String string) { + throw new UnsupportedOperationException("Can't set group type"); + } + } + + +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthLogoutHandler.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthLogoutHandler.java new file mode 100644 index 000000000..4857845bc --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/OAuthLogoutHandler.java @@ -0,0 +1,46 @@ +package io.miragon.miranum.platform.example.engine.sso.webapp; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +@Component +@Slf4j +public class OAuthLogoutHandler implements LogoutSuccessHandler { + + private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private String oauth2UserLogoutUri; + + public OAuthLogoutHandler(@Value("${spring.security.oauth2.client.provider.keycloak.authorization-uri}") String oauth2UserAuthorizationUri) { + if (!oauth2UserAuthorizationUri.isEmpty()) { + // in order to get the valid logout uri: simply replace "/auth" at the end of the user authorization uri with "/logout" + this.oauth2UserLogoutUri = oauth2UserAuthorizationUri.replace("openid-connect/auth", "openid-connect/logout"); + } + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + if (!oauth2UserLogoutUri.isEmpty()) { + String requestUrl = request.getRequestURL().toString(); + String redirectUri = requestUrl.substring(0, requestUrl.indexOf("/api")); + String idToken = ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); + String logoutUrl = String.format("%s?post_logout_redirect_uri=%s&id_token_hint=%s", + oauth2UserLogoutUri, + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), + URLEncoder.encode(idToken, StandardCharsets.UTF_8)); + + redirectStrategy.sendRedirect(request, response, logoutUrl); + } + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/ReadOnlyIdentityProviderConfiguration.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/ReadOnlyIdentityProviderConfiguration.java new file mode 100644 index 000000000..4d3d9bfe6 --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/engine/sso/webapp/ReadOnlyIdentityProviderConfiguration.java @@ -0,0 +1,20 @@ +package io.miragon.miranum.platform.example.engine.sso.webapp; + +import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.camunda.bpm.engine.impl.persistence.GenericManagerFactory; +import org.camunda.bpm.spring.boot.starter.configuration.CamundaProcessEngineConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * Camunda ENGINE configuration. + * Sets the identity provider to read-only. + */ +@Configuration +public class ReadOnlyIdentityProviderConfiguration implements CamundaProcessEngineConfiguration { + @Override + public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) { + processEngineConfiguration.setIdentityProviderSessionFactory( + new GenericManagerFactory(OAuthIdentityServiceProvider.class) + ); + } +} diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/configuration/NoToastPostgresSQLDialect.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/database/NoToastPostgresSQLDialect.java similarity index 96% rename from examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/configuration/NoToastPostgresSQLDialect.java rename to examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/database/NoToastPostgresSQLDialect.java index 4b463c450..00fce7162 100644 --- a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/configuration/NoToastPostgresSQLDialect.java +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/database/NoToastPostgresSQLDialect.java @@ -1,4 +1,4 @@ -package io.miragon.miranum.platform.example.shared.configuration; +package io.miragon.miranum.platform.example.shared.database; import lombok.val; import org.hibernate.boot.model.TypeContributions; diff --git a/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/security/SecurityConfiguration.java b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/security/SecurityConfiguration.java new file mode 100644 index 000000000..efff3bfee --- /dev/null +++ b/examples/single-deployment-unit-example/src/main/java/io/miragon/miranum/platform/example/shared/security/SecurityConfiguration.java @@ -0,0 +1,138 @@ +package io.miragon.miranum.platform.example.shared.security; + +import io.miragon.miranum.platform.example.engine.sso.GrantedAuthoritiesExtractor; +import io.miragon.miranum.platform.example.engine.sso.TokenParsingOAuth2UserService; +import io.miragon.miranum.platform.example.engine.sso.TokenParsingOidcUserService; +import io.miragon.miranum.platform.example.engine.sso.rest.RestExceptionHandler; +import io.miragon.miranum.platform.example.engine.sso.webapp.OAuthContainerBasedAuthenticationProvider; +import io.miragon.miranum.platform.example.engine.sso.webapp.OAuthLogoutHandler; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.DispatcherType; +import lombok.RequiredArgsConstructor; +import org.camunda.bpm.engine.rest.impl.CamundaRestResources; +import org.camunda.bpm.engine.rest.security.auth.ProcessEngineAuthenticationFilter; +import org.camunda.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.filter.ForwardedHeaderFilter; + +import java.util.EnumSet; +import java.util.Set; + +import static java.util.Collections.singletonMap; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +@Configuration +@Profile("!no-security") +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) +@RequiredArgsConstructor +public class SecurityConfiguration { + + private final TokenParsingOAuth2UserService oAuth2UserService; + private final GrantedAuthoritiesExtractor grantedAuthoritiesExtractor; + private final OAuthLogoutHandler oAuthLogoutHandler; + + @Value("${camunda.sso.webapps-role}") + private String webappsRole; + + @Value("${camunda.sso.worker-role}") + private String workerRole; + + private final String registration = "keycloak"; + + @Bean + protected SecurityFilterChain configure(HttpSecurity http) throws Exception { + + return http + .csrf(csrf -> csrf.ignoringRequestMatchers( + antMatcher("/rest/**"), + antMatcher("/assets/**"), + antMatcher("/lib/**"), + antMatcher("/actuator/**"), + antMatcher("/camunda/**"), + antMatcher("/engine-rest/**") + )) + // oAuth2 login via keycloak + .authorizeHttpRequests(authorize -> authorize + .requestMatchers( + antMatcher("/camunda/**"), + antMatcher("/rest/**"), + antMatcher("/assets/**"), + antMatcher("/lib/**") + ).hasRole(webappsRole) + ) + .oauth2Login( + oauth2Login -> oauth2Login + .authorizationEndpoint(endpoint -> + endpoint.baseUri("/camunda" + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI) + ) + .userInfoEndpoint(userinfo -> userinfo + .userService(oAuth2UserService) + .oidcUserService(new TokenParsingOidcUserService(oAuth2UserService)) + ) + .loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI) + .loginPage("/camunda" + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registration) + .successHandler((request, response, authentication) -> response.sendRedirect( "/camunda/app/cockpit/default/")) + ).logout(logout -> logout + .logoutRequestMatcher(antMatcher("/camunda/**/logout")) + .logoutSuccessHandler(oAuthLogoutHandler) + ) + // oAuth2 service accounts via keycloak + .authorizeHttpRequests(authorize -> authorize + .requestMatchers( + antMatcher(HttpMethod.OPTIONS), + antMatcher("/actuator/**"), + antMatcher("/error"), + antMatcher("/public") + ).permitAll() + .requestMatchers( + antMatcher("/engine-rest/**"), + antMatcher("/rest/**") + ).hasRole(workerRole) + ).oauth2ResourceServer(resource -> resource + .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor)) + ).build(); + } + + + // The ForwardedHeaderFilter is required to correctly assemble the redirect URL for OAUth2 login. Without the filter, Spring generates an http URL even though the OpenShift + // route is accessed through https. + @Bean + public FilterRegistrationBean forwardedHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new ForwardedHeaderFilter()); + filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return filterRegistrationBean; + } + + // This filter is responsible for integrating the camunda webapps security with spring security. It is configured with the ContainerBasedAuthenticationProvider defined below. + @Bean + public FilterRegistrationBean containerBasedAuthenticationFilterRegistrationBean() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(new ContainerBasedAuthenticationFilter()); + registrationBean.setInitParameters(singletonMap(ProcessEngineAuthenticationFilter.AUTHENTICATION_PROVIDER_PARAM, OAuthContainerBasedAuthenticationProvider.class.getName())); + registrationBean.addUrlPatterns("/*"); + registrationBean.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST)); + return registrationBean; + } + + // Quite a dirty hack to replace camunda's RestExceptionHandler with our own that logs exceptions more selectively. + // Workaround for https://app.camunda.com/jira/browse/CAM-10799. + @PostConstruct + public void replaceRestExceptionHandler() { + Set> configurationClasses = CamundaRestResources.getConfigurationClasses(); + configurationClasses.remove(org.camunda.bpm.engine.rest.exception.RestExceptionHandler.class); + configurationClasses.add(RestExceptionHandler.class); + } +} diff --git a/examples/single-deployment-unit-example/src/main/resources/application.yml b/examples/single-deployment-unit-example/src/main/resources/application.yml index 9a077148f..7a60a0d51 100644 --- a/examples/single-deployment-unit-example/src/main/resources/application.yml +++ b/examples/single-deployment-unit-example/src/main/resources/application.yml @@ -3,13 +3,19 @@ info: name: '@project.artifactId@' version: '@project.version@' -camunda: - authorization: - enabled: true +camunda: # https://docs.camunda.org/manual/7.21/user-guide/spring-boot-integration/configuration + sso: + webapps-role: "${SSO_ENGINE_CLIENT_ID}:${SSO_ENGINE_WEBAPPS_REQUIRED_ROLE}" + worker-role: "${SSO_ENGINE_CLIENT_ID}:${SSO_ENGINE_WORKER_REQUIRED_ROLE}" bpm: authorization: - enabled: false + enabled: true job-executor-acquire-by-priority: true + webapp: + index-redirect-enabled: true + header-security: + xss-protection-option: SANITIZE # to show keycloak logout page + content-security-policy-disabled: true # to show keycloak logout page generic-properties: properties: history-time-to-live: '185' @@ -21,16 +27,12 @@ camunda: enable-exceptions-after-unhandled-bpmn-error: true database: schema-update: false - auto-deployment-enabled: true - admin-user: - id: ${CAMUNDA_COCKPIT_USERNAME:demo} - password: ${CAMUNDA_COCKPIT_PASSWORD:demo} - firstName: ${CAMUNDA_COCKPIT_USER_FIRSTNAME:Demo} filter: create: All tasks metrics: enabled: false db-reporter-activate: false + auto-deployment-enabled: false deployment-resource-pattern: "processes/*.bpmn" management: @@ -88,25 +90,6 @@ spring: enabled: true locations: "classpath:db/migration/{vendor}" - security: - oauth2: - resourceserver: - jwt: - issuer-uri: ${SSO_ISSUER_URL} - client: - provider: - keycloak: - issuer-uri: ${SSO_ISSUER_URL} - user-info-uri: ${SSO_BASE_URL}/realms/${SSO_REALM}/protocol/openid-connect/userinfo - # jwk-set-uri: ${SSO_BASE_URL}/realms/${SSO_REALM}/protocol/openid-connect/certs - user-name-attribute: user_name - registration: - keycloak: - provider: keycloak - client-id: ${SSO_ENGINE_CLIENT_ID} - client-secret: ${SSO_ENGINE_CLIENT_SECRET} - scope: email, profile, openid - springdoc: packagesToScan: io.miragon.miranum.platform pathsToMatch: /** @@ -117,25 +100,23 @@ springdoc: realm: ${SSO_REALM} appName: Engine csrf: - enabled: false + enabled: true writer-with-default-pretty-printer: true miranum: - # override default security settings security: - permittedUrls: - - "/error" - - "/actuator/**" - - "/engine-rest/deployment" - - "/api/deployment" - - "/swagger-ui/index.html" # allow access to swagger - - "/swagger-ui*/*swagger-initializer.js" # allow access to swagger - - "/swagger-ui*/**" # allow access to swagger - - "/v3/api-docs/*" # allow access to swagger - - "/v3/api-docs" # allow access to swagger - - "/camunda/**" # allow camunda webapps + server: + base-url: ${SSO_BASE_URL} + realm: ${SSO_REALM} + user-name-attribute: email + client: + clientId: ${SSO_ENGINE_CLIENT_ID} + clientSecret: ${SSO_ENGINE_CLIENT_SECRET} tasklist: # if you want to disable the tasklist, set this to false enabled: true # if you want a custom prefix for the tasklist custom fields, set this to your desired prefix customFieldsPrefix: "miranum_task_" +logging: + level: + root: debug diff --git a/platform/engine/task/pom.xml b/platform/engine/task/pom.xml index 54731b3de..19ada1116 100644 --- a/platform/engine/task/pom.xml +++ b/platform/engine/task/pom.xml @@ -52,7 +52,7 @@ io.miragon.miranum.platform spring-security-starter - provided + ${project.version} diff --git a/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/adapter/in/task/TaskInfoListener.java b/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/adapter/in/task/TaskInfoListener.java index 63e69a303..6a83f3291 100644 --- a/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/adapter/in/task/TaskInfoListener.java +++ b/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/adapter/in/task/TaskInfoListener.java @@ -16,7 +16,7 @@ public class TaskInfoListener { private final TaskInfoUseCase taskInfoInPort; @EventListener - public void taskInfoListeners(final DelegateTask delegateTask) throws Exception { + public void taskInfoListeners(final DelegateTask delegateTask) { switch (delegateTask.getEventName()) { case "create": log.debug("TaskInfo Listener: {}, Event: {}", delegateTask.getName(), delegateTask.getEventName()); diff --git a/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/application/service/UserTaskQueryImpl.java b/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/application/service/UserTaskQueryImpl.java index 83be9ca7a..09ab019fa 100644 --- a/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/application/service/UserTaskQueryImpl.java +++ b/platform/engine/task/src/main/java/io/miragon/miranum/platform/tasklist/application/service/UserTaskQueryImpl.java @@ -18,8 +18,8 @@ public class UserTaskQueryImpl implements UserTaskQuery { @Override - public List getTasksForUserGroup(final String group, final String user) { - return this.taskOutPort.getTasksForUserGroup(group, user); + public List getTasksForUserGroup(final String user, final String group) { + return this.taskOutPort.getTasksForUserGroup(user, group); } @Override diff --git a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/JwtAuthenticationConverter.java b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/JwtAuthenticationConverter.java index a98f31062..e92a0931d 100644 --- a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/JwtAuthenticationConverter.java +++ b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/JwtAuthenticationConverter.java @@ -24,12 +24,10 @@ public class JwtAuthenticationConverter implements Converter authorities = new ArrayList<>(); if (source.getClaims().containsKey((CLAIM_ROLES))) { authorities.addAll(asAuthorities(source.getClaims().get(CLAIM_ROLES))); } - return new JwtAuthenticationToken(source, authorities); } diff --git a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SpringSecurityProperties.java b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SpringSecurityProperties.java index 13da1000e..7f5951f7b 100644 --- a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SpringSecurityProperties.java +++ b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/SpringSecurityProperties.java @@ -3,20 +3,30 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.ArrayList; +import java.util.List; + @Data @ConfigurationProperties(prefix = "miranum.security") public class SpringSecurityProperties { - /** - * Name of the registration used in main security protection (as resource server). - */ - private String clientRegistration = "keycloak"; - private String[] permittedUrls = { - "/error", // allow the error page - "/actuator/info", // allow access to /actuator/info - "/actuator/health", // allow access to /actuator/health for OpenShift Health Check - "/actuator/metrics", // allow access to /actuator/metrics for Prometheus monitoring in OpenShift - "/engine-rest/deployment" - }; + private String clientRegistration; + private List permittedUrls = new ArrayList<>(); + private Server server = new Server(); + private Client client = new Client(); + + @Data + public static class Server { + private String baseUrl; + private String realm; + private String userNameAttribute; + } + + @Data + public static class Client { + private String enabled; + private String clientId; + private String clientSecret; + } } diff --git a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProvider.java b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProvider.java index cf7e1ae5f..e5e3f91c3 100644 --- a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProvider.java +++ b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProvider.java @@ -6,7 +6,6 @@ public interface UserAuthenticationProvider { - @NonNull String getLoggedInUser(); diff --git a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProviderImpl.java b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProviderImpl.java index a0410b68f..372dfb8d6 100644 --- a/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProviderImpl.java +++ b/platform/libs/spring-security/spring-security-core/src/main/java/io/miragon/miranum/platform/security/authentication/UserAuthenticationProviderImpl.java @@ -1,6 +1,5 @@ package io.miragon.miranum.platform.security.authentication; -import io.miragon.miranum.platform.security.SecurityConfiguration; import io.miragon.miranum.platform.security.SpringSecurityProperties; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @@ -20,34 +19,20 @@ * Extracts the username from the token. */ @Component -@Profile(SecurityConfiguration.SECURITY) +@Profile("!no-security") @RequiredArgsConstructor @Slf4j public class UserAuthenticationProviderImpl implements UserAuthenticationProvider { public static final String NAME_UNAUTHENTICATED_USER = "unauthenticated"; private final SpringSecurityProperties springSecurityProperties; - private final ClientRegistrationRepository clientRegistrationRepository; - private String userNameAttribute; - - @PostConstruct - public void getUsernameAttributeName() { - try { - userNameAttribute = clientRegistrationRepository.findByRegistrationId(springSecurityProperties.getClientRegistration()) - .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); - } catch (Exception e) { - userNameAttribute = "user_name"; - log.error("Error reading username attribute for configured client registration " - + springSecurityProperties.getClientRegistration() + ". Falling back to " + userNameAttribute, e); - } - } @Override @NonNull public String getLoggedInUser() { final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication.getPrincipal() instanceof Jwt jwt) { - return (String) jwt.getClaims().get(userNameAttribute); + return (String) jwt.getClaims().get(springSecurityProperties.getServer().getUserNameAttribute()); } return NAME_UNAUTHENTICATED_USER; } diff --git a/platform/libs/spring-security/spring-security-starter/src/main/resources/application-spring-security-starter.yaml b/platform/libs/spring-security/spring-security-starter/src/main/resources/application-spring-security-starter.yaml index 54e536466..244f22781 100644 --- a/platform/libs/spring-security/spring-security-starter/src/main/resources/application-spring-security-starter.yaml +++ b/platform/libs/spring-security/spring-security-starter/src/main/resources/application-spring-security-starter.yaml @@ -1,28 +1,39 @@ miranum: security: client-registration: keycloak + server: + base-url: + realm: + user-name-attribute: preferred_username + client: + enabled: true + clientId: + clientSecret: permitted-urls: - /error # allow the error page - /actuator/info # allow access to /actuator/info - - /actuator/health # allow access to /actuator/health for OpenShift Health Check - - /actuator/metrics # allow access to /actuator/metrics for Prometheus monitoring in OpenShift + - /actuator/health # allow access to /actuator/health + - /actuator/metrics # allow access to /actuator/metrics + spring: security: oauth2: resourceserver: jwt: - issuer-uri: ${SSO_ISSUER_URL} + issuer-uri: ${miranum.security.server.base-url}/realms/${miranum.security.server.realm} client: provider: keycloak: - issuer-uri: ${SSO_ISSUER_URL} - user-info-uri: ${SSO_BASE_URL}/realms/${SSO_REALM}/protocol/openid-connect/userinfo - jwk-set-uri: ${SSO_BASE_URL}/realms/${SSO_REALM}/protocol/openid-connect/certs - user-name-attribute: user_name + issuer-uri: ${miranum.security.server.base-url}/realms/${miranum.security.server.realm} + authorization-uri: ${spring.security.oauth2.client.provider.keycloak.issuer-uri}/protocol/openid-connect/auth + token-uri: ${spring.security.oauth2.client.provider.keycloak.issuer-uri}/protocol/openid-connect/token + user-info-uri: ${spring.security.oauth2.client.provider.keycloak.issuer-uri}/protocol/openid-connect/userinfo + jwk-set-uri: ${spring.security.oauth2.client.provider.keycloak.issuer-uri}/protocol/openid-connect/certs + user-name-attribute: ${miranum.security.server.user-name-attribute} registration: keycloak: provider: keycloak - client-id: ${SSO_ENGINE_CLIENT_ID} - client-secret: ${SSO_ENGINE_CLIENT_SECRET} + client-id: ${miranum.security.client.clientId} + client-secret: ${miranum.security.client.clientSecret} scope: email, profile, openid # needed for userInfo endpoint diff --git a/platform/pom.xml b/platform/pom.xml index f798e7f26..9efd4f383 100644 --- a/platform/pom.xml +++ b/platform/pom.xml @@ -99,11 +99,6 @@ miranum-unit-test ${project.version} - - io.miragon.miranum.platform - spring-security-starter - ${project.version} -