Search code examples
javaspringspring-bootspring-security

The problem with error 403 (Forbidden) before processing the request in Dispatcher Servlet Spring Security


I have a problem in my application using Spring Security. In case of any error, before the request gets to the Dispatcher Servlet, the error status 403 (Forbidden) is returned.

The problem occurs even before the request reaches the Dispatcher Servlet. For example, when I try to access the /api/v1/offer path, I validate the fields, an error occurs, after which Spring security sends a 403 response. Even when I wrote a non-existent url instead of a 404 response (not found) I was getting a 403 response.

I would be grateful for any recommendations or hints on how to solve this problem before passing the request through the Dispatcher Servlet.

Here is my Spring Security configuration:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity
public class SecurityConfiguration {

private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;

@Value("${adapter.controller.base-path}")
private String basePath;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService, JwtService jwtService) throws Exception {
    http
            .csrf()
            .disable()
            .cors()
            .and()
            .authorizeHttpRequests(
                    c -> c
                            .requestMatchers("/api/v1/employee/**").hasAnyRole(ADMIN.name(), USER.name(), HR.name())
                            .requestMatchers(GET, basePath + "/employee/**").hasAnyAuthority(ADMIN_READ.name(), USER_READ.name())
                            .requestMatchers(POST, basePath + "/employee/**").hasAnyAuthority(ADMIN_CREATE.name(), USER_CREATE.name())
                            .requestMatchers(PUT, basePath + "/employee/**").hasAnyAuthority(ADMIN_UPDATE.name(), USER_UPDATE.name())
                            .requestMatchers(DELETE, basePath + "/employee/**").hasAnyAuthority(ADMIN_DELETE.name(), USER_DELETE.name())
                            .anyRequest()
                            .authenticated())
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(new JwtAuthenticationFilter(userDetailsService, jwtService), UsernamePasswordAuthenticationFilter.class)
            .logout()
            .logoutUrl(basePath + "/auth/logout")
            .addLogoutHandler(logoutHandler)
            .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext());

    return http.build();
}

@Bean
public WebSecurityCustomizer ignoringCustomizer() {
    return (web) -> web.ignoring().requestMatchers(basePath + "/auth", "/swagger-ui/**", "/v3/api-docs/**");
}
}

Here is my custom filter:

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String BEARER_SUBSTRING = "Bearer ";
private final UserDetailsService userDetailsService;
private final JwtService jwtService;
private final AuthenticationEntryPoint entryPoint = new BearerTokenAuthenticationEntryPoint();

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
    final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
    final String jwt;
    final String userEmail;
    if (authHeader == null || !authHeader.startsWith(BEARER_SUBSTRING)) {
        log.error("Authentication headers not found.");
        entryPoint.commence(request, response, new AuthenticationCredentialsNotFoundException("Authentication headers not found."));
        return;
    }
    jwt = authHeader.substring(BEARER_SUBSTRING.length());
    try {
        userEmail = jwtService.extractEmail(jwt);
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    } catch (ExpiredJwtException e) {
        log.error(e.getMessage());
        entryPoint.commence(request, response, new CredentialsExpiredException(e.getMessage()));
    }
}
}

Solution

  • Permit anyone to access the error endpoint:

        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, UserDetailsService userDetailsService, JwtService jwtService) throws Exception {
        http
                .csrf()
                .disable()
                .cors()
                .and()
                .authorizeHttpRequests(
                        c -> c
                                .requestMatchers("error").permitAll()
                                .requestMatchers("/api/v1/employee/**").hasAnyRole(ADMIN.name(), USER.name(), HR.name())
                                .requestMatchers(GET, basePath + "/employee/**").hasAnyAuthority(ADMIN_READ.name(), USER_READ.name())
                                .requestMatchers(POST, basePath + "/employee/**").hasAnyAuthority(ADMIN_CREATE.name(), USER_CREATE.name())
                                .requestMatchers(PUT, basePath + "/employee/**").hasAnyAuthority(ADMIN_UPDATE.name(), USER_UPDATE.name())
                                .requestMatchers(DELETE, basePath + "/employee/**").hasAnyAuthority(ADMIN_DELETE.name(), USER_DELETE.name())
                                .anyRequest()
                                .authenticated())
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authenticationProvider(authenticationProvider)
                .addFilterBefore(new JwtAuthenticationFilter(userDetailsService, jwtService), UsernamePasswordAuthenticationFilter.class)
                .logout()
                .logoutUrl(basePath + "/auth/logout")
                .addLogoutHandler(logoutHandler)
                .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext());
    
        return http.build();
    }
    

    The issue is that when an error happens, Spring send the user to the /error endpoint, but in your case that is protected, so when the users tries to access it, it gets denied and the user gets a 403 instead.