12
votes

I'm building app on spring webflux, and i'm stuck because spring security webflux (v.M5) did not behave like Spring 4 in term of exception handling.

I saw following post about how to customise spring security webflux: Spring webflux custom authentication for API

If we throw exception let say in ServerSecurityContextRepository.load, Spring will update http header to 500 and nothing i can do to manipulate this exception.

However, any error thrown in controller can be handled using regular @ControllerAdvice, it just spring webflux security.

Is there anyway to handle exception in spring webflux security?

3

3 Answers

10
votes

The solution I found is creating a component implementing ErrorWebExceptionHandler. The instances of ErrorWebExceptionHandler bean run before Spring Security filters. Here's a sample that I use:

@Slf4j
@Component
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

  @Autowired
  private DataBufferWriter bufferWriter;

  @Override
  public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
    HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
    AppError appError = ErrorCode.GENERIC.toAppError();

    if (ex instanceof AppException) {
        AppException ae = (AppException) ex;
        status = ae.getStatusCode();
        appError = new AppError(ae.getCode(), ae.getText());

        log.debug(appError.toString());
    } else {
        log.error(ex.getMessage(), ex);
    }

    if (exchange.getResponse().isCommitted()) {
        return Mono.error(ex);
    }

    exchange.getResponse().setStatusCode(status);
    return bufferWriter.write(exchange.getResponse(), appError);
  }
}

If you're injecting the HttpHandler instead, then it's a bit different but the idea is the same.

UPDATE: For completeness, here's my DataBufferWriter object, which is a @Component:

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class DataBufferWriter {
    private final ObjectMapper objectMapper;

    public <T> Mono<Void> write(ServerHttpResponse httpResponse, T object) {
        return httpResponse
            .writeWith(Mono.fromSupplier(() -> {
                DataBufferFactory bufferFactory = httpResponse.bufferFactory();
                try {
                    return bufferFactory.wrap(objectMapper.writeValueAsBytes(object));
                } catch (Exception ex) {
                    log.warn("Error writing response", ex);
                    return bufferFactory.wrap(new byte[0]);
                }
            }));
    }
}
5
votes

There is no need to register any bean and change default Spring behavior. Try more elegant solution instead:

We have:

  1. The custom implementation of the ServerSecurityContextRepository
  2. The method .load return Mono

    public class HttpRequestHeaderSecurityContextRepository implements ServerSecurityContextRepository {
        ....
        @Override
        public Mono<SecurityContext> load(ServerWebExchange exchange) {
            List<String> tokens = exchange.getRequest().getHeaders().get("X-Auth-Token");
            String token = (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;
    
            Mono<Authentication> authMono = reactiveAuthenticationManager
                    .authenticate( new HttpRequestHeaderToken(token) );
    
            return authMono
                    .map( auth -> (SecurityContext)new SecurityContextImpl(auth))
        }
    

    }

The problem is: if the authMono will contains an error instead of Authentication - spring will return the http response with 500 status (which means "an unknown internal error") instead of 401. Even the error is AuthenticationException or it's subclass - it makes no sense - Spring will return 500.

But it is clear for us: an AuthenticationException should produce the 401 error...

To solve the problem we have to help Spring how to convert an Exception into the HTTP response status code.

To make it we have can just use the appropriate Exception class: ResponseStatusException or just map an original exception to this one (for instance, by adding the onErrorMap() to the authMono object). See the final code:

    public class HttpRequestHeaderSecurityContextRepository implements ServerSecurityContextRepository {
        ....
        @Override
        public Mono<SecurityContext> load(ServerWebExchange exchange) {
            List<String> tokens = exchange.getRequest().getHeaders().get("X-Auth-Token");
            String token = (tokens != null && !tokens.isEmpty()) ? tokens.get(0) : null;

            Mono<Authentication> authMono = reactiveAuthenticationManager
                    .authenticate( new HttpRequestHeaderToken(token) );

            return authMono
                    .map( auth -> (SecurityContext)new SecurityContextImpl(auth))
                    .onErrorMap(
                            er -> er instanceof AuthenticationException,
                            autEx -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, autEx.getMessage(), autEx)
                    )
                ;
            )
        }
   }
4
votes

I just went trough lots of documentation, having a similar problem.

My solution was using ResponseStatusException. AccessException of Spring-security seems to be understood.

.doOnError(
          t -> AccessDeniedException.class.isAssignableFrom(t.getClass()),
          t -> AUDIT.error("Error {} {}, tried to access {}", t.getMessage(), principal, exchange.getRequest().getURI())) // if an error happens in the stream, show its message
.onErrorMap(
        SomeOtherException.class, 
        t -> { return new ResponseStatusException(HttpStatus.NOT_FOUND,  "Collection not found");})
      ;

If this goes in the right direction for you, I can provide a bit better sample.