question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add component to handle Authentication filter exceptions

See original GitHub issue

Detailed Description

To complement the Problem mapping capability of SecurityProblemSupport, a custom AuthenticationFailureHandler should be added to ensure exceptions in the security filter chain are also returned as proper RFC7807 responses.

The AuthenticationFailureHandler strategy can be used to modify the behaviour of spring security. When using spring boot as a resource server for APIs (not web pages), the out-of-the-box AuthenticationFailureHandlers are no sufficiently configurable to ensure the failure is written to the response in RFC7807 format. There are a few documented hacks out there that sort of get you half way but since this is part of a security system, probably not a good idea to copy-and-paste a bunch of car insurance quote examples together to return auth errors in json format.

Context

We use this AuthenticationFailureHandler together with the SecurityProblemSupport to setup spring boot APIs in a way that all responses are RFC7807 compliant. Other users might find it convenient to use an API optimised failure handler instead trying to make the spring supplied ones do something they are not designed to do.

If you think it’s worthwhile, I’ll throw in the unit tests for free 😉 .

Possible Implementation

We use something like this to close the gap:

package org.zalando.problem.spring.web.advice.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.client.RestTemplate;
import org.zalando.problem.Problem;
import org.zalando.problem.Status;
import org.zalando.problem.spring.web.advice.AdviceTrait;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.stream.Collectors;

import static java.util.Collections.*;
import static java.util.Optional.ofNullable;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR;
import static org.zalando.problem.Status.UNAUTHORIZED;

@Component
public class ProblemAuthenticationFailureHandler implements AuthenticationFailureHandler, AdviceTrait {

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

  private MediaType defaultMediaType = APPLICATION_JSON;

  private List<HttpMessageConverter<?>> messageConverters = new RestTemplate().getMessageConverters();

  /**
   * If the client did not indicate which media type to use in the {@link HttpHeaders#ACCEPT} header,
   * this value will be used as the default to determine the response content type.
   *
   * @param defaultMediaType the media type to be used as default
   */
  public void setDefaultMediaType(MediaType defaultMediaType) {
    this.defaultMediaType = defaultMediaType;
  }

  /**
   * The message converters are used to write the authentication exception error to the response.
   *
   * @param messageConverters message converters available in the application context
   */
  @Autowired
  public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    this.messageConverters = messageConverters;
  }

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {

    request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);

    if (response.isCommitted()) {
      logger.error("Response is already committed. Unable to write exception {}: {}", exception.getClass().getSimpleName(), exception.getLocalizedMessage());
      return;
    }

    // important: not all AuthenticationException(s) are client side failures
    Status status = (exception instanceof AuthenticationServiceException) ? INTERNAL_SERVER_ERROR : UNAUTHORIZED;

    Problem problem = toProblem(exception, status);

    try {
      response.setStatus(status.getStatusCode());
      // TODO add spring sec headers
      internalWrite(request, response, problem, defaultMediaType);
    } catch (IOException e) {
      if (logger.isWarnEnabled()) {
        logger.error("Failed to write error response", e);
      }
    } catch (HttpMediaTypeNotAcceptableException e) {
      response.setStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value());
    }

  }

  /**
   * This process will extract the acceptable response content types from the request and sorts these in order of the
   * optional quality designator.
   *
   * @param request          the servlet request for extracting the accept headers
   * @param response         the servlet response to write the entity to
   * @param problem          the entity to write to the response
   * @param defaultMediaType the default media type should no accept headers be provided
   * @throws IOException                         something went wrong writing the entity to the response
   * @throws HttpMediaTypeNotAcceptableException the requested media type to respond with cannot be honoured
   */
  private void internalWrite(HttpServletRequest request, HttpServletResponse response, Problem problem, MediaType defaultMediaType) throws IOException, HttpMediaTypeNotAcceptableException {

    Enumeration<String> acceptedHeaderValues = ofNullable(request.getHeaders(HttpHeaders.ACCEPT))
      .orElse(enumeration(singletonList(defaultMediaType.toString())));

    List<MediaType> acceptedMediaTypes = list(acceptedHeaderValues).stream()
      .flatMap(x -> Arrays.stream(x.split(",")))
      .map(MediaType::parseMediaType)
      .collect(Collectors.toList()).stream()
      .sorted(MediaType.QUALITY_VALUE_COMPARATOR)
      .collect(Collectors.toList());

    for (MediaType acceptedMediaType : acceptedMediaTypes) {
      if (write(problem, acceptedMediaType, response)) {
        return;
      }
    }

    // nothing written clearly we do not support requested content type
    throw new HttpMediaTypeNotAcceptableException(messageConverters.stream()
      .map(HttpMessageConverter::getSupportedMediaTypes)
      .flatMap(List::stream)
      .collect(Collectors.toList())
    );

  }

  /**
   * Attempt to write the problem entity to the response for the given media type.
   *
   * @param problem           the response entity to write
   * @param acceptedMediaType the media type accepted by the caller (in quality order)
   * @param response          the servlet response to write the message to
   * @return <code>true</code> when the entity was written to the response successfully, <code>false</code> otherwise
   * @throws IOException something went wrong writing the entity to the response
   */
  @SuppressWarnings("unchecked")
  private boolean write(Problem problem, MediaType acceptedMediaType, HttpServletResponse response) throws IOException {

    for (HttpMessageConverter messageConverter : messageConverters) {
      if (messageConverter.canWrite(problem.getClass(), acceptedMediaType)) {
        messageConverter.write(problem, acceptedMediaType, new ServletServerHttpResponse(response));
        return true;
      }
    }

    return false;

  }

}

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:4
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
chicobentocommented, Sep 17, 2021

Hi, stumble upon this issue when integrating with keycloak-adapter. Keycloak configures by default its on KeycloakAuthenticationFailureHandler.

I got around the issue by doing something like:

@Bean
public AuthenticationFailureHandler problemAuthenticationFailureHandler(final SecurityProblemSupport securityProblem) {
        return securityProblem::commence;
}

I wonder if SecurityProblemSupport shouldn’t implement AuthenticationFailureHandler exactly as it does for AuthenticationEntryPoint.

0reactions
whiskeysierracommented, Sep 17, 2021

@chicobento Probably makes sense, yes.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Handle spring security authentication exceptions with ...
The best way I've found is to delegate the exception to the HandlerExceptionResolver @Component("restAuthenticationEntryPoint") public class ...
Read more >
Handle Spring Security Exceptions With @ExceptionHandler
Spring security exceptions can be directly handled by adding custom filters and constructing the response body. To handle these exceptions ...
Read more >
Filters in ASP.NET Core | Microsoft Learn
Do not throw exceptions within authorization filters: The exception will not be handled. Exception filters will not handle the exception.
Read more >
Exception Handling In Spring Security | DevGlan
Hence, it is required to insert a custom filter (RestAccessDeniedHandler and RestAuthenticationEntryPoint) earlier in the chain to catch the ...
Read more >
Spring Security Before Authentication Filter Examples
This custom filter will override all the existing configurations for login success handler, login failure handler and logout success handler.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found