Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jwt instead of opaque tokens #8

Merged
merged 11 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading