I have a question regarding the use of the RFC7807 ProblemDetails-type that was introduced in Spring 6 and the use of Spring Security 6 to validate JWT tokens.
My hypothesis:
When an invalid token is supplied to a secured endpoint, the application should return a 401 and a ProblemDetails body. However, I cannot seem to find any config options to turn on this ProblemDetails-body.
What is actually happening:
When an invalid token is supplied to a secured endpoint, the application returns a 401 without a body
Here is an example of my SecurityFilterChain
bean:
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity, NTKeycloakAuthProperties ntKeycloakAuthProperties) throws Exception {
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
authorizationManagerRequestMatcherRegistry.requestMatchers("/secured/**").authenticated()
authorizationManagerRequestMatcherRegistry.requestMatchers("/**").permitAll();
})
.oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> {
httpSecurityOAuth2ResourceServerConfigurer.authenticationManagerResolver(
new JwtIssuerAuthenticationManagerResolver(ntKeycloakAuthProperties.getIssuers())
);
})
// Question: Should there exist something pre-built and easy configurable that can enable RFC7807-type error response bodies?
.build();
}
So in short; Is it possible to enable RFC7807-style error response bodies when using Spring Security 6?
This answer is thanks to @M.Deinum
1. There is no built in ProblemDetails-feature for Spring Security
Since Spring Security does not leverage exception handling from Spring, there is no ProblemDetails support built in.
2. How to implement this yourself
One can easily achieve this behavior by implementing two different classes and wiring them up.
Firstly we need to create an ExceptionHandler that can handle the AuthenticationException.class
from Spring Security
@RestControllerAdvice
public class AuthenticationExceptionEntityExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler(AuthenticationException.class)
ProblemDetail handleAuthenticationErrorResponseException() {
return ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED,
"Could not authenticate user"
);
}
}
Then we need to implement an AuthenticationEntryPoint.class
which will delegate the exception to the HandlerExceptionResolver
-implementation
public class BearerTokenProblemDetailsAuthenticationEntryPoint
implements AuthenticationEntryPoint {
HandlerExceptionResolver handlerExceptionResolver;
public BearerTokenProblemDetailsAuthenticationEntryPoint(
HandlerExceptionResolver handlerExceptionResolver
) {
this.handlerExceptionResolver = handlerExceptionResolver;
}
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) {
handlerExceptionResolver.resolveException(
request,
response,
null,
authException
);
}
}
And finally we need to configure our SecurityFilterChain
-bean
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity, NTKeycloakAuthProperties ntKeycloakAuthProperties) throws Exception {
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
authorizationManagerRequestMatcherRegistry.requestMatchers("/secured/**").authenticated()
authorizationManagerRequestMatcherRegistry.requestMatchers("/**").permitAll();
})
.oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer -> {
httpSecurityOAuth2ResourceServerConfigurer.authenticationManagerResolver(
new JwtIssuerAuthenticationManagerResolver(ntKeycloakAuthProperties.getIssuers())
);
// Add the entry point here
httpSecurityOAuth2ResourceServerConfigurer.authenticationEntryPoint(
new BearerTokenProblemDetailsAuthenticationEntryPoint(handlerExceptionResolver)
);
})
.build();
}