SecurityConfig.java
package access.security;
import access.manage.Manage;
import access.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.scheduling.annotation.EnableScheduling;
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.http.SessionCreationPolicy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
@EnableWebSecurity
@EnableScheduling
@Configuration
@EnableMethodSecurity
@EnableCaching
public class SecurityConfig {
public static final String API_TOKEN_HEADER = "X-API-TOKEN";
private final ClientRegistrationRepository clientRegistrationRepository;
private final Environment environment;
private final List<String> eduidIdpEntityIdentifiers;
private final String minimalStepupAcrLevel;
@Autowired
public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository,
Environment environment,
@Value("${eduid-idp-entity-id}") String eduidIdpEntityId,
@Value("${config.minimal_stepup_acr_level}") String minimalStepupAcrLevel) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.environment = environment;
this.eduidIdpEntityIdentifiers = Stream.of(eduidIdpEntityId.split(",")).map(entityId -> entityId.trim()).toList();
this.minimalStepupAcrLevel = minimalStepupAcrLevel;
}
@Configuration
@EnableConfigurationProperties({SuperAdmin.class})
public static class MvcConfig implements WebMvcConfigurer {
private final UserRepository userRepository;
private final SuperAdmin superAdmin;
@Autowired
public MvcConfig(UserRepository userRepository, SuperAdmin superAdmin) {
this.userRepository = userRepository;
this.superAdmin = superAdmin;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UserHandlerMethodArgumentResolver(userRepository, superAdmin));
}
}
@Bean
public CookieSerializer cookieSerializer(@Value("${server.servlet.session.cookie.secure}") boolean secure) {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setUseSecureCookie(secure);
return serializer;
}
@Bean
@Order(1)
SecurityFilterChain sessionSecurityFilterChain(HttpSecurity http,
UserRepository userRepository,
Manage manage,
@Value("${institution-admin.entitlement}") String entitlement,
@Value("${institution-admin.organization-guid-prefix}") String organizationGuidPrefix) throws Exception {
http
.csrf(csrfConfigurer -> csrfConfigurer
.ignoringRequestMatchers(
"/api/v1/test/login",
"/login/oauth2/code/oidcng",
"/api/v1/validations/**"))
.securityMatcher("/login/oauth2/**", "/oauth2/authorization/**", "/api/v1/**")
.authorizeHttpRequests(authorizeHttpRequestsConfigurer -> authorizeHttpRequestsConfigurer
.requestMatchers(
"/api/v1/csrf",
"/api/v1/disclaimer",
"/api/v1/users/config",
"/api/v1/users/logout",
"/api/v1/validations/**",
"/api/v1/test/login",
"/api/v1/public/**",
"/api/v1/manage/arp",
"/api/v1/stats/loginTimeFrame",
"/api/v1/monitoring",
"/api/v1/manage/allowed-attributes",
"/api/v1/manage/privacy",
"/ui/**",
"/internal/health",
"/internal/info")
.permitAll()
.anyRequest()
.authenticated()
)
.oauth2Login(oAuth2LoginConfigurer -> oAuth2LoginConfigurer
.authorizationEndpoint(authorization -> authorization
.authorizationRequestResolver(
authorizationRequestResolver(this.clientRegistrationRepository)
)
).userInfoEndpoint(userInfoEndpointConfigurer -> userInfoEndpointConfigurer.oidcUserService(
new CustomOidcUserService(userRepository, manage, entitlement, organizationGuidPrefix)))
)
//We need a reference to the securityContextRepository to update the authentication after an InstitutionAdmin accepts an invitation
.securityContext(securityContextConfigurer ->
securityContextConfigurer.securityContextRepository(this.securityContextRepository()));
if (environment.acceptsProfiles(Profiles.of("dev"))) {
//Thus avoiding an oauth2 login for local development
http.addFilterBefore(new LocalDevelopmentAuthenticationFilter(), AnonymousAuthenticationFilter.class);
}
return http.build();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
);
}
@Bean
@Order(2)
SecurityFilterChain basicAuthenticationSecurityFilterChain(HttpSecurity http,
@Value("${lifecycle.user}") String lifeCycleUser,
@Value("${lifecycle.password}") String lifeCyclePassword) throws Exception {
http.csrf(c -> c.disable())
.securityMatcher(
"/api/external/v1/deprovision/**",
"/internal/prometheus"
).sessionManagement(c -> c
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).authorizeHttpRequests(auth -> auth
.requestMatchers("/internal/prometheus").hasRole("ACTUATOR")
.requestMatchers("/api/external/v1/deprovision/**").hasRole("LIFECYCLE"))
.authorizeHttpRequests(c -> c
.anyRequest()
.authenticated()
)
.userDetailsService(new InMemoryUserDetailsManager(
new org.springframework.security.core.userdetails.User(
lifeCycleUser,
"{noop}".concat(lifeCyclePassword),
List.of(new SimpleGrantedAuthority("ROLE_LIFECYCLE"))
)
))
.httpBasic(Customizer.withDefaults());
return http.build();
}
private OAuth2AuthorizationRequestResolver authorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
authorizationRequestResolver.setAuthorizationRequestCustomizer(
new AuthorizationRequestCustomizer(eduidIdpEntityIdentifiers, minimalStepupAcrLevel));
return authorizationRequestResolver;
}
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver slr = new AcceptHeaderLocaleResolver();
slr.setDefaultLocale(Locale.ENGLISH);
return slr;
}
}