View Javadoc
1   package pk.lucidxpo.ynami.spring.security;
2   
3   import org.springframework.beans.factory.annotation.Autowired;
4   import org.springframework.beans.factory.annotation.Value;
5   import org.springframework.context.annotation.Bean;
6   import org.springframework.context.annotation.Configuration;
7   import org.springframework.security.authentication.AuthenticationManager;
8   import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
9   import org.springframework.security.config.annotation.web.builders.HttpSecurity;
10  import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
11  import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
12  import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
13  import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
14  import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
15  import org.springframework.security.web.DefaultSecurityFilterChain;
16  import org.springframework.security.web.SecurityFilterChain;
17  import org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter;
18  import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
19  import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
20  import org.springframework.security.web.util.matcher.RequestMatcher;
21  import pk.lucidxpo.ynami.spring.aspect.FeatureAssociation;
22  import pk.lucidxpo.ynami.spring.features.FeatureManagerWrappable;
23  import pk.lucidxpo.ynami.utils.ProfileManager;
24  
25  import static org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN;
26  import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
27  import static pk.lucidxpo.ynami.spring.features.FeatureToggles.WEB_SECURITY;
28  
29  @Configuration
30  @EnableWebSecurity
31  public class SecurityConfig {
32      public static final String LOGIN_PAGE_URL = "/login.html";
33      static final String LOGIN_PROCESSING_URL = "/perform_login";
34      public static final String LOGIN_FAILURE_URL = LOGIN_PAGE_URL + "?error=true";
35  
36      static final String LOGOUT_URL = "/perform_logout";
37      static final String LOGOUT_SUCCESS_URL = LOGIN_PAGE_URL + "?logout";
38  
39      private static final AntPathRequestMatcher[] ENDPOINTS_WHITELIST = {
40              antMatcher("/css/**"),
41              antMatcher("/js/**"),
42              antMatcher("/img/**"),
43              antMatcher("/webjars/**"),
44              antMatcher("/favicon.ico") // 403 not working
45      };
46  
47      /**
48       * Setting content security policy header to fix "Content Security Policy (CSP) Header Not Set" reported by ZAP.
49       * <p>
50       * The header value can be picked from the
51       * <a href="https://owasp.org/www-project-secure-headers/ci/headers_add.json"> HTTP response security headers to add</a>
52       */
53      private static final String CONTENT_POLICY_DIRECTIVES = "default-src 'self'"
54              + "; form-action 'self'"
55              + "; object-src 'none'"
56              + "; frame-ancestors 'none'"
57              + "; upgrade-insecure-requests"
58              + "; block-all-mixed-content"
59              + "; report-uri /report"
60              + "; report-to csp-violation-report";
61  
62      private final String h2ConsolePattern;
63      private final ProfileManager profileManager;
64      private final FeatureManagerWrappable featureManager;
65  
66      @Autowired
67      public SecurityConfig(@Value("${spring.h2.console.path:/h2-console}") final String h2ConsolePath,
68                            final ProfileManager profileManager,
69                            final FeatureManagerWrappable featureManager) {
70          this.profileManager = profileManager;
71          this.featureManager = featureManager;
72          this.h2ConsolePattern = h2ConsolePath.endsWith("/") ? h2ConsolePath + "**" : h2ConsolePath + "/**";
73      }
74  
75      @Bean
76      public BCryptPasswordEncoder passwordEncoder() {
77          return new BCryptPasswordEncoder();
78      }
79  
80      @Bean
81      // TODO: do I need @FeatureAssociation(value = WEB_SECURITY) it here???
82      public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception {
83  //        TODO: might need this for a security fix mentioned by the ZAP/security tests but bdd/selenium tests fail with
84  //        this, need a proper solution.
85  //        http
86  //                .headers(headers -> headers
87  //                        .contentSecurityPolicy(contentSecurityPolicy -> // TODO: add tests for contentSecurityPolicy header
88  //                                contentSecurityPolicy.policyDirectives(CONTENT_POLICY_DIRECTIVES)
89  //                        )
90  //                );
91  
92          // TODO: does this need to be inside any other condition?? Cover it through integration tests.
93          if (profileManager.isH2Active()) {
94              setupH2ConsoleSecurity(http);
95          }
96  
97          if (!featureManager.isActive(WEB_SECURITY)) {
98              return configureInsecureAccess(http);
99          }
100 
101         return http
102                 .authorizeHttpRequests((auth) -> auth.requestMatchers(ENDPOINTS_WHITELIST).permitAll())
103 
104                 .authorizeHttpRequests((auth) -> auth.anyRequest().authenticated())
105 
106                 // One reason to override most of the defaults in Spring Security is to hide that the application
107                 // is secured with Spring Security. We also want to minimize the information a potential attacker
108                 // knows about the application.
109                 .formLogin(this::configureFormLogin)
110 
111                 .logout(this::configureFormLogout)
112 
113                 .build();
114     }
115 
116     @Bean
117     @FeatureAssociation(value = WEB_SECURITY)
118     public AuthenticationManager authenticationManager(
119             final AuthenticationConfiguration authenticationConfiguration) throws Exception {
120         return authenticationConfiguration.getAuthenticationManager();
121     }
122 
123     // TODO: do I need @FeatureAssociation(value = WEB_SECURITY) it here???
124     public void setupH2ConsoleSecurity(final HttpSecurity http) throws Exception {
125         final RequestMatcher h2PathMatcher = new AntPathRequestMatcher(h2ConsolePattern);
126         final XFrameOptionsHeaderWriter sameOriginXFrameHeaderWriter = new XFrameOptionsHeaderWriter(SAMEORIGIN);
127         // DelegatingRequestMatcherHeaderWriter is used to apply the XFrame header only to the h2 path. On the other
128         // hand, if we had used .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)),
129         // then it'd have applied the XFrame header to all the paths/urls.
130         final DelegatingRequestMatcherHeaderWriter headerWriter =
131                 new DelegatingRequestMatcherHeaderWriter(h2PathMatcher, sameOriginXFrameHeaderWriter);
132         http
133                 .authorizeHttpRequests(auth -> auth.requestMatchers(h2PathMatcher).permitAll())
134                 .headers(headers -> headers.addHeaderWriter(headerWriter))
135         ;
136     }
137 
138     private DefaultSecurityFilterChain configureInsecureAccess(final HttpSecurity http) throws Exception {
139         return http
140                 // Spring's recommendation is to use CSRF protection for any request that could be processed by a
141                 // browser by normal users. If you are only creating a service that is used by non-browser clients,
142                 // you will likely want to disable CSRF protection. If our stateless API uses token-based
143                 // authentication, such as JWT, we don't need CSRF protection, and we must disable.
144                 // However, if our stateless API uses a session cookie authentication, we need to enable CSRF protection
145                 .csrf(AbstractHttpConfigurer::disable)
146                 .authorizeHttpRequests((auth) -> auth.anyRequest().permitAll())
147                 .build();
148     }
149 
150     private void configureFormLogin(final FormLoginConfigurer<HttpSecurity> formLoginConfigurer) {
151         formLoginConfigurer
152                 .loginPage(LOGIN_PAGE_URL)
153                 // By overriding this default URL, we’re concealing that the application is actually secured
154                 // with Spring Security. This information should not be available externally.
155                 .loginProcessingUrl(LOGIN_PROCESSING_URL)
156                 .permitAll()
157                 .failureUrl(LOGIN_FAILURE_URL);
158     }
159 
160     private void configureFormLogout(LogoutConfigurer<HttpSecurity> logoutConfigurer) {
161         logoutConfigurer
162                 .logoutUrl(LOGOUT_URL)
163                 .logoutSuccessUrl(LOGOUT_SUCCESS_URL);
164     }
165 
166 //    @Bean
167 //    @FeatureAssociation(value = WEB_SECURITY)
168 //    public WebSecurityCustomizer webSecurityCustomizer() {
169 //        return (web) -> web.ignoring().requestMatchers(
170 //                antMatcher("/css/**"),
171 //                antMatcher("/js/**"),
172 //                antMatcher("/img/**"),
173 //                antMatcher("/webjars/**")
174 //        );
175 //    }
176 }