From 51f59dacdd9fdfb3f4747de80404c9a1e3f7b9b4 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Wed, 15 May 2024 10:34:00 +0200
Subject: [PATCH 01/10] switch to jwt and validate aud claim

---
 .../api/KitaAppApiController.java             |  4 +-
 .../rbs/kitafindereai/api/model/Child.java    |  2 +-
 .../config/SecurityConfiguration.java         | 82 ++++++++++++-------
 ...itional-spring-configuration-metadata.json |  4 +-
 src/main/resources/application.yml            |  6 +-
 5 files changed, 59 insertions(+), 39 deletions(-)

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
index dfe9039..73d3c2f 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
@@ -47,7 +47,7 @@ public class KitaAppApiController {
 
     @Autowired
     private AuditService auditService;
-    
+
     @Autowired
     private Validator validator;
 
@@ -78,7 +78,7 @@ public ResponseEntity<Institute> getGroupsWithKidsByKibigwebid(
 
         KitafinderExport export = kitaFinderService.exportKitaData(kibigWebId);
         Institute institute = mapper.map(export, Institute.class);
-        
+
         Set<ConstraintViolation<Institute>> validationErrors = validator.validate(institute);
         if (validationErrors.size() > 0) {
             throw new ConstraintViolationException(validationErrors);
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/model/Child.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/model/Child.java
index 8db256b..dea0f49 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/model/Child.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/model/Child.java
@@ -23,7 +23,7 @@ public class Child   {
     @NotNull
     @Size(max = 25)
     @Schema(example = "Stark", description = "Dieses Feld wird bei Bewerbungen vom erfassenden System, also vom Kita-Planer 2 selbst, vergeben und zur eindeutigen Identifizierung der Bewerbung/des Vertrags genutzt.Es handelt sich NICHT um eine ID des physischen Kindes. Die KIND_ID_EXTERN ist eindeutig für das Tripel: Kind, Bewerbungsprozess, Kita. Pro Kita und Bewerbungsprozess (z.B. andere Altersgruppe) gibt es also für das gleiche physische Kind eine unterschiedliche externe ID.")
-    // Kitafinder-Column [KITA_ID_EXTERN]
+    // Kitafinder-Column [KIND_ID_EXTERN]
     private String childId;
     
     @NotNull
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
index 98e80ba..05179f0 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
@@ -4,20 +4,26 @@
  */
 package de.muenchen.rbs.kitafindereai.config;
 
+import java.util.List;
+
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.annotation.Order;
-import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
-import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
-import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimValidator;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtDecoders;
+import org.springframework.security.oauth2.jwt.JwtValidators;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.web.SecurityFilterChain;
 
 import de.muenchen.rbs.kitafindereai.api.InternalApiController;
@@ -39,54 +45,37 @@
 @EnableMethodSecurity(prePostEnabled = true)
 public class SecurityConfiguration {
 
+    private static final String AUD_CLAIM = "aud";
+
     /** Security for {@link InternalApiController} */
     @Bean
     @Order(1)
     @Profile("!no-security")
     public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
-            @Qualifier("internalTokenIntrospector") OpaqueTokenIntrospector introspector) throws Exception {
+            @Qualifier("internalDecoder") JwtDecoder decoder) throws Exception {
         http.securityMatcher("/internal/**")
                 .authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .opaqueToken(config -> config.introspector(introspector)));
+                        .jwt(jwt -> jwt.decoder(decoder)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
 
-    @Primary
-    @Profile("!no-security")
-    @Bean("internalTokenIntrospector")
-    public OpaqueTokenIntrospector internalTokenIntrospector(
-            @Value("${app.security.introspection-url}") String introspectionUri,
-            @Value("${app.security.internal.client-id}") String clientId,
-            @Value("${app.security.internal.client-secret}") String clientSecret) {
-        return new CustomAuthoritiesOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
-    }
-
     /** Security for {@link KitaAppApiController} */
     @Bean
     @Order(2)
     @Profile("!no-security")
     public SecurityFilterChain kitaAppApiSecurityFilterChain(HttpSecurity http,
-            @Qualifier("apiTokenIntrospector") OpaqueTokenIntrospector introspector) throws Exception {
+            @Qualifier("apiDecoder") JwtDecoder decoder) throws Exception {
         http.securityMatcher("/kitaApp/**")
                 .authorizeHttpRequests((authorize) -> authorize
                         .anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .opaqueToken(config -> config.introspector(introspector)));
+                        .jwt(jwt -> jwt.decoder(decoder)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
 
-    @Bean("apiTokenIntrospector")
-    @Profile("!no-security")
-    public OpaqueTokenIntrospector apiTokenIntrospector(
-            @Value("${app.security.introspection-url}") String introspectionUri,
-            @Value("${app.security.api.client-id}") String clientId,
-            @Value("${app.security.api.client-secret}") String clientSecret) {
-        return new SpringOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
-    }
-
     /**
      * Security for remaining endpoints.
      * Excluding Swagger UI and spring actuators.
@@ -94,18 +83,53 @@ public OpaqueTokenIntrospector apiTokenIntrospector(
     @Bean
     @Order(3)
     @Profile("!no-security")
-    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
+    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
+            @Qualifier("internalDecoder") JwtDecoder decoder) throws Exception {
         http.authorizeHttpRequests((authorize) -> authorize
                 .requestMatchers("/actuator/info", "/actuator/health/**",
                         "/swagger-ui/**", "/v3/api-docs/**")
                 .permitAll()
                 .anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .opaqueToken(Customizer.withDefaults()));
+                        .jwt(jwt -> jwt.decoder(decoder)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
 
+    @Bean("apiDecoder")
+    @Profile("!no-security")
+    JwtDecoder apiJwtDecoder(@Value("${app.security.issuer-url}") String issuerUri,
+            @Value("${app.security.api.client-id}") String requiredAudience) {
+        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);
+
+        OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator(requiredAudience);
+        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
+        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
+
+        jwtDecoder.setJwtValidator(withAudience);
+
+        return jwtDecoder;
+    }
+
+    @Bean("internalDecoder")
+    @Profile("!no-security")
+    JwtDecoder internalJwtDecoder(@Value("${app.security.issuer-url}") String issuerUri,
+            @Value("${app.security.internal.client-id}") String requiredAudience) {
+        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);
+
+        OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator(requiredAudience);
+        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
+        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
+
+        jwtDecoder.setJwtValidator(withAudience);
+
+        return jwtDecoder;
+    }
+
+    OAuth2TokenValidator<Jwt> audienceValidator(String requiredAudience) {
+        return new JwtClaimValidator<List<String>>(AUD_CLAIM, aud -> aud.contains(requiredAudience));
+    }
+
     /** Security-config for profile 'no-security' */
     @Bean
     @Profile("no-security")
diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 4b7518f..44ac403 100644
--- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -50,9 +50,9 @@
     "description": "URL zur Authentifizierung am OAuth-Server."
   },
   {
-    "name": "app.security.introspection-url",
+    "name": "app.security.issuer-url",
     "type": "java.lang.String",
-    "description": "URL für die Introspection der Token."
+    "description": "Issuer-URL des OAuth-Servers."
   },
   {
     "name": "app.kitafinderAdapter.base-url",
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index b05690a..9e0f901 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -7,17 +7,13 @@ app:
   security:
     sso-base-url: https://ssodev.muenchen.de
     realm: A23
-    introspection-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/token/introspect
     authorization-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/auth
     token-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/token
+    issuer-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}
     api:
       client-id: instikom-kf-schnittstelle
-      # app.security.client-secret: required for oauth authentication to work.
-      # Add in programm arguments or environment
     internal:
       client-id: kf-app-eai-internal
-      # app.security.client-secret: required for oauth authentication to work.
-      # Add in programm arguments or environment
   passwordEncoder:
     # Encryption properties. Overwrite in non-dev-environments!
     encryptor:

From 56c9eb294ec0fe0e77cc43db0412d1d5adb7c6f0 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Wed, 15 May 2024 11:26:46 +0200
Subject: [PATCH 02/10] use no-security

---
 .../java/de/muenchen/rbs/kitafindereai/PasswordEncoderTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/de/muenchen/rbs/kitafindereai/PasswordEncoderTest.java b/src/test/java/de/muenchen/rbs/kitafindereai/PasswordEncoderTest.java
index 30862fa..62746d9 100644
--- a/src/test/java/de/muenchen/rbs/kitafindereai/PasswordEncoderTest.java
+++ b/src/test/java/de/muenchen/rbs/kitafindereai/PasswordEncoderTest.java
@@ -16,7 +16,7 @@
  * @author m.zollbrecht
  */
 @SpringBootTest
-@ActiveProfiles("test")
+@ActiveProfiles({ "test", "no-security" })
 class PasswordEncoderTest {
 
     @Autowired

From eb5854a374b56afd609253d1e4fb1d2692209d05 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Wed, 15 May 2024 17:17:33 +0200
Subject: [PATCH 03/10] Update clients

---
 .../api/InternalApiController.java            |  2 +-
 .../api/KitaAppApiController.java             |  2 +
 .../config/SecurityConfiguration.java         | 37 +++++++++----------
 ...itional-spring-configuration-metadata.json | 16 ++------
 src/main/resources/application.yml            |  6 +--
 5 files changed, 26 insertions(+), 37 deletions(-)

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
index 1113da5..6c159f7 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
@@ -30,7 +30,7 @@
 @Slf4j
 @CrossOrigin
 @RestController
-@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_kf-app-eai-access')")
+@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_internal-access')")
 @SecurityRequirement(name = "InternalLogin")
 @RequestMapping(path = "/internal/", produces = "application/json")
 public class InternalApiController {
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
index 73d3c2f..97263ae 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
@@ -6,6 +6,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -33,6 +34,7 @@
 @Slf4j
 @CrossOrigin
 @RestController
+@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_api-access')")
 @SecurityRequirement(name = "ApiClient")
 @RequestMapping(path = "/kitaApp/v1", produces = "application/json")
 public class KitaAppApiController {
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
index 05179f0..e010651 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
@@ -6,7 +6,6 @@
 
 import java.util.List;
 
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -24,6 +23,8 @@
 import org.springframework.security.oauth2.jwt.JwtDecoders;
 import org.springframework.security.oauth2.jwt.JwtValidators;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
 import org.springframework.security.web.SecurityFilterChain;
 
 import de.muenchen.rbs.kitafindereai.api.InternalApiController;
@@ -52,7 +53,7 @@ public class SecurityConfiguration {
     @Order(1)
     @Profile("!no-security")
     public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
-            @Qualifier("internalDecoder") JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder) throws Exception {
         http.securityMatcher("/internal/**")
                 .authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
@@ -66,7 +67,7 @@ public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
     @Order(2)
     @Profile("!no-security")
     public SecurityFilterChain kitaAppApiSecurityFilterChain(HttpSecurity http,
-            @Qualifier("apiDecoder") JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder) throws Exception {
         http.securityMatcher("/kitaApp/**")
                 .authorizeHttpRequests((authorize) -> authorize
                         .anyRequest().authenticated())
@@ -84,7 +85,7 @@ public SecurityFilterChain kitaAppApiSecurityFilterChain(HttpSecurity http,
     @Order(3)
     @Profile("!no-security")
     public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
-            @Qualifier("internalDecoder") JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder) throws Exception {
         http.authorizeHttpRequests((authorize) -> authorize
                 .requestMatchers("/actuator/info", "/actuator/health/**",
                         "/swagger-ui/**", "/v3/api-docs/**")
@@ -96,25 +97,23 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
         return http.build();
     }
 
-    @Bean("apiDecoder")
+    @Bean
     @Profile("!no-security")
-    JwtDecoder apiJwtDecoder(@Value("${app.security.issuer-url}") String issuerUri,
-            @Value("${app.security.api.client-id}") String requiredAudience) {
-        NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);
-
-        OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator(requiredAudience);
-        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
-        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
-
-        jwtDecoder.setJwtValidator(withAudience);
-
-        return jwtDecoder;
+    public JwtAuthenticationConverter jwtAuthenticationConverter(
+            @Value("app.security.roleClaimName") String roleClaimName) {
+        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
+        grantedAuthoritiesConverter.setAuthoritiesClaimName(roleClaimName);
+        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
+
+        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
+        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
+        return jwtAuthenticationConverter;
     }
 
-    @Bean("internalDecoder")
+    @Bean
     @Profile("!no-security")
-    JwtDecoder internalJwtDecoder(@Value("${app.security.issuer-url}") String issuerUri,
-            @Value("${app.security.internal.client-id}") String requiredAudience) {
+    JwtDecoder jwtDecoder(@Value("${app.security.issuer-url}") String issuerUri,
+            @Value("${app.security.client-id}") String requiredAudience) {
         NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);
 
         OAuth2TokenValidator<Jwt> audienceValidator = audienceValidator(requiredAudience);
diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 44ac403..74d4f35 100644
--- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -20,24 +20,14 @@
     "description": "SSO-Realm."
   },
   {
-    "name": "app.security.api.client-id",
+    "name": "app.security.client-id",
     "type": "java.lang.String",
     "description": "client-id für die Authentifizierung"
   },
   {
-    "name": "app.security.api.client-secret",
+    "name": "app.security.roleClaimName",
     "type": "java.lang.String",
-    "description": "client-secret für die Authentifizierung"
-  },
-  {
-    "name": "app.security.internal.client-id",
-    "type": "java.lang.String",
-    "description": "client-id für die Authentifizierung"
-  },
-  {
-    "name": "app.security.internal.client-secret",
-    "type": "java.lang.String",
-    "description": "client-secret für die Authentifizierung"
+    "description": "Name des claims in dem die Rollen stehen."
   },
   {
     "name": "app.security.token-url",
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9e0f901..1d983c5 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -10,10 +10,8 @@ app:
     authorization-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/auth
     token-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/token
     issuer-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}
-    api:
-      client-id: instikom-kf-schnittstelle
-    internal:
-      client-id: kf-app-eai-internal
+    client-id: kf-app-eai
+    roleClaimName: kf-eai-roles
   passwordEncoder:
     # Encryption properties. Overwrite in non-dev-environments!
     encryptor:

From d657565b60cf55d992e829f3a2e4450224e98e72 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Thu, 16 May 2024 15:18:31 +0200
Subject: [PATCH 04/10] Update role conversion

---
 .../config/JwtAuthoritiesConverter.java       | 43 +++++++++++++++++++
 .../config/SecurityConfiguration.java         | 28 ++++++------
 src/main/resources/application.yml            |  4 +-
 3 files changed, 58 insertions(+), 17 deletions(-)
 create mode 100644 src/main/java/de/muenchen/rbs/kitafindereai/config/JwtAuthoritiesConverter.java

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/JwtAuthoritiesConverter.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/JwtAuthoritiesConverter.java
new file mode 100644
index 0000000..cefd5b6
--- /dev/null
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/JwtAuthoritiesConverter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c): it@M - Dienstleister für Informations- und Telekommunikationstechnik
+ * der Landeshauptstadt München, 2024
+ */
+package de.muenchen.rbs.kitafindereai.config;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+/**
+ * 
+ */
+public class JwtAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Collection<GrantedAuthority> convert(Jwt jwt) {
+        // lade Rollen aus folgender Struktur:
+        // "resource_access": {
+        //   "kf-app-eai": {
+        //     "roles": [
+        //       "lhm-ab-kf-app-eai-internal-access",
+        //       "internal-access"
+        //     ]
+        //   }
+        // }
+        final Map<String, Object> resourceAccess = (Map<String, Object>) jwt.getClaims().getOrDefault("resource_access",
+                Map.of());
+        final Map<String, Object> kfAppEai = (Map<String, Object>) resourceAccess.getOrDefault("kf-app-eai", Map.of());
+        final Collection<String> roles = (Collection<String>) kfAppEai.getOrDefault("roles", List.of());
+
+        return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+}
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
index e010651..5218cdd 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
@@ -24,7 +24,6 @@
 import org.springframework.security.oauth2.jwt.JwtValidators;
 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
-import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
 import org.springframework.security.web.SecurityFilterChain;
 
 import de.muenchen.rbs.kitafindereai.api.InternalApiController;
@@ -53,11 +52,13 @@ public class SecurityConfiguration {
     @Order(1)
     @Profile("!no-security")
     public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
-            JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder, JwtAuthenticationConverter authConverter) throws Exception {
         http.securityMatcher("/internal/**")
                 .authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .jwt(jwt -> jwt.decoder(decoder)));
+                        .jwt(jwt -> jwt
+                                .decoder(decoder)
+                                .jwtAuthenticationConverter(authConverter)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
@@ -67,12 +68,14 @@ public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
     @Order(2)
     @Profile("!no-security")
     public SecurityFilterChain kitaAppApiSecurityFilterChain(HttpSecurity http,
-            JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder, JwtAuthenticationConverter authConverter) throws Exception {
         http.securityMatcher("/kitaApp/**")
                 .authorizeHttpRequests((authorize) -> authorize
                         .anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .jwt(jwt -> jwt.decoder(decoder)));
+                        .jwt(jwt -> jwt
+                                .decoder(decoder)
+                                .jwtAuthenticationConverter(authConverter)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
@@ -85,28 +88,25 @@ public SecurityFilterChain kitaAppApiSecurityFilterChain(HttpSecurity http,
     @Order(3)
     @Profile("!no-security")
     public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
-            JwtDecoder decoder) throws Exception {
+            JwtDecoder decoder, JwtAuthenticationConverter authConverter) throws Exception {
         http.authorizeHttpRequests((authorize) -> authorize
                 .requestMatchers("/actuator/info", "/actuator/health/**",
                         "/swagger-ui/**", "/v3/api-docs/**")
                 .permitAll()
                 .anyRequest().authenticated())
                 .oauth2ResourceServer((oauth2) -> oauth2
-                        .jwt(jwt -> jwt.decoder(decoder)));
+                        .jwt(jwt -> jwt
+                                .decoder(decoder)
+                                .jwtAuthenticationConverter(authConverter)));
         http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable());
         return http.build();
     }
 
     @Bean
     @Profile("!no-security")
-    public JwtAuthenticationConverter jwtAuthenticationConverter(
-            @Value("app.security.roleClaimName") String roleClaimName) {
-        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
-        grantedAuthoritiesConverter.setAuthoritiesClaimName(roleClaimName);
-        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
-
+    public JwtAuthenticationConverter jwtAuthenticationConverter() {
         JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
-        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
+        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtAuthoritiesConverter());
         return jwtAuthenticationConverter;
     }
 
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 1d983c5..0f3b790 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -11,7 +11,6 @@ app:
     token-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}/protocol/openid-connect/token
     issuer-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}
     client-id: kf-app-eai
-    roleClaimName: kf-eai-roles
   passwordEncoder:
     # Encryption properties. Overwrite in non-dev-environments!
     encryptor:
@@ -42,11 +41,10 @@ springdoc:
   swagger-ui:
     path: /swagger-ui.html
     oauth:
-      clientId: ${app.security.api.client-id}
       realm: ${app.security.realm}
       appName: ${spring.application.name}
       scopes:
-      - email
+      - LHM_Extended
   default-produces-media-type: application/json
   
 server:

From cbbaf916fa356dd2e85d9e967f73bfe6237721e6 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Thu, 16 May 2024 17:10:46 +0200
Subject: [PATCH 05/10] remove unused config property

---
 .../META-INF/additional-spring-configuration-metadata.json   | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 74d4f35..ca3a938 100644
--- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -24,11 +24,6 @@
     "type": "java.lang.String",
     "description": "client-id für die Authentifizierung"
   },
-  {
-    "name": "app.security.roleClaimName",
-    "type": "java.lang.String",
-    "description": "Name des claims in dem die Rollen stehen."
-  },
   {
     "name": "app.security.token-url",
     "type": "java.lang.String",

From f03e54a37d348a73e317bc728200d378eb607202 Mon Sep 17 00:00:00 2001
From: "matthias.karl" <matthias.karl@muenchen.de>
Date: Fri, 17 May 2024 15:30:10 +0200
Subject: [PATCH 06/10] added required scopes to swagger-ui

---
 .../kitafindereai/api/InternalApiController.java    |  4 +++-
 .../rbs/kitafindereai/api/KitaAppApiController.java |  4 +++-
 .../kitafindereai/config/SecurityConfiguration.java | 13 +++++++++++--
 src/main/resources/application.yml                  |  2 --
 4 files changed, 17 insertions(+), 6 deletions(-)

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
index 6c159f7..6d26324 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
@@ -20,6 +20,7 @@
 
 import de.muenchen.rbs.kitafindereai.adapter.kitaplaner.data.KitafinderKitaKonfigData;
 import de.muenchen.rbs.kitafindereai.adapter.kitaplaner.data.KitafinderKitaKonfigDataRepository;
+import de.muenchen.rbs.kitafindereai.config.SecurityConfiguration;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -31,7 +32,8 @@
 @CrossOrigin
 @RestController
 @PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_internal-access')")
-@SecurityRequirement(name = "InternalLogin")
+@SecurityRequirement(name = "InternalLogin", scopes = { SecurityConfiguration.SCOPE_LHM_EXTENDED,
+        SecurityConfiguration.SCOPE_ROLES, SecurityConfiguration.SCOPE_OPENID })
 @RequestMapping(path = "/internal/", produces = "application/json")
 public class InternalApiController {
 
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
index 97263ae..8b68ac0 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/KitaAppApiController.java
@@ -18,6 +18,7 @@
 import de.muenchen.rbs.kitafindereai.api.model.Institute;
 import de.muenchen.rbs.kitafindereai.audit.AuditService;
 import de.muenchen.rbs.kitafindereai.config.KitaAppApiErrorHandlingControllerAdvice.ErrorResponse;
+import de.muenchen.rbs.kitafindereai.config.SecurityConfiguration;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -35,7 +36,8 @@
 @CrossOrigin
 @RestController
 @PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_api-access')")
-@SecurityRequirement(name = "ApiClient")
+@SecurityRequirement(name = "ApiClient", scopes = { SecurityConfiguration.SCOPE_LHM_EXTENDED,
+        SecurityConfiguration.SCOPE_ROLES })
 @RequestMapping(path = "/kitaApp/v1", produces = "application/json")
 public class KitaAppApiController {
 
diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
index 5218cdd..0bca12d 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
@@ -31,6 +31,7 @@
 import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
 import io.swagger.v3.oas.annotations.security.OAuthFlow;
 import io.swagger.v3.oas.annotations.security.OAuthFlows;
+import io.swagger.v3.oas.annotations.security.OAuthScope;
 import io.swagger.v3.oas.annotations.security.SecurityScheme;
 import lombok.extern.slf4j.Slf4j;
 
@@ -47,6 +48,10 @@ public class SecurityConfiguration {
 
     private static final String AUD_CLAIM = "aud";
 
+    public static final String SCOPE_LHM_EXTENDED = "LHM_Extended";
+    public static final String SCOPE_ROLES = "roles";
+    public static final String SCOPE_OPENID = "openid";
+
     /** Security for {@link InternalApiController} */
     @Bean
     @Order(1)
@@ -144,8 +149,12 @@ public SecurityFilterChain noSecurityFilterChain(HttpSecurity http)
     /** Swagger-API config for security */
     @Configuration
     @Profile("!no-security")
-    @SecurityScheme(name = "ApiClient", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(clientCredentials = @OAuthFlow(tokenUrl = "${app.security.token-url}")))
-    @SecurityScheme(name = "InternalLogin", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(authorizationCode = @OAuthFlow(tokenUrl = "${app.security.token-url}", authorizationUrl = "${app.security.authorization-url}", refreshUrl = "${app.security.token-url}")))
+    @SecurityScheme(name = "ApiClient", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(clientCredentials = @OAuthFlow(tokenUrl = "${app.security.token-url}", scopes = {
+            @OAuthScope(name = SCOPE_LHM_EXTENDED), @OAuthScope(name = SCOPE_ROLES) })))
+    @SecurityScheme(name = "InternalLogin", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(authorizationCode = @OAuthFlow(tokenUrl = "${app.security.token-url}", authorizationUrl = "${app.security.authorization-url}", refreshUrl = "${app.security.token-url}", scopes = {
+            @OAuthScope(name = SCOPE_LHM_EXTENDED), @OAuthScope(name = SCOPE_ROLES),
+            @OAuthScope(name = SCOPE_OPENID) })))
+
     public class SpringdocConfig {
     }
 
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 0f3b790..48e4156 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -43,8 +43,6 @@ springdoc:
     oauth:
       realm: ${app.security.realm}
       appName: ${spring.application.name}
-      scopes:
-      - LHM_Extended
   default-produces-media-type: application/json
   
 server:

From 369d4b29604148f7b197c1dd449d603eb09d02fd Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Tue, 21 May 2024 11:58:19 +0200
Subject: [PATCH 07/10] use id_token in internal security

---
 .../rbs/kitafindereai/config/SecurityConfiguration.java   | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
index 0bca12d..a1348b0 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/config/SecurityConfiguration.java
@@ -29,6 +29,8 @@
 import de.muenchen.rbs.kitafindereai.api.InternalApiController;
 import de.muenchen.rbs.kitafindereai.api.KitaAppApiController;
 import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
 import io.swagger.v3.oas.annotations.security.OAuthFlow;
 import io.swagger.v3.oas.annotations.security.OAuthFlows;
 import io.swagger.v3.oas.annotations.security.OAuthScope;
@@ -151,10 +153,8 @@ public SecurityFilterChain noSecurityFilterChain(HttpSecurity http)
     @Profile("!no-security")
     @SecurityScheme(name = "ApiClient", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(clientCredentials = @OAuthFlow(tokenUrl = "${app.security.token-url}", scopes = {
             @OAuthScope(name = SCOPE_LHM_EXTENDED), @OAuthScope(name = SCOPE_ROLES) })))
-    @SecurityScheme(name = "InternalLogin", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(authorizationCode = @OAuthFlow(tokenUrl = "${app.security.token-url}", authorizationUrl = "${app.security.authorization-url}", refreshUrl = "${app.security.token-url}", scopes = {
-            @OAuthScope(name = SCOPE_LHM_EXTENDED), @OAuthScope(name = SCOPE_ROLES),
-            @OAuthScope(name = SCOPE_OPENID) })))
-
+    @SecurityScheme(name = "InternalLogin", type = SecuritySchemeType.OAUTH2, extensions = @Extension(properties = @ExtensionProperty(name = "tokenName", value = "id_token")), flows = @OAuthFlows(authorizationCode = @OAuthFlow(tokenUrl = "${app.security.token-url}", authorizationUrl = "${app.security.authorization-url}", refreshUrl = "${app.security.token-url}", scopes = {
+            @OAuthScope(name = SCOPE_LHM_EXTENDED), @OAuthScope(name = SCOPE_OPENID) })))
     public class SpringdocConfig {
     }
 

From 6ac16e3416fcfa7f462d9554135ac099f38b34c8 Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Tue, 21 May 2024 12:03:17 +0200
Subject: [PATCH 08/10] pre-select scopes

---
 src/main/resources/application.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 48e4156..c8a34cc 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -43,6 +43,10 @@ springdoc:
     oauth:
       realm: ${app.security.realm}
       appName: ${spring.application.name}
+      scopes:
+      - LHM_Extended
+      - roles
+      - openid
   default-produces-media-type: application/json
   
 server:

From d04d010c5d982231b638516f87e1238acdfe538e Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Tue, 21 May 2024 12:10:09 +0200
Subject: [PATCH 09/10] remove unused scope

---
 .../muenchen/rbs/kitafindereai/api/InternalApiController.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
index 6d26324..fecbff7 100644
--- a/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
+++ b/src/main/java/de/muenchen/rbs/kitafindereai/api/InternalApiController.java
@@ -33,7 +33,7 @@
 @RestController
 @PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_internal-access')")
 @SecurityRequirement(name = "InternalLogin", scopes = { SecurityConfiguration.SCOPE_LHM_EXTENDED,
-        SecurityConfiguration.SCOPE_ROLES, SecurityConfiguration.SCOPE_OPENID })
+        SecurityConfiguration.SCOPE_OPENID })
 @RequestMapping(path = "/internal/", produces = "application/json")
 public class InternalApiController {
 

From aad0ddb28529aa7dea1106da52fb0093fd7b517e Mon Sep 17 00:00:00 2001
From: Max Zollbrecht <m.zollbrecht@muenchen.de>
Date: Tue, 21 May 2024 12:17:42 +0200
Subject: [PATCH 10/10] remove non-general scopes

---
 src/main/resources/application.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index c8a34cc..0f3b790 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -45,8 +45,6 @@ springdoc:
       appName: ${spring.application.name}
       scopes:
       - LHM_Extended
-      - roles
-      - openid
   default-produces-media-type: application/json
   
 server: