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: