Skip to content

Commit

Permalink
Merge pull request #8 from it-at-m/jwt
Browse files Browse the repository at this point in the history
Jwt instead of opaque tokens
  • Loading branch information
maximilian-zollbrecht authored Jun 3, 2024
2 parents fb07a2d + 3e9f2ab commit f860362
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,8 +31,9 @@
@Slf4j
@CrossOrigin
@RestController
@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_kf-app-eai-access')")
@SecurityRequirement(name = "InternalLogin")
@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_internal-access')")
@SecurityRequirement(name = "InternalLogin", scopes = { SecurityConfiguration.SCOPE_LHM_EXTENDED,
SecurityConfiguration.SCOPE_OPENID })
@RequestMapping(path = "/internal/", produces = "application/json")
public class InternalApiController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,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;
Expand All @@ -33,7 +35,9 @@
@Slf4j
@CrossOrigin
@RestController
@SecurityRequirement(name = "ApiClient")
@PreAuthorize("@environment.acceptsProfiles('no-security') || hasAuthority('ROLE_api-access')")
@SecurityRequirement(name = "ApiClient", scopes = { SecurityConfiguration.SCOPE_LHM_EXTENDED,
SecurityConfiguration.SCOPE_ROLES })
@RequestMapping(path = "/kitaApp/v1", produces = "application/json")
public class KitaAppApiController {

Expand All @@ -47,7 +51,7 @@ public class KitaAppApiController {

@Autowired
private AuditService auditService;

@Autowired
private Validator validator;

Expand Down Expand Up @@ -78,7 +82,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,36 @@
*/
package de.muenchen.rbs.kitafindereai.config;

import org.springframework.beans.factory.annotation.Qualifier;
import java.util.List;

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.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;

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;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -39,73 +48,94 @@
@EnableMethodSecurity(prePostEnabled = true)
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)
@Profile("!no-security")
public SecurityFilterChain internalApiSecurityFilterChain(HttpSecurity http,
@Qualifier("internalTokenIntrospector") OpaqueTokenIntrospector introspector) throws Exception {
JwtDecoder decoder, JwtAuthenticationConverter authConverter) throws Exception {
http.securityMatcher("/internal/**")
.authorizeHttpRequests(requests -> requests.anyRequest().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.opaqueToken(config -> config.introspector(introspector)));
.jwt(jwt -> jwt
.decoder(decoder)
.jwtAuthenticationConverter(authConverter)));
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 {
JwtDecoder decoder, JwtAuthenticationConverter authConverter) throws Exception {
http.securityMatcher("/kitaApp/**")
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated())
.oauth2ResourceServer((oauth2) -> oauth2
.opaqueToken(config -> config.introspector(introspector)));
.jwt(jwt -> jwt
.decoder(decoder)
.jwtAuthenticationConverter(authConverter)));
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.
*/
@Bean
@Order(3)
@Profile("!no-security")
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http,
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
.opaqueToken(Customizer.withDefaults()));
.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() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtAuthoritiesConverter());
return jwtAuthenticationConverter;
}

@Bean
@Profile("!no-security")
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);
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")
Expand All @@ -121,8 +151,10 @@ 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, 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 {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,10 @@
"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",
"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"
},
{
"name": "app.security.token-url",
"type": "java.lang.String",
Expand All @@ -50,9 +35,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",
Expand Down
14 changes: 3 additions & 11 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,10 @@ 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
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
issuer-url: ${app.security.sso-base-url}/auth/realms/${app.security.realm}
client-id: kf-app-eai
passwordEncoder:
# Encryption properties. Overwrite in non-dev-environments!
encryptor:
Expand Down Expand Up @@ -48,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* @author m.zollbrecht
*/
@SpringBootTest
@ActiveProfiles("test")
@ActiveProfiles({ "test", "no-security" })
class PasswordEncoderTest {

@Autowired
Expand Down

0 comments on commit f860362

Please sign in to comment.