14

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?

Mistletoe
  • 235
  • 1
  • 3
  • 11

3 Answers3

14

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]);
                }
            }));
    }
}
MuratOzkan
  • 2,599
  • 13
  • 25
  • How do you get the DataBufferWritter in this case? – Thomas Sep 03 '18 at 14:00
  • httpResponse.writeWith was the only variant that worked for me, allowing to customize message on configuration level. – Ermintar Jul 04 '19 at 10:15
  • this is the correct answer, you can find a complete example code in https://github.com/eriknyk/webflux-jwt-security-demo – erik.aortiz Apr 09 '21 at 20:00
  • I tried that and here is the CORS error I am getting: Access to XMLHttpRequest at 'http://localhost:8084/users/files' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. – user3648235 Jun 14 '21 at 17:24
6

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)
                    )
                ;
            )
        }
   }
user1726919
  • 83
  • 1
  • 6
5

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.

Frischling
  • 2,100
  • 14
  • 34
  • This is by far the simplest solution and works like a charm for me. Thanks! – Freya Jan 23 '19 at 17:39
  • How do you map any generic exception without relying in ControllerAdvice? https://www.baeldung.com/spring-webflux-errors I was trying this one but it does not work with spring boot webflux 2.3.2.RELEASE – Coder Aug 07 '20 at 00:53
  • Sorry, don't know, but I think that's worth another question... – Frischling Aug 17 '20 at 06:38