javaspringspring-bootspring-security

Spring Security 6 and Spring Problem Details


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?


Solution

  • 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();
    }