javaspring-bootauthenticationspring-securityexceptionhandler

How I can process authentication exceptions and basic HTTP500-like exceptions separetely in Spring Boot 3


I'm building an API using Spring Boot 3.0.1 with Spring Security, I've built out the security filter chain and use custom exception handler in it to process exceptions in authentication (for example when I get request without correct JWT token). But this handler processing all others exceptions (such as invalid agruments in query) by default and in situation when I try to process invalid request I getting 401-unauthorised exception from my handler by default instead of 500-internal server error. What should I do for make my custom handler in FilterChain process ONLY authentication exceptions?

My SecurityChain config:

@Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(AbstractHttpConfigurer::disable)
                .exceptionHandling(
                        exception -> exception.authenticationEntryPoint(unauthorizedEntryPoint)
                )
                .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/taskservice/api/v1/auth/**").permitAll()
                    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    .anyRequest().authenticated()
                )
                .sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
                .authenticationProvider(authenticationProvider())
                .addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
        ;

        return http.build();
    }

My custom exception handler:

@Component
@Slf4j
public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        log.error("Unauthorized error: {}", authException.getMessage());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        LoginResponseError body = LoginResponseError.builder()
                .status(HttpServletResponse.SC_UNAUTHORIZED)
                .error("Unauthorized")
                .timestamp(Instant.now())
                .message(authException.getMessage())
                .path(request.getServletPath())
                .build();
        final ObjectMapper mapper = new ObjectMapper();
        // register the JavaTimeModule, which enables Jackson to support Java 8 and higher date and time types
        mapper.registerModule(new JavaTimeModule());
        // ask Jackson to serialize dates as strings in the ISO 8601 format
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,false);
        mapper.writeValue(response.getOutputStream(), body);
    }
}

My filter:

@Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {


        // try to get JWT in cookie or in Authorization Header
        String jwt = jwtService.getJwtFromCookies(request);
        final String authHeader = request.getHeader("Authorization");

        if((jwt == null && (authHeader ==  null || !authHeader.startsWith("Bearer "))) || request.getRequestURI().contains("/auth")){
            filterChain.doFilter(request, response);
            return;
        }

        // If the JWT is not in the cookies but in the "Authorization" header
        if (jwt == null && authHeader.startsWith("Bearer ")) {
            jwt = authHeader.substring(7); // after "Bearer "
        }


        final String userEmail = jwtService.extractUserName(jwt);
        /*
           SecurityContextHolder: is where Spring Security stores the details of who is authenticated.
           Spring Security uses that information for authorization.*/

        if(StringUtils.isNotEmpty(userEmail)
                && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.employeeUserDetailsService.loadUserByUsername(userEmail);
            if(jwtService.isTokenValid(jwt, userDetails)){
                //update the spring security context by adding a new UsernamePasswordAuthenticationToken
                SecurityContext context = SecurityContextHolder.createEmptyContext();
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                context.setAuthentication(authToken);
                SecurityContextHolder.setContext(context);
            }
        }
        filterChain.doFilter(request,response);
    }

I tried to .requestMatchers("/error**").permitAll() but it leads to opposite situation: Im getting 500-internal server error errors for all errors, even for problems with authentication


Solution

  • I had a similar problem, probably this will help somehow. When throws an exception (you expect 500), the request is redirected to the "/error" endpoint. From the point of view of your configuration of securityFilterChain this endpoint is only for authenticated (anyRequest().authenticated()). So, you can add ignoring().requestMatchers("/error/**"), smth like

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
    
        return (web) ->
                web.ignoring().requestMatchers("/error/**");
    }
    

    But it is was not enought.In my case I changed logic in my filter(but it was custom filter that extends OncePerRequestFilter, added override method doFilterInternal). It threw only 500 in case of failure. Adding SecurityContextHolder.clearContext() helped me:

        try {
    ... token validation logic, etc
     } catch (Exception e) {
                    SecurityContextHolder.clearContext(); //  In case of failure. Guarantee user won't be authenticated
                }
    

    But, i didn't use exception handler class that you use. Also I've commented ".anyRequest().authenticated());" when solved the problem, but may be for another reason, don't remember.