7

This is a typical part of Spring Security configuration:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().and().cors().disable();
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll();
    http.authorizeRequests().anyRequest().authenticated();
}

I have a problem with http.authorizeRequests().anyRequest().authenticated().

After adding it, when I call non-existing endpoints, for example: GET: /api/v1/not-existing, I receive 403 instead of expected 404 response.

I want to protect all my resources but I want to get 404 when calling not existing resources.

How can I fix it?

João Dias
  • 16,277
  • 6
  • 33
  • 45
Avaldor
  • 317
  • 2
  • 8

4 Answers4

3

I am okay with this behaviour . If an user is not authenticated , why bother to worry about telling him more information about your system. Just like if an user does not have permission to view your harddisk , why need to let him can discover your harddisk directory tree structure .

If you really want to return 404 , you need to customize AuthenticationEntryPoint and AccessDeniedHandler in the ExceptionTranslationFilter . Both of them will be invoked if an user does not have enough permission to visit an endpoint (i.e. AccessDeniedException happen). The former is for the anonymous user and the latter is for the non-anonymous user (i.e. user that is authenticated successfully but without enough permission)

Both of their default implementation (i.e Http403ForbiddenEntryPoint and AccessDeniedHandlerImpl) simply return 403 now . You have to customize them such that they will first check if there are existing endpoints to serve the current HttpServletRequest and return 404 if no. You can do it by looping through the HandlerMapping inside the DispatcherServlet and check if any of the HandlerMapping can handle the current HttpServletRequest.

First create an object that do this check :

public class HttpRequestEndpointChecker {

    private DispatcherServlet servlet;

    public HttpRequestEndpointChecker(DispatcherServlet servlet) {
        this.servlet = servlet;
    }

    public boolean isEndpointExist(HttpServletRequest request) {

        for (HandlerMapping handlerMapping : servlet.getHandlerMappings()) {
            try {
                HandlerExecutionChain foundHandler = handlerMapping.getHandler(request);
                if (foundHandler != null) {
                    return true;
                }
            } catch (Exception e) {
                return false;
            }
        }
        return false;
    }
}   

Then customize AuthenticationEntryPoint and AccessDeniedHandler to use this object for checking :

public  class MyAccessDeniedHandler extends AccessDeniedHandlerImpl {
    private HttpRequestEndpointChecker endpointChecker;

    public MyAccessDeniedHandler(HttpRequestEndpointChecker endpointChecker) {
        this.endpointChecker = endpointChecker;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {

        if (!endpointChecker.isEndpointExist(request)) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource not found");
        } else {
            super.handle(request, response, accessDeniedException);
        }
    }
}
public class MyAuthenticationEntryPoint extends Http403ForbiddenEntryPoint {

        private HttpRequestEndpointChecker endpointChecker;

        public MyAuthenticationEntryPoint(HttpRequestEndpointChecker endpointChecker) {
            this.endpointChecker = endpointChecker;
        }

        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException authException) throws IOException {
            if (!endpointChecker.isEndpointExist(request)) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Resource not found");
            } else {
                super.commence(request, response, authException);
            }
        }
}

And configure them :

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DispatcherServlet dispatcherServlet;

    @Autowired
    private HttpRequestEndpointChecker endpointChecker;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            ..............
            ..............
            http.exceptionHandling()
                .authenticationEntryPoint(new MyAuthenticationEntryPoint(endpointChecker))
                .accessDeniedHandler(new MyAccessDeniedHandler(endpointChecker));

    }

    @Bean
    public HttpRequestEndpointChecker endpointChecker() {
        return new HttpRequestEndpointChecker(dispatcherServlet);
    }

}
Ken Chan
  • 84,777
  • 26
  • 143
  • 172
  • I have always though that default behaviour is 404 and not 403, isn't it?... :o And that it's something unusual that I return 403. – Avaldor Nov 26 '21 at 10:06
  • Depend on how you view it . I prefer to return 403 in this case with the reasons that I mentioned in the answer. I feel it is very natural and very make sense – Ken Chan Nov 26 '21 at 14:52
1

It seems to me that your only option is the following:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().and().cors().disable();
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll();
    http.authorizeRequests().antMatchers(all-your-endpoints).authenticated();
    http.authorizeRequests().anyRequest().permitAll();
}

You need to replace all-your-endpoints with a regex or multiple regex that match all your endpoints. In fact, you can even get rid of http.authorizeRequests().antMatchers("/login", "/api/v1/auth/**").permitAll(); unless you really want to be explicit about it.

João Dias
  • 16,277
  • 6
  • 33
  • 45
0

I was having a similar issue, where I was getting 403 for any url. After a while I found out the with the spring update from 1.5.x to something later, the way to declare the contextPath changed. So Spring could not resolve my url because I had wrong declaration on my application.yml

It changed from:

server:
  contextPath: /app-name

To:

server:  
  servlet:
    contextPath: /app-name
0

You can define /error in the SecurityFilterChain to get this behavior

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeHttpRequests(requests -> {
                    requests.requestMatchers("/public-end-points/**","/error").permitAll();
                    requests.anyRequest().authenticated();
                })
                .build();
    }
}
jfk
  • 4,335
  • 34
  • 27