Skip to content

Commit b8488a9

Browse files
committed
feat(security): set secure response headers and ensure custom error responses
1 parent 33152d1 commit b8488a9

17 files changed

+847
-8
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/error/ApiExceptionHandler.java

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

1010
import javax.servlet.http.HttpServletRequest;
11+
import org.springframework.core.Ordered;
12+
import org.springframework.core.annotation.Order;
1113
import org.springframework.http.HttpStatus;
1214
import org.springframework.http.ResponseEntity;
1315
import org.springframework.web.bind.annotation.ControllerAdvice;
@@ -16,6 +18,7 @@
1618
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
1719

1820
@ControllerAdvice
21+
@Order(Ordered.HIGHEST_PRECEDENCE)
1922
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
2023

2124
@ResponseBody
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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;
9+
10+
import java.lang.annotation.ElementType;
11+
import java.lang.annotation.Retention;
12+
import java.lang.annotation.RetentionPolicy;
13+
import java.lang.annotation.Target;
14+
import org.springframework.context.annotation.Import;
15+
16+
@Retention(RetentionPolicy.RUNTIME)
17+
@Target(ElementType.TYPE)
18+
@Import({SdaWebSecurityConfiguration.class})
19+
public @interface EnableSdaWebSecurity {}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
package org.sdase.commons.spring.boot.web.security;
99

10+
import org.sdase.commons.spring.boot.web.security.headers.RestfulApiSecurityConfiguration;
1011
import org.springframework.boot.autoconfigure.AutoConfiguration;
1112
import org.springframework.boot.web.servlet.FilterRegistrationBean;
1213
import org.springframework.context.annotation.Bean;
@@ -19,7 +20,7 @@
1920
@EnableWebSecurity
2021
@ComponentScan
2122
@AutoConfiguration
22-
@Import({SdaCorsConfigurer.class})
23+
@Import({RestfulApiSecurityConfiguration.class, SdaCorsConfigurer.class})
2324
public class SdaWebSecurityConfiguration {
2425
@Bean
2526
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.handler;
9+
10+
import org.sdase.commons.spring.boot.web.error.ApiError;
11+
import org.springframework.core.MethodParameter;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.http.MediaType;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.http.converter.HttpMessageConverter;
16+
import org.springframework.http.server.ServerHttpRequest;
17+
import org.springframework.http.server.ServerHttpResponse;
18+
import org.springframework.http.server.ServletServerHttpResponse;
19+
import org.springframework.web.bind.annotation.RestControllerAdvice;
20+
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
21+
22+
/**
23+
* Error handle that replaces default errors responses with custom {@link ApiError}.
24+
*
25+
* <p>This handler is invoked for explicitly defined error responses like {@code return
26+
* ResponseEntity.status(404).build();} and is not invoked for response indirectly create by {@code
27+
* throw new NotFoundException()}.
28+
*
29+
* <p>This handler addresses risks identified in the security guide as:
30+
*
31+
* <ul>
32+
* <li>"Risk: Detection of confidential components ... Removal of application related Error
33+
* messages."
34+
* </ul>
35+
*/
36+
@RestControllerAdvice
37+
public class ObscuringErrorHandler implements ResponseBodyAdvice<Object> {
38+
private static final String RESPONSE_ENTITY_TYPE = ResponseEntity.class.getName();
39+
private static final String ACTUATOR_PACKAGE_NAME =
40+
"org.springframework.boot.actuate.endpoint.web.servlet";
41+
private static final String API_ERROR_RESPONSE_ENTITY_TYPE =
42+
String.format("%s<%s>", ResponseEntity.class.getTypeName(), ApiError.class.getTypeName());
43+
44+
@Override
45+
public boolean supports(
46+
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
47+
return shouldTransformResponse(returnType);
48+
}
49+
50+
@Override
51+
public Object beforeBodyWrite(
52+
Object body,
53+
MethodParameter returnType,
54+
MediaType selectedContentType,
55+
Class<? extends HttpMessageConverter<?>> selectedConverterType,
56+
ServerHttpRequest request,
57+
ServerHttpResponse response) {
58+
var responseStatus =
59+
HttpStatus.resolve(((ServletServerHttpResponse) response).getServletResponse().getStatus());
60+
61+
// No replacement for `ResponseEntity.status(204).build()`
62+
if (responseStatus == null || !responseStatus.isError()) {
63+
return body;
64+
}
65+
66+
// Replacement error responses with standard errors, e.g
67+
// `ResponseEntity.internalServerError().body(customErrorBody)
68+
var apiError = new ApiError();
69+
apiError.setTitle("HTTP Error " + responseStatus.value() + " occurred.");
70+
return apiError;
71+
}
72+
73+
private boolean shouldTransformResponse(MethodParameter returnType) {
74+
// No replacement for `ResponseEntity<ApiError>`
75+
if (API_ERROR_RESPONSE_ENTITY_TYPE.equals(returnType.getGenericParameterType().getTypeName())) {
76+
return false;
77+
}
78+
79+
// No replacement for `ResponseEntity<MyDto>`
80+
if (!RESPONSE_ENTITY_TYPE.equals(returnType.getParameterType().getTypeName())) {
81+
return false;
82+
}
83+
84+
// No replacement for `ResponseEntity<Object>` returned by spring actuator
85+
return !ACTUATOR_PACKAGE_NAME.equals(
86+
returnType.getExecutable().getDeclaringClass().getPackageName());
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.handler;
9+
10+
import org.sdase.commons.spring.boot.web.error.ApiError;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.web.bind.annotation.ControllerAdvice;
17+
import org.springframework.web.bind.annotation.ExceptionHandler;
18+
import org.springframework.web.context.request.WebRequest;
19+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
20+
21+
/**
22+
* This handler addresses risks identified in the security guide as:
23+
*
24+
* <ul>
25+
* <li>"Risk: Detection of confidential components ... Removal of application related Error
26+
* messages."
27+
* </ul>
28+
*/
29+
@ControllerAdvice
30+
public class RunTimeExceptionHandler extends ResponseEntityExceptionHandler {
31+
private static final String ERROR_MESSAGE = "An exception occurred.";
32+
private static final Logger LOG = LoggerFactory.getLogger(RunTimeExceptionHandler.class);
33+
34+
@ExceptionHandler(value = {RuntimeException.class})
35+
protected ResponseEntity<ApiError> handleRuntimeException(
36+
RuntimeException ex, WebRequest request) {
37+
LOG.error(ERROR_MESSAGE, ex);
38+
var headers = new HttpHeaders();
39+
return new ResponseEntity<>(
40+
new ApiError(ERROR_MESSAGE), headers, HttpStatus.INTERNAL_SERVER_ERROR);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.lang.annotation.ElementType;
11+
import java.lang.annotation.Retention;
12+
import java.lang.annotation.RetentionPolicy;
13+
import java.lang.annotation.Target;
14+
import org.springframework.context.annotation.Import;
15+
16+
/**
17+
* Activates setting headers to the response that enhance the security of web applications. Usually
18+
* we do not provide web content from services. But we address the risks identified in the security
19+
* guide as:
20+
*
21+
* <ul>
22+
* <li>"Risk: Cross Site Scripting (XSS)"
23+
* <li>"Risk: Reloading content into Flash and PDFs"
24+
* <li>"Risk: Clickjacking"
25+
* <li>"Risk: Passing on visited URLs to third parties"
26+
* <li>"Risk: Interpretation of content by the browser"
27+
* </ul>
28+
*
29+
* <p>This feature should only be enabled in services that provide frontend resources like HTML
30+
* pages themselves. Services that provide REST APIs only shall not activate this feature. If not
31+
* enabled, headers are set {@linkplain RestfulApiSecurityConfiguration according risks for backend
32+
* service}.
33+
*/
34+
@Retention(RetentionPolicy.RUNTIME)
35+
@Target(ElementType.TYPE)
36+
@Import({FrontendSecurityConfiguration.class})
37+
public @interface EnableFrontendSecurity {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 org.springframework.context.annotation.Bean;
11+
12+
/**
13+
* This filter adds headers to the response that enhance the security of web applications. Usually
14+
* we do not provide web content from services. But we address the risks identified in the security
15+
* guide as:
16+
*
17+
* <ul>
18+
* <li>"Risk: Cross Site Scripting (XSS)"
19+
* <li>"Risk: Reloading content into Flash and PDFs"
20+
* <li>"Risk: Clickjacking"
21+
* <li>"Risk: Passing on visited URLs to third parties"
22+
* <li>"Risk: Interpretation of content by the browser"
23+
* </ul>
24+
*
25+
* <p>This feature should only be enabled in services that provide frontend resources like HTML
26+
* pages themselves. Services that provide REST APIs only shall not activate this feature. If not
27+
* enabled, headers are set {@linkplain RestfulApiSecurityConfiguration according risks for backend
28+
* service}.
29+
*/
30+
public class FrontendSecurityConfiguration {
31+
32+
public static final String FRONTEND_SECURITY_ADVICE_BEAN_NAME = "frontendHeadersAdvice";
33+
34+
@Bean(FRONTEND_SECURITY_ADVICE_BEAN_NAME)
35+
public SdaSecurityHeaders sdaSecurityHeaders() {
36+
return SdaSecurityType.FRONTEND_SECURITY::headers;
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.Configuration;
13+
14+
/**
15+
* This filter adds headers to the response that enhance the security of applications that serve
16+
* only REST APIs. The following risks are addressed:
17+
*
18+
* <ul>
19+
* <li>"Risk: Cross Site Scripting (XSS)"
20+
* <li>"Risk: Reloading content into Flash and PDFs"
21+
* <li>"Risk: Clickjacking"
22+
* <li>"Risk: Passing on visited URLs to third parties"
23+
* <li>"Risk: Interpretation of content by the browser"
24+
* </ul>
25+
*/
26+
@Configuration
27+
public class RestfulApiSecurityConfiguration {
28+
@Bean
29+
@ConditionalOnMissingBean(name = FrontendSecurityConfiguration.FRONTEND_SECURITY_ADVICE_BEAN_NAME)
30+
public SdaSecurityHeaders sdaSecurityHeaders() {
31+
return SdaSecurityType.RESTFUL_SECURITY::headers;
32+
}
33+
}
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+
}

0 commit comments

Comments
 (0)