SecurityConfig.java
package pk.lucidxpo.ynami.spring.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import pk.lucidxpo.ynami.spring.aspect.FeatureAssociation;
import pk.lucidxpo.ynami.spring.features.FeatureManagerWrappable;
import pk.lucidxpo.ynami.utils.ProfileManager;
import static org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
import static pk.lucidxpo.ynami.spring.features.FeatureToggles.WEB_SECURITY;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String LOGIN_PAGE_URL = "/login.html";
static final String LOGIN_PROCESSING_URL = "/perform_login";
public static final String LOGIN_FAILURE_URL = LOGIN_PAGE_URL + "?error=true";
static final String LOGOUT_URL = "/perform_logout";
static final String LOGOUT_SUCCESS_URL = LOGIN_PAGE_URL + "?logout";
private static final AntPathRequestMatcher[] ENDPOINTS_WHITELIST = {
antMatcher("/css/**"),
antMatcher("/js/**"),
antMatcher("/img/**"),
antMatcher("/webjars/**"),
antMatcher("/favicon.ico") // 403 not working
};
/**
* Setting content security policy header to fix "Content Security Policy (CSP) Header Not Set" reported by ZAP.
* <p>
* The header value can be picked from the
* <a href="https://owasp.org/www-project-secure-headers/ci/headers_add.json"> HTTP response security headers to add</a>
*/
private static final String CONTENT_POLICY_DIRECTIVES = "default-src 'self'"
+ "; form-action 'self'"
+ "; object-src 'none'"
+ "; frame-ancestors 'none'"
+ "; upgrade-insecure-requests"
+ "; block-all-mixed-content"
+ "; report-uri /report"
+ "; report-to csp-violation-report";
private final String h2ConsolePattern;
private final ProfileManager profileManager;
private final FeatureManagerWrappable featureManager;
@Autowired
public SecurityConfig(@Value("${spring.h2.console.path:/h2-console}") final String h2ConsolePath,
final ProfileManager profileManager,
final FeatureManagerWrappable featureManager) {
this.profileManager = profileManager;
this.featureManager = featureManager;
this.h2ConsolePattern = h2ConsolePath.endsWith("/") ? h2ConsolePath + "**" : h2ConsolePath + "/**";
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
// TODO: do I need @FeatureAssociation(value = WEB_SECURITY) it here???
public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
// TODO: might need this for a security fix mentioned by the ZAP/security tests but bdd/selenium tests fail with
// this, need a proper solution.
// http
// .headers(headers -> headers
// .contentSecurityPolicy(contentSecurityPolicy -> // TODO: add tests for contentSecurityPolicy header
// contentSecurityPolicy.policyDirectives(CONTENT_POLICY_DIRECTIVES)
// )
// );
// TODO: does this need to be inside any other condition?? Cover it through integration tests.
if (profileManager.isH2Active()) {
setupH2ConsoleSecurity(http);
}
if (!featureManager.isActive(WEB_SECURITY)) {
return configureInsecureAccess(http);
}
return http
.authorizeHttpRequests((auth) -> auth.requestMatchers(ENDPOINTS_WHITELIST).permitAll())
.authorizeHttpRequests((auth) -> auth.anyRequest().authenticated())
// One reason to override most of the defaults in Spring Security is to hide that the application
// is secured with Spring Security. We also want to minimize the information a potential attacker
// knows about the application.
.formLogin(this::configureFormLogin)
.logout(this::configureFormLogout)
.build();
}
@Bean
@FeatureAssociation(value = WEB_SECURITY)
public AuthenticationManager authenticationManager(
final AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// TODO: do I need @FeatureAssociation(value = WEB_SECURITY) it here???
public void setupH2ConsoleSecurity(final HttpSecurity http) throws Exception {
final RequestMatcher h2PathMatcher = new AntPathRequestMatcher(h2ConsolePattern);
final XFrameOptionsHeaderWriter sameOriginXFrameHeaderWriter = new XFrameOptionsHeaderWriter(SAMEORIGIN);
// DelegatingRequestMatcherHeaderWriter is used to apply the XFrame header only to the h2 path. On the other
// hand, if we had used .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)),
// then it'd have applied the XFrame header to all the paths/urls.
final DelegatingRequestMatcherHeaderWriter headerWriter =
new DelegatingRequestMatcherHeaderWriter(h2PathMatcher, sameOriginXFrameHeaderWriter);
http
.authorizeHttpRequests(auth -> auth.requestMatchers(h2PathMatcher).permitAll())
.headers(headers -> headers.addHeaderWriter(headerWriter))
;
}
private DefaultSecurityFilterChain configureInsecureAccess(final HttpSecurity http) throws Exception {
return http
// Spring's recommendation is to use CSRF protection for any request that could be processed by a
// browser by normal users. If you are only creating a service that is used by non-browser clients,
// you will likely want to disable CSRF protection. If our stateless API uses token-based
// authentication, such as JWT, we don't need CSRF protection, and we must disable.
// However, if our stateless API uses a session cookie authentication, we need to enable CSRF protection
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
.build();
}
private void configureFormLogin(final FormLoginConfigurer<HttpSecurity> formLoginConfigurer) {
formLoginConfigurer
.loginPage(LOGIN_PAGE_URL)
// By overriding this default URL, we’re concealing that the application is actually secured
// with Spring Security. This information should not be available externally.
.loginProcessingUrl(LOGIN_PROCESSING_URL)
.permitAll()
.failureUrl(LOGIN_FAILURE_URL);
}
private void configureFormLogout(LogoutConfigurer<HttpSecurity> logoutConfigurer) {
logoutConfigurer
.logoutUrl(LOGOUT_URL)
.logoutSuccessUrl(LOGOUT_SUCCESS_URL);
}
// @Bean
// @FeatureAssociation(value = WEB_SECURITY)
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) -> web.ignoring().requestMatchers(
// antMatcher("/css/**"),
// antMatcher("/js/**"),
// antMatcher("/img/**"),
// antMatcher("/webjars/**")
// );
// }
}