mnagdev
mnagdev

Reputation: 514

How to handle custom exceptions thrown by a filter in Spring Security

I am new to Spring Security.

I have a piece of code where I check if an Authorization header is passed in a request and I throw an exception if it's missing.

public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final String BEARER = "Bearer";

    public TokenAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        super(requiresAuthenticationRequestMatcher);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        String username = request.getParameter("username");
        String authorization = request.getHeader("AUTHORIZATION");

        if (!request.getRequestURI().equals(UniversalConstants.LOGIN_PATH)) {
            if (authorization == null || authorization.length() == 0 || !authorization.startsWith(BEARER)) {
                throw new InvalidCredentialsException("Missing authentication token"); //<-----------------
            }

        }

        String password = request.getParameter("password");
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

I am targeting to handle all exceptions globally so I'm using @ControllerAdvice.

Note: I know that @ControllerAdvice will not work for exceptions thrown outside of Controllers from this and this, so I have also followed the suggestions in these links.

RestAuthenticationEntryPoint.java

@Component("restAuthenticationEntryPoint")
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    public RestAuthenticationEntryPoint() {
        System.out.println("RestAuthenticationEntryPoint");
    }

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        resolver.resolveException(request, response, null, authException);

    }

}

This is how I configure the authenticationEntryPoint:

@Override
protected void configure(HttpSecurity http) throws Exception {
  
    http.exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint()).and().cors().and().csrf().disable().exceptionHandling().defaultAuthenticationEntryPointFor(new RestAuthenticationEntryPoint(), PROTECTED_URLS)
            .and().authenticationProvider(customAuthenticationProvider())
            .addFilterBefore(tokenAuthenticationFilter(), AnonymousAuthenticationFilter.class).authorizeRequests()
            .requestMatchers(PROTECTED_URLS).authenticated().and().formLogin().disable().httpBasic().disable();

}

CustomExceptionHandler.java

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({InvalidCredentialsException.class, AuthenticationException.class})
    public ResponseEntity<ErrorResponse> handleUnauthorizedError(InvalidCredentialsException e, WebRequest request) {
        String errorMessage = e.getLocalizedMessage();
        ErrorResponse errorResponse = new ErrorResponse(errorMessage, null);
        return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
    }
}

InvalidCredentialsException.java

@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class InvalidCredentialsException extends RuntimeException {

    public InvalidCredentialsException(String errorMessage) {
        super(errorMessage);
    }

}

Upon debugging, I've found that the resolver.resolveException(...) in RestAuthenticationEntryPoint and the handleUnauthorizedError(..) in CustomExceptionHandler never get called.

I wish to handle throw new InvalidCredentialsException("Missing authentication token") in an elegant way and show a decent JSON output in the response. Any help would be appreciated.

Edit: The stack trace

2021-05-20 17:41:29.985 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/public/**']
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/user/hello'; against '/public/**'
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/error**']
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/user/hello'; against '/error**'
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
2021-05-20 17:41:29.986 DEBUG 24808 --- [nio-8181-exec-3] o.s.security.web.FilterChainProxy        : /user/hello?username=user&password=user at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2021-05-20 17:41:29.988 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Request 'GET /user/hello' doesn't match 'DELETE /logout'
2021-05-20 17:41:29.988 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
2021-05-20 17:41:29.988 DEBUG 24808 --- [nio-8181-exec-3] o.s.security.web.FilterChainProxy        : /user/hello?username=user&password=user at position 6 of 12 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.s.HttpSessionRequestCache        : saved request doesn't match
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.security.web.FilterChainProxy        : /user/hello?username=user&password=user at position 7 of 12 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.security.web.FilterChainProxy        : /user/hello?username=user&password=user at position 8 of 12 in additional filter chain; firing Filter: 'TokenAuthenticationFilter'
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/public/**']
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/user/hello'; against '/public/**'
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
2021-05-20 17:41:29.989 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.NegatedRequestMatcher  : matches = true
2021-05-20 17:41:38.030 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@7fb6b4e0
2021-05-20 17:41:38.030 DEBUG 24808 --- [nio-8181-exec-3] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2021-05-20 17:41:38.030 DEBUG 24808 --- [nio-8181-exec-3] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2021-05-20 17:41:38.033 ERROR 24808 --- [nio-8181-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

com.spring.fieldSecurity.Exceptions.InvalidCredentialsException: Missing authentication token
    at com.spring.fieldSecurity.Service.TokenAuthenticationFilter.attemptAuthentication(TokenAuthenticationFilter.java:44) ~[classes/:na]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) ~[spring-security-web-5.3.3.RELEASE.jar:5.3.3.RELEASE]
.
.   // more error trace here
.
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/public/**']
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/error'; against '/public/**'
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : No matches found
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : Trying to match using Ant [pattern='/error**']
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/error'; against '/error**'
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.s.web.util.matcher.OrRequestMatcher  : matched
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.security.web.FilterChainProxy        : /error?username=user&password=user has an empty filter list
2021-05-20 17:41:38.034 DEBUG 24808 --- [nio-8181-exec-3] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error?username=user&password=user", parameters={masked}
2021-05-20 17:41:38.035 DEBUG 24808 --- [nio-8181-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2021-05-20 17:41:38.035 DEBUG 24808 --- [nio-8181-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
2021-05-20 17:41:38.724 DEBUG 24808 --- [nio-8181-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [application/json] and supported [application/json, application/*+json, application/json, application/*+json]
2021-05-20 17:41:38.724 DEBUG 24808 --- [nio-8181-exec-3] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Thu May 20 17:41:38 IST 2021, status=500, error=Internal Server Error, message=, path=/us (truncated)...]
2021-05-20 17:41:38.726 DEBUG 24808 --- [nio-8181-exec-3] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
2021-05-20 17:41:38.727 DEBUG 24808 --- [nio-8181-exec-3] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

Upvotes: 4

Views: 9524

Answers (1)

Toerktumlare
Toerktumlare

Reputation: 14732

Spring security has a filter which is called the ExceptionTranslationFilter which translates AccessDeniedException and AuthenticationException into responses. This filter catches these thrown exceptions in the spring security filter chain.

So if you want to return a custom exception, you could instead inherit from one of these classes instead of RuntimeException and add a custom message.

I just want to emphasis and it can never be said too many times:

Providing friendly error messages in production applications when it comes to authentication/authorization is in general bad practice from a security standpoint. These types of messages can benefit malicious actors, when trying out things so that they realize what they have done wrong and guide them in their hacking attempts.

Providing friendly messages in test environments may be okey, but make sure that they are disabled in production. In production all failed authentication attempts a recommendation is to return a 401 with no additional information. And in graphical clients, generalized error messages should be displayed for instance "failed to authenticate" with no given specifics.

Also:

Writing custom security as you have done is also in general bad practice. Spring security is battle tested with 100000 of applications running it in production environments. Writing a custom filter to handle token and passwords, is in general not needed. Spring security already has implemented filters to handle security and authentication using standards like BASIC authentication and TOKEN/JWT. If you implement a non standard login, one bug might expose your application to a huge risk.

Upvotes: 8

Related Questions