2

Using SecurityFilterChain and MethodSecurity (@Protected, @PreAuthorize, etc.) I am able to secure my endpoints no problem. The issue is, I'd like to get individualized messages for when a user is unauthenticated (not logged in) and a separate message for when the user is unauthorized (logged in, but does not have the required role or permission to access the endpoint).

Here's my AuthenticationEntryPoint class:

@Component
public class AuthEntryPoint implements AuthenticationEntryPoint {

  private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
      throws IOException {
    logger.error("Unauthorized error: {}", authException.getMessage());

    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setStatus(HttpServletResponse.SC_FORBIDDEN);

    final Map<String, Object> body = new HashMap<>();
    body.put("status", HttpServletResponse.SC_FORBIDDEN);
    body.put("error", "Unauthorized");
    body.put("message", authException.getMessage());
    body.put("path", request.getRequestURI());

    final ObjectMapper mapper = new ObjectMapper();
    mapper.writeValue(response.getOutputStream(), body);
  }
}

Here's my SecurityFilterChain Bean in SecurityConfig:

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeHttpRequests()
                .requestMatchers("/","*","/register","/login").permitAll()
                .requestMatchers("/test2").hasAnyAuthority("ROLE_USER")
                .anyRequest().authenticated()
                .and().sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .httpBasic().and()
                .authenticationProvider(authenticationProvider())
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }

The main issue is that the error is the same for both Unauthenticated and Unauthorized scenarios. I've tried printing out some info about the error with this:

    System.out.println("Error message: " +authException.getMessage());
    System.out.println("Error class: " +authException.getClass());
    System.out.println("Error cause: " +authException.getCause());
    authException.printStackTrace();

getMessage returns the same message for both scenarios. ("Full authentication is required to access this resource")

getClass returns the same exception class for both scenarios. (org.springframework.security.authentication.InsufficientAuthenticationException)

getCause returns null for both scenarios.

authException.printStackTrace() returns slightly different stacktraces.

I've also tried creating a Handler class:

@Component
public class AuthEntryPointExceptionHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException exception) throws IOException {
        // 401 HTTP Response
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "You need to be logged in to do that.");
    }

    @ExceptionHandler (value = {AccessDeniedException.class})
    public void commence(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException {
        // 403 HTTP Response
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "You don't have permission to do that: "+exception.getMessage());
    }

    @ExceptionHandler (value = {Exception.class})
    public void commence(HttpServletRequest request, HttpServletResponse response, Exception exception) throws IOException {
        // 500 HTTP Response
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error: " + exception.getMessage());
    }
}

but I still got the same error, handled by the original AuthEntryPoint class. Also for this class I tried all of these annotations: @Component, @Controller, @ControllerAdvice and I tried implementing the original AuthEntryPoint class instead of Spring's included AuthenticationEntryPoint.

I know I can check authentication status inside the first handler and manipulate the response from that, but I believe Spring should return different Exceptions for Unauthenticated and Unauthorized scenarios by default. Correct me if I'm wrong.

P.S. Authentication process works as intended. I get the JWT token in return and use it to Authorize/Authenticate myself for other requests.

I've tried looking at other people's questions on similar topics, tried watching youtube tutorials, etc., the issue still persists, and I believe the underlying cause is that spring returns the same exception for both scenarios.

UPDATE

When I update my SecurityFilterChain to .requestMatchers("/test2").authenticated(), I can extract the Authentication inside the /test2 endpoint's method via SecurityContextHolder.getContext().getAuthentication(), but when I secure the endpoint with a role .requestMatchers("/test2").hasAnyAuthority("ROLE_USER") and put the same SecurityContextHolder line inside AuthEntryPoint's commence method, I get an exception the return value of "org.springframework.security.core.context.SecurityContext.getAuthentication()" is null. Same endpoint, same valid JWT token. How come the authentication is null, if it gets set in AuthTokenFilter class, which initializes earlier than AuthEntryPoint does? I verified the order. What am I missing?

UPDATE2:

Principal principal = request.getUserPrincipal(); System.out.println(principal.getName());

This does the same thing, the same way.

UPDATE3: I've found the source of the problem. Kinda. When I change SecurityFilterChain to permit any request (http.authorizeHttpRequests().anyRequest().permitAll()) and only use Method Security (@Secured & @PreAuthorize), I get the desired separation between Forbidden and Unauthorized (unauthenticated) responses. If only I change SecurityFilterChain to ...anyRequest().authenticated(), all unaccessible scenarios just return the initial Unauthorized response. (Can't replicate the Forbidden response when SecurityFilterChain has ...authenticated()).

Basically, maybe this somewhat narrows down what might be happening. And while knowing this lets me make a usable app with correct separation, I'd still like to make ...authenticated() behave correctly.

In other words, SecurityFilterChain does not go through the same exceptions and their handlers as Method Security does, as far as I understand.

Janas
  • 21
  • 3
  • This is common problem in Spring. Please check this topic https://stackoverflow.com/questions/19767267/handle-spring-security-authentication-exceptions-with-exceptionhandler – doom4s Jan 29 '23 at 17:07
  • Tried to add `.exceptionHandling().accessDeniedHandler(authExceptionHandler).and()` to the filter chain. Still no luck. Here's the class: `@ControllerAdvice public class AuthEntryPointExceptionHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException { response.sendError(HttpServletResponse.SC_FORBIDDEN, "You don't have permission to do that: "+exception.getMessage()); } }` – Janas Jan 29 '23 at 17:36
  • @doom4s Most of these solutions include what I already tried, others catch the different errors which I also tried, but as I explained the exception itself remains the same in both cases. Other solutions are too robust and there's no guarantee they'll work. That question was asked 9 years ago, I believe whatever they got working is now outdated, and if it was such a common issue, I'm more than certain there have been more elegant solutions over the years. In case no one can point out a critical flaw in my code, I'll just use the SecurityContextHolder to check for *authentication* – Janas Jan 29 '23 at 17:44
  • Create separate JWT authentication filter by extending AbstractAuthenticationProcessingFilter. Override method "unsuccessfulAuthentication" and in response set custom JSON message. Prior to that create custom authentication exception by extending AuthenticationException and throw that on JWT token validation. Also please check this access denied handler. https://www-baeldung-com.translate.goog/spring-security-exceptions?_x_tr_sl=en&_x_tr_tl=sr&_x_tr_hl=sr&_x_tr_pto=sc Hope this will help you somewhat.GL. – doom4s Jan 29 '23 at 21:03
  • Nothing seems to work. I have another project that I worked on earlier, where, if I try accessing a protected endpoint, i'll get "Unauthorized" status in response, but if I try to access the same endpoint, with a token of a user who does not have the required role, I get "Forbidden" status. All configuration seems to be exactly identical between the two projects, except for maybe some dependencies (like Gson, Thymeleaf, etc.) which I don't think should have any effect, especially since all security logic is the same. Only "path" changes in current project's response, depending on the scenario. – Janas Feb 01 '23 at 18:32
  • And i tried finding methods in my another project that match the response format, but I found none, which tells me that Spring takes care of it on its own. The question is, why doesn't it do that on this project?? I'm lost. Sorry for the late response. I tried following your suggestion, but got into a dead end, missing beans and whatnot. Seems that anything I do, the primary commence method (with @Override) overrides any damn thing I throw at it. I've thrown exceptions from inside JWT validation, trying to override the response, but I only got the other exception in the console. – Janas Feb 01 '23 at 18:36

0 Answers0