Skip to content

Commit 1ae44cf

Browse files
committed
fix(security): set secure response headers in any condition of SdaSecurityConfiguration
1 parent d64820c commit 1ae44cf

File tree

10 files changed

+257
-261
lines changed

10 files changed

+257
-261
lines changed

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/auth/SdaSecurityConfiguration.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
package org.sdase.commons.spring.boot.web.auth;
99

1010
import java.util.List;
11+
import java.util.Optional;
1112
import java.util.stream.Stream;
1213
import javax.servlet.http.HttpServletRequest;
14+
import org.sdase.commons.spring.boot.web.security.headers.SdaSecurityHeaders;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
1517
import org.springframework.beans.factory.annotation.Value;
@@ -26,6 +28,7 @@
2628
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
2729
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
2830
import org.springframework.security.web.SecurityFilterChain;
31+
import org.springframework.security.web.header.writers.StaticHeadersWriter;
2932
import org.springframework.util.StringUtils;
3033

3134
@EnableWebSecurity
@@ -41,6 +44,8 @@ public class SdaSecurityConfiguration {
4144

4245
private final SdaAccessDecisionManager sdaAccessDecisionManager;
4346

47+
private final SdaSecurityHeaders sdaSecurityHeaders;
48+
4449
/**
4550
* @param issuers Comma separated string of open id discovery key sources with required issuers.
4651
* @param disableAuthentication Disables all authentication
@@ -50,10 +55,12 @@ public class SdaSecurityConfiguration {
5055
public SdaSecurityConfiguration(
5156
@Value("${auth.issuers:}") String issuers,
5257
@Value("${auth.disable:false}") boolean disableAuthentication,
53-
SdaAccessDecisionManager sdaAccessDecisionManager) {
58+
SdaAccessDecisionManager sdaAccessDecisionManager,
59+
Optional<SdaSecurityHeaders> sdaSecurityHeaders) {
5460
this.issuers = issuers;
5561
this.disableAuthentication = disableAuthentication;
5662
this.sdaAccessDecisionManager = sdaAccessDecisionManager;
63+
this.sdaSecurityHeaders = sdaSecurityHeaders.orElse(List::of);
5764
}
5865

5966
@Bean
@@ -82,7 +89,11 @@ private void oidcAuthentication(HttpSecurity http) throws Exception {
8289
authorize ->
8390
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager))
8491
.oauth2ResourceServer(
85-
oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver));
92+
oauth2 -> oauth2.authenticationManagerResolver(authenticationManagerResolver))
93+
.headers(
94+
configurer ->
95+
configurer.addHeaderWriter(
96+
new StaticHeadersWriter(sdaSecurityHeaders.getSecurityHeaders())));
8697
}
8798

8899
private AuthenticationManagerResolver<HttpServletRequest> createAuthenticationManagerResolver() {
@@ -111,7 +122,11 @@ private void noAuthentication(HttpSecurity http) throws Exception {
111122
.disable() // NOSONAR
112123
.authorizeRequests(
113124
authorize ->
114-
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager));
125+
authorize.anyRequest().permitAll().accessDecisionManager(sdaAccessDecisionManager))
126+
.headers(
127+
configurer ->
128+
configurer.addHeaderWriter(
129+
new StaticHeadersWriter(sdaSecurityHeaders.getSecurityHeaders())));
115130
}
116131

117132
private List<String> commaSeparatedStringToList(String issuers) {

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/FrontendSecurityConfiguration.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
package org.sdase.commons.spring.boot.web.security.headers;
99

1010
import org.springframework.context.annotation.Bean;
11-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12-
import org.springframework.security.web.SecurityFilterChain;
1311

1412
/**
1513
* This filter adds headers to the response that enhance the security of web applications. Usually
@@ -34,9 +32,7 @@ public class FrontendSecurityConfiguration {
3432
public static final String FRONTEND_SECURITY_ADVICE_BEAN_NAME = "frontendHeadersAdvice";
3533

3634
@Bean(FRONTEND_SECURITY_ADVICE_BEAN_NAME)
37-
public SecurityFilterChain frontendHeadersAdvice(HttpSecurity http) throws Exception {
38-
// add specific headers for frontends
39-
http.headers(HttpHeadersAdvice::addFrontendHttpHeadersCustomizer);
40-
return http.build();
35+
public SdaSecurityHeaders sdaSecurityHeaders() {
36+
return SdaSecurityType.FRONTEND_SECURITY::headers;
4137
}
4238
}

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/HttpHeadersAdvice.java

Lines changed: 0 additions & 67 deletions
This file was deleted.

sda-commons-web-autoconfigure/src/main/java/org/sdase/commons/spring/boot/web/security/headers/RestfulApiSecurityConfiguration.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
1111
import org.springframework.context.annotation.Bean;
1212
import org.springframework.context.annotation.Configuration;
13-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14-
import org.springframework.security.web.SecurityFilterChain;
1513

1614
/**
1715
* This filter adds headers to the response that enhance the security of applications that serve
@@ -27,12 +25,9 @@
2725
*/
2826
@Configuration
2927
public class RestfulApiSecurityConfiguration {
30-
3128
@Bean
3229
@ConditionalOnMissingBean(name = FrontendSecurityConfiguration.FRONTEND_SECURITY_ADVICE_BEAN_NAME)
33-
public SecurityFilterChain restfulApiHeadersAdvice(HttpSecurity http) throws Exception {
34-
// define specific headers for restful apis
35-
http.headers(HttpHeadersAdvice::addRestfulHttpHeadersCustomizer);
36-
return http.build();
30+
public SdaSecurityHeaders sdaSecurityHeaders() {
31+
return SdaSecurityType.RESTFUL_SECURITY::headers;
3732
}
3833
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
3+
*
4+
* Use of this source code is governed by an MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
package org.sdase.commons.spring.boot.web.security.headers;
9+
10+
import java.util.List;
11+
import org.springframework.security.web.header.Header;
12+
13+
public interface SdaSecurityHeaders {
14+
15+
List<Header> getSecurityHeaders();
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
3+
*
4+
* Use of this source code is governed by an MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
package org.sdase.commons.spring.boot.web.security.headers;
9+
10+
import static java.util.Arrays.asList;
11+
import static org.sdase.commons.spring.boot.web.security.headers.SdaSecurityType.Common.WEB_SECURITY_HEADERS;
12+
13+
import java.util.List;
14+
import java.util.stream.Stream;
15+
import org.springframework.security.web.header.Header;
16+
17+
public enum SdaSecurityType {
18+
RESTFUL_SECURITY(
19+
Stream.concat(
20+
WEB_SECURITY_HEADERS.stream(),
21+
Stream.of(
22+
new Header(
23+
"Content-Security-Policy",
24+
String.join(
25+
"; ",
26+
asList("default-src 'none'", "frame-ancestors 'none'", "sandbox")))))
27+
.toList()),
28+
29+
FRONTEND_SECURITY(
30+
Stream.concat(
31+
WEB_SECURITY_HEADERS.stream(),
32+
Stream.of(
33+
new Header(
34+
"Content-Security-Policy",
35+
String.join(
36+
"; ",
37+
asList(
38+
"default-src 'self'",
39+
"script-src 'self'",
40+
"img-src 'self'",
41+
"style-src 'self'",
42+
"font-src 'self'",
43+
"frame-src 'none'",
44+
"object-src 'none'")))))
45+
.toList());
46+
47+
private final List<Header> headers;
48+
49+
SdaSecurityType(List<Header> headers) {
50+
this.headers = headers;
51+
}
52+
53+
public List<Header> headers() {
54+
return headers;
55+
}
56+
57+
static class Common {
58+
private Common() {
59+
// just to hold WEB_SECURITY_HEADERS
60+
}
61+
62+
static final List<Header> WEB_SECURITY_HEADERS =
63+
List.of(
64+
new Header("X-Frame-Options", "DENY"),
65+
new Header("X-Content-Type-Options", "nosniff"),
66+
new Header("X-XSS-Protection", "1; mode=block"),
67+
new Header("Referrer-Policy", "same-origin"),
68+
new Header("X-Permitted-Cross-Domain-Policies", "none"));
69+
}
70+
}
Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,33 @@
3535
import org.springframework.http.ResponseEntity;
3636
import org.springframework.test.context.ContextConfiguration;
3737

38-
/** This test class depends on the {@code @EnableFrontendSecurity} annotation being present. */
39-
@EnableFrontendSecurity
4038
@SpringBootTest(
4139
classes = SecurityTestApp.class,
4240
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
4341
@ContextConfiguration(initializers = DisableSdaAuthInitializer.class)
44-
class FrontendSecurityHeadersTest {
42+
// TODO needs same test with unconfigured SdaSecurityConfiguration and with enabled authorization
43+
abstract class AbstractSecurityHeadersTest {
4544
@LocalServerPort private int port;
4645

4746
@Autowired private TestRestTemplate client;
4847

49-
static Stream<Arguments> predefinedSecurityHeaders() {
48+
static Stream<Arguments> predefinedRestfulApiSecurityHeaders() {
49+
return Stream.of(
50+
// cache headers
51+
of("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"),
52+
of("Pragma", "no-cache"),
53+
of("Expires", "0"),
54+
55+
// security headers
56+
of("X-Frame-Options", "DENY"),
57+
of("X-Content-Type-Options", "nosniff"),
58+
of("X-XSS-Protection", "1; mode=block"),
59+
of("Referrer-Policy", "same-origin"),
60+
of("X-Permitted-Cross-Domain-Policies", "none"),
61+
of("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; sandbox"));
62+
}
63+
64+
static Stream<Arguments> predefinedFrontendSecurityHeaders() {
5065
return Stream.of(
5166
// cache headers
5267
of("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"),
@@ -73,10 +88,11 @@ static Stream<Arguments> predefinedSecurityHeaders() {
7388
"object-src 'none'"))));
7489
}
7590

91+
protected abstract Stream<Arguments> predefinedSecurityHeaders();
92+
7693
@ParameterizedTest
77-
@MethodSource("predefinedSecurityHeaders")
78-
void shouldAddFrontendSecurityHeaders(
79-
String predefinedHeaderName, String expectedPredefinedHeaderValue) {
94+
@MethodSource("predefinedRestfulApiSecurityHeaders")
95+
void shouldAddSecurityHeaders(String predefinedHeaderName, String expectedPredefinedHeaderValue) {
8096

8197
ResponseEntity<TestResource> actual =
8298
client.getForEntity(getServerBaseUrl() + "/api/resource", TestResource.class);
@@ -92,8 +108,8 @@ void shouldAddFrontendSecurityHeaders(
92108
}
93109

94110
@ParameterizedTest
95-
@MethodSource("predefinedSecurityHeaders")
96-
void shouldAllowOverwritingFrontendSecurityHeaders(String predefinedHeaderName) {
111+
@MethodSource("predefinedRestfulApiSecurityHeaders")
112+
void shouldAllowOverwritingHeaders(String predefinedHeaderName) {
97113
// for unknown reason, X-Frame-Options can't be modified
98114
assumeThat(predefinedHeaderName).isNotEqualTo("X-Frame-Options");
99115

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
3+
*
4+
* Use of this source code is governed by an MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
package org.sdase.commons.spring.boot.web.security.headers;
9+
10+
import java.util.stream.Stream;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.params.ParameterizedTest;
13+
import org.junit.jupiter.params.provider.Arguments;
14+
import org.junit.jupiter.params.provider.MethodSource;
15+
import org.sdase.commons.spring.boot.web.security.test.SecurityTestApp;
16+
import org.sdase.commons.spring.boot.web.testing.auth.AuthMock;
17+
import org.sdase.commons.spring.boot.web.testing.auth.DisableSdaAuthInitializer;
18+
import org.sdase.commons.spring.boot.web.testing.auth.EnableSdaAuthMockInitializer;
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.boot.test.context.SpringBootTest;
21+
import org.springframework.test.context.ContextConfiguration;
22+
23+
/** This test class depends on the {@code @EnableFrontendSecurity} annotation being present. */
24+
@EnableFrontendSecurity
25+
abstract class FrontendSecurityHeadersTestHolder extends AbstractSecurityHeadersTest {
26+
27+
@SpringBootTest(
28+
classes = SecurityTestApp.class,
29+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
30+
@ContextConfiguration(initializers = EnableSdaAuthMockInitializer.class)
31+
static class AuthEnabledTest extends FrontendSecurityHeadersTestHolder {
32+
33+
@Autowired AuthMock authMock;
34+
35+
@BeforeEach
36+
void allow() {
37+
authMock.authorizeAnyRequest().allow();
38+
}
39+
}
40+
41+
@SpringBootTest(
42+
classes = SecurityTestApp.class,
43+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
44+
@ContextConfiguration(initializers = DisableSdaAuthInitializer.class)
45+
static class AuthDisabledTest extends FrontendSecurityHeadersTestHolder {}
46+
47+
@Override
48+
protected Stream<Arguments> predefinedSecurityHeaders() {
49+
return AbstractSecurityHeadersTest.predefinedFrontendSecurityHeaders();
50+
}
51+
52+
@ParameterizedTest
53+
@MethodSource("predefinedFrontendSecurityHeaders")
54+
void shouldAddSecurityHeaders(String predefinedHeaderName, String expectedPredefinedHeaderValue) {
55+
super.shouldAddSecurityHeaders(predefinedHeaderName, expectedPredefinedHeaderValue);
56+
}
57+
58+
@ParameterizedTest
59+
@MethodSource("predefinedFrontendSecurityHeaders")
60+
void shouldAllowOverwritingSecurityHeaders(String predefinedHeaderName) {
61+
super.shouldAllowOverwritingHeaders(predefinedHeaderName);
62+
}
63+
}

0 commit comments

Comments
 (0)