Repository: provectus/kafka-ui
Issue: Simple login doesn't work with SERVER_SERVLET_CONTEXT_PATH 
Labels: type/bug
scope/backend

**Describe the bug**
I have setup SERVER_SERVLET_CONTEXT_PATH as part of the deployment in Kubernetes. The ingress rule uses this URI. Everything works well if the spring AUTH ENABLED is not set. Once I enable the AUTH, the URL takes me to the default /login page instead of the $SERVER_SERVLET_CONTEXT_PATH/login page and fails to load.


Issue comments:
Hi, @asatsi thx for creating this issue. We'll try to fix it in the next minor version. 

Pull request review comments:
0. name suggestion EmptyPathAwareRedirectStrategy
1. please add a comment why we need this class
2. maybe extend DefaultServerRedirectStrategy with sendRedirect method override?

Patch that resolved the issue:
diff --git a/docker/kafka-ui-auth-context.yaml b/docker/kafka-ui-auth-context.yaml
new file mode 100644
index 00000000000..77d6fea4866
--- /dev/null
+++ b/docker/kafka-ui-auth-context.yaml
@@ -0,0 +1,59 @@
+---
+version: '2'
+services:
+
+  kafka-ui:
+    container_name: kafka-ui
+    image: provectuslabs/kafka-ui:latest
+    ports:
+      - 8080:8080
+    depends_on:
+      - zookeeper0
+      - kafka0
+    environment:
+      KAFKA_CLUSTERS_0_NAME: local
+      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092
+      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper0:2181
+      KAFKA_CLUSTERS_0_JMXPORT: 9997
+      SERVER_SERVLET_CONTEXT_PATH: /kafkaui
+      AUTH_ENABLED: "true"
+      SPRING_SECURITY_USER_NAME: admin
+      SPRING_SECURITY_USER_PASSWORD: pass
+
+  zookeeper0:
+    image: confluentinc/cp-zookeeper:5.2.4
+    environment:
+      ZOOKEEPER_CLIENT_PORT: 2181
+      ZOOKEEPER_TICK_TIME: 2000
+    ports:
+      - 2181:2181
+
+  kafka0:
+    image: confluentinc/cp-kafka:5.2.4
+    depends_on:
+      - zookeeper0
+    ports:
+      - 9092:9092
+      - 9997:9997
+    environment:
+      KAFKA_BROKER_ID: 1
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper0:2181
+      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka0:29092,PLAINTEXT_HOST://localhost:9092
+      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
+      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+      JMX_PORT: 9997
+      KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997
+
+  kafka-init-topics:
+    image: confluentinc/cp-kafka:5.2.4
+    volumes:
+       - ./message.json:/data/message.json
+    depends_on:
+      - kafka0
+    command: "bash -c 'echo Waiting for Kafka to be ready... && \
+               cub kafka-ready -b kafka0:29092 1 30 && \
+               kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-topics --create --topic first.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper0:2181 && \
+               kafka-console-producer --broker-list kafka0:29092 -topic second.users < /data/message.json'"
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java
index e9d897cf43b..aae652cd08a 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/Config.java
@@ -2,20 +2,53 @@
 
 import com.provectus.kafka.ui.model.JmxConnectionInfo;
 import com.provectus.kafka.ui.util.JmxPoolFactory;
+import java.util.Collections;
+import java.util.Map;
 import javax.management.remote.JMXConnector;
+import lombok.AllArgsConstructor;
 import org.apache.commons.pool2.KeyedObjectPool;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
 import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
+import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.server.reactive.ContextPathCompositeHandler;
+import org.springframework.http.server.reactive.HttpHandler;
 import org.springframework.jmx.export.MBeanExporter;
+import org.springframework.util.StringUtils;
 import org.springframework.util.unit.DataSize;
 import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
 
 @Configuration
