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}
-