+@AllArgsConstructor
 public class Config {
 
+  private final ApplicationContext applicationContext;
+
+  private final ServerProperties serverProperties;
+
+  @Bean
+  public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {
+
+    final String basePath = serverProperties.getServlet().getContextPath();
+
+    HttpHandler httpHandler = WebHttpHandlerBuilder
+        .applicationContext(this.applicationContext).build();
+
+    if (StringUtils.hasText(basePath)) {
+      Map<String, HttpHandler> handlersMap =
+          Collections.singletonMap(basePath, httpHandler);
+      return new ContextPathCompositeHandler(handlersMap);
+    }
+    return httpHandler;
+  }
+
+
   @Bean
   public KeyedObjectPool<JmxConnectionInfo, JMXConnector> pool() {
     var pool = new GenericKeyedObjectPool<>(new JmxPoolFactory());
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java
index 6e46b607bcb..0128110ab72 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CorsGlobalConfiguration.java
@@ -1,45 +1,58 @@
 package com.provectus.kafka.ui.config;
 
+import lombok.AllArgsConstructor;
+import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.StringUtils;
 import org.springframework.web.reactive.config.CorsRegistry;
-import org.springframework.web.reactive.config.EnableWebFlux;
 import org.springframework.web.reactive.config.WebFluxConfigurer;
 import org.springframework.web.reactive.function.server.RouterFunction;
 import org.springframework.web.reactive.function.server.RouterFunctions;
 import org.springframework.web.reactive.function.server.ServerResponse;
 
 @Configuration
-@EnableWebFlux
 @Profile("local")
+@AllArgsConstructor
 public class CorsGlobalConfiguration implements WebFluxConfigurer {
 
+  private final ServerProperties serverProperties;
+
   @Override
   public void addCorsMappings(CorsRegistry registry) {
     registry.addMapping("/**")
         .allowedOrigins("*")
         .allowedMethods("*")
         .allowedHeaders("*")
-        .allowCredentials(true);
+        .allowCredentials(false);
+  }
+
+  private String withContext(String pattern) {
+    final String basePath = serverProperties.getServlet().getContextPath();
+    if (StringUtils.hasText(basePath)) {
+      return basePath + pattern;
+    } else {
+      return pattern;
+    }
   }
 
   @Bean
   public RouterFunction<ServerResponse> cssFilesRouter() {
     return RouterFunctions
-        .resources("/static/css/**", new ClassPathResource("static/static/css/"));
+        .resources(withContext("/static/css/**"), new ClassPathResource("static/static/css/"));
   }
 
   @Bean
   public RouterFunction<ServerResponse> jsFilesRouter() {
     return RouterFunctions
-        .resources("/static/js/**", new ClassPathResource("static/static/js/"));
+        .resources(withContext("/static/js/**"), new ClassPathResource("static/static/js/"));
   }
 
   @Bean
   public RouterFunction<ServerResponse> mediaFilesRouter() {
     return RouterFunctions
-        .resources("/static/media/**", new ClassPathResource("static/static/media/"));
+        .resources(withContext("/static/media/**"), new ClassPathResource("static/static/media/"));
   }
 }
\ No newline at end of file
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java
index 6dce3b5e012..a74a3fa6a16 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CustomWebFilter.java
@@ -1,6 +1,5 @@
 package com.provectus.kafka.ui.config;
 
-import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.stereotype.Component;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
@@ -8,32 +7,25 @@
 import reactor.core.publisher.Mono;
 
 @Component
-
 public class CustomWebFilter implements WebFilter {
 
-  private final ServerProperties serverProperties;
-
-  public CustomWebFilter(ServerProperties serverProperties) {
-    this.serverProperties = serverProperties;
-  }
-
   @Override
   public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
-    String contextPath = serverProperties.getServlet().getContextPath() != null 
-        ? serverProperties.getServlet().getContextPath() : "";
 
-    final String path = exchange.getRequest().getURI().getPath().replaceAll("/$", "");
-    if (path.equals(contextPath) || path.startsWith(contextPath + "/ui")) {
-      return chain.filter(
-          exchange.mutate().request(exchange.getRequest().mutate().path("/index.html").build())
-              .build()
-      );
-    } else if (path.startsWith(contextPath)) {
+    final String basePath = exchange.getRequest().getPath().contextPath().value();
+
+    final String path = exchange.getRequest().getPath().pathWithinApplication().value();
+
+    if (path.startsWith("/ui") || path.equals("/")) {
       return chain.filter(
-          exchange.mutate().request(exchange.getRequest().mutate().contextPath(contextPath).build())
-              .build()
+          exchange.mutate().request(
+              exchange.getRequest().mutate()
+                  .path(basePath + "/index.html")
+                  .contextPath(basePath)
+                  .build()
+          ).build()
       );
-    }    
+    }
 
     return chain.filter(exchange);
   }
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/OAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/OAuthSecurityConfig.java
index 3e8bcd478f2..4d1a16860fb 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/OAuthSecurityConfig.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/OAuthSecurityConfig.java
@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.config;
 
-import org.springframework.beans.factory.annotation.Autowired;
+import com.provectus.kafka.ui.util.EmptyRedirectStrategy;
+import lombok.AllArgsConstructor;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
@@ -8,18 +9,20 @@
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
 import org.springframework.util.ClassUtils;
 
 @Configuration
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.enabled", havingValue = "true")
+@AllArgsConstructor
 public class OAuthSecurityConfig {
 
   public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME =
       "org.springframework.security.oauth2.client.registration."
           + "ReactiveClientRegistrationRepository";
 
-  private static final boolean isOAuth2Present = ClassUtils.isPresent(
+  private static final boolean IS_OAUTH2_PRESENT = ClassUtils.isPresent(
       REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
       OAuthSecurityConfig.class.getClassLoader()
   );
@@ -31,37 +34,45 @@ public class OAuthSecurityConfig {
       "/resources/**",
       "/actuator/health",
       "/actuator/info",
+      "/auth",
       "/login",
       "/logout",
       "/oauth2/**"
   };
 
-  @Autowired
-  ApplicationContext context;
+  private final ApplicationContext context;
 
   @Bean
   public SecurityWebFilterChain configure(ServerHttpSecurity http) {
     http.authorizeExchange()
-        .pathMatchers(AUTH_WHITELIST).permitAll()
+        .pathMatchers(
+            AUTH_WHITELIST
+        ).permitAll()
         .anyExchange()
         .authenticated();
 
-    if (isOAuth2Present && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
+    if (IS_OAUTH2_PRESENT && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
       OAuth2ClasspathGuard.configure(this.context, http);
     } else {
+      final RedirectServerAuthenticationSuccessHandler handler =
+          new RedirectServerAuthenticationSuccessHandler();
+      handler.setRedirectStrategy(new EmptyRedirectStrategy());
+
       http
           .httpBasic().and()
-          .formLogin();
+          .formLogin()
+          .loginPage("/auth")
+          .authenticationSuccessHandler(handler);
     }
 
-    SecurityWebFilterChain result = http.csrf().disable().build();
-    return result;
+    return http.csrf().disable().build();
   }
 
   private static class OAuth2ClasspathGuard {
     static void configure(ApplicationContext context, ServerHttpSecurity http) {
       http
-          .oauth2Login().and()
+          .oauth2Login()
+          .and()
           .oauth2Client();
     }
 
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java
index 35e0f8397bf..b998748fccb 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/ReadOnlyModeFilter.java
@@ -31,7 +31,7 @@ public Mono<Void> filter(ServerWebExchange exchange, @NotNull WebFilterChain cha
       return chain.filter(exchange);
     }
 
-    var path = exchange.getRequest().getURI().getPath();
+    var path = exchange.getRequest().getPath().pathWithinApplication().value();
     var matcher = CLUSTER_NAME_REGEX.matcher(path);
     if (!matcher.find()) {
       return chain.filter(exchange);
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java
new file mode 100644
index 00000000000..b847e58f990
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AuthController.java
@@ -0,0 +1,100 @@
+package com.provectus.kafka.ui.controller;
+
+import java.nio.charset.Charset;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.security.web.server.csrf.CsrfToken;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequiredArgsConstructor
+@Log4j2
+public class AuthController {
+
+  @GetMapping(value = "/auth", produces = { "text/html" })
+  private Mono<byte[]> getAuth(ServerWebExchange exchange) {
+    Mono<CsrfToken> token = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
+    return token
+        .map(AuthController::csrfToken)
+        .defaultIfEmpty("")
+        .map(csrfTokenHtmlInput -> createPage(exchange, csrfTokenHtmlInput));
+  }
+
+  private byte[] createPage(ServerWebExchange exchange, String csrfTokenHtmlInput) {
+    MultiValueMap<String, String> queryParams = exchange.getRequest()
+        .getQueryParams();
+    String contextPath = exchange.getRequest().getPath().contextPath().value();
+    String page =
+        "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + "  <head>\n"
+        + "    <meta charset=\"utf-8\">\n"
+        + "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, "
+        + "shrink-to-fit=no\">\n"
+        + "    <meta name=\"description\" content=\"\">\n"
+        + "    <meta name=\"author\" content=\"\">\n"
+        + "    <title>Please sign in</title>\n"
+        + "    <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/"
+        + "4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" "
+        + "integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" "
+        + "crossorigin=\"anonymous\">\n"
+        + "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" "
+        + "rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
+        + "  </head>\n"
+        + "  <body>\n"
+        + "     <div class=\"container\">\n"
+        + formLogin(queryParams, contextPath, csrfTokenHtmlInput)
+        + "    </div>\n"
+        + "  </body>\n"
+        + "</html>";
+
+    return page.getBytes(Charset.defaultCharset());
+  }
+
+  private String formLogin(
+      MultiValueMap<String, String> queryParams,
+      String contextPath, String csrfTokenHtmlInput) {
+
+    boolean isError = queryParams.containsKey("error");
+    boolean isLogoutSuccess = queryParams.containsKey("logout");
+    return
+        "      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + "/auth\">\n"
+        + "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
+        + createError(isError)
+        + createLogoutSuccess(isLogoutSuccess)
+        + "        <p>\n"
+        + "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
+        + "          <input type=\"text\" id=\"username\" name=\"username\" class=\"form-control\" "
+        + "placeholder=\"Username\" required autofocus>\n"
+        + "        </p>\n" + "        <p>\n"
+        + "          <label for=\"password\" class=\"sr-only\">Password</label>\n"
+        + "          <input type=\"password\" id=\"password\" name=\"password\" "
+        + "class=\"form-control\" placeholder=\"Password\" required>\n"
+        + "        </p>\n" + csrfTokenHtmlInput
+        + "        <button class=\"btn btn-lg btn-primary btn-block\" "
+        + "type=\"submit\">Sign in</button>\n"
+        + "      </form>\n";
+  }
+
+  private static String csrfToken(CsrfToken token) {
+    return "          <input type=\"hidden\" name=\""
+        + token.getParameterName()
+        + "\" value=\""
+        + token.getToken()
+        + "\">\n";
+  }
+
+  private static String createError(boolean isError) {
+    return isError
+        ? "<div class=\"alert alert-danger\" role=\"alert\">Invalid credentials</div>"
+        : "";
+  }
+
+  private static String createLogoutSuccess(boolean isLogoutSuccess) {
+    return isLogoutSuccess
+        ? "<div class=\"alert alert-success\" role=\"alert\">You have been signed out</div>"
+        : "";
+  }
+}
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
index 2b48d53a7a9..f8278701c06 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/StaticController.java
@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.controller;
 
 import com.provectus.kafka.ui.util.ResourceUtil;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicReference;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
@@ -11,27 +12,27 @@
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 
 @RestController
 @RequiredArgsConstructor
 @Log4j2
 public class StaticController {
-  private final ServerProperties serverProperties;
 
   @Value("classpath:static/index.html")
   private Resource indexFile;
   private final AtomicReference<String> renderedIndexFile = new AtomicReference<>();
 
   @GetMapping(value = "/index.html", produces = { "text/html" })
-  public Mono<ResponseEntity<String>> getIndex() {
-    return Mono.just(ResponseEntity.ok(getRenderedIndexFile()));
+  public Mono<ResponseEntity<String>> getIndex(ServerWebExchange exchange) {
+    return Mono.just(ResponseEntity.ok(getRenderedIndexFile(exchange)));
   }
 
-  public String getRenderedIndexFile() {
+  public String getRenderedIndexFile(ServerWebExchange exchange) {
     String rendered = renderedIndexFile.get();
     if (rendered == null) {
-      rendered = buildIndexFile();
+      rendered = buildIndexFile(exchange.getRequest().getPath().contextPath().value());
       if (renderedIndexFile.compareAndSet(null, rendered)) {
         return rendered;
       } else {
@@ -43,9 +44,7 @@ public String getRenderedIndexFile() {
   }
 
   @SneakyThrows
-  private String buildIndexFile() {
-    final String contextPath = serverProperties.getServlet().getContextPath() != null
-        ? serverProperties.getServlet().getContextPath() : "";
+  private String buildIndexFile(String contextPath) {
     final String staticPath = contextPath + "/static";
     return ResourceUtil.readAsString(indexFile)
         .replace("href=\"./static", "href=\"" + staticPath)
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/EmptyRedirectStrategy.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/EmptyRedirectStrategy.java
new file mode 100644
index 00000000000..39a4a7f2eb2
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/EmptyRedirectStrategy.java
@@ -0,0 +1,50 @@
+package com.provectus.kafka.ui.util;
+
+import java.net.URI;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.web.server.ServerRedirectStrategy;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+public class EmptyRedirectStrategy implements ServerRedirectStrategy {
+
+  private HttpStatus httpStatus = HttpStatus.FOUND;
+
+  private boolean contextRelative = true;
+
+  public Mono<Void> sendRedirect(ServerWebExchange exchange, URI location) {
+    Assert.notNull(exchange, "exchange cannot be null");
+    Assert.notNull(location, "location cannot be null");
+    return Mono.fromRunnable(() -> {
+      ServerHttpResponse response = exchange.getResponse();
+      response.setStatusCode(this.httpStatus);
+      response.getHeaders().setLocation(createLocation(exchange, location));
+    });
+  }
+
+  private URI createLocation(ServerWebExchange exchange, URI location) {
+    if (!this.contextRelative) {
+      return location;
+    }
+
+    String url = location.getPath().isEmpty() ? "/"
+        : location.toASCIIString();
+
+    if (url.startsWith("/")) {
+      String context = exchange.getRequest().getPath().contextPath().value();
+      return URI.create(context + url);
+    }
+    return location;
+  }
+
+  public void setHttpStatus(HttpStatus httpStatus) {
+    Assert.notNull(httpStatus, "httpStatus cannot be null");
+    this.httpStatus = httpStatus;
+  }
+
+  public void setContextRelative(boolean contextRelative) {
+    this.contextRelative = contextRelative;
+  }
+}


Test patch: