Repository: provectus/kafka-ui
Issue: Cognito logout
Labels: type/enhancement
scope/backend
status/accepted


Issue comments:

Pull request review comments:
It's a bad idea to have provider-dependent classes. Could we just add a configurable URL for logout?
I don't like the idea to have such configs.
Not really, this one is required since cognito doesn't work like others oauth providers, this custom logic is required. We have discussed this previously within the same feature in ODD.
The same as in ODD as well, though. What do you suggest? We don't have any other markers to identify if the user's using cognito or not besides this introduced variable.
done
looks dirty
extracted a props bean
done

Patch that resolved the issue:
diff --git a/documentation/compose/DOCKER_COMPOSE.md b/documentation/compose/DOCKER_COMPOSE.md
index c5fc1f1476c..d019427d747 100644
--- a/documentation/compose/DOCKER_COMPOSE.md
+++ b/documentation/compose/DOCKER_COMPOSE.md
@@ -11,3 +11,4 @@
 9. [kafka-ui-reverse-proxy.yaml](./kafka-ui-reverse-proxy.yaml) - An example for using the app behind a proxy (like nginx).
 10. [kafka-ui-sasl.yaml](./kafka-ui-sasl.yaml) - SASL auth for Kafka.
 11. [kafka-ui-traefik-proxy.yaml](./kafka-ui-traefik-proxy.yaml) - Traefik specific proxy configuration.
+12. [oauth-cognito.yaml](./oauth-cognito.yaml) - OAuth2 with Cognito
diff --git a/documentation/compose/oauth-cognito.yaml b/documentation/compose/oauth-cognito.yaml
new file mode 100644
index 00000000000..a905c9c2ee5
--- /dev/null
+++ b/documentation/compose/oauth-cognito.yaml
@@ -0,0 +1,22 @@
+---
+version: '3.4'
+services:
+
+  kafka-ui:
+    container_name: kafka-ui
+    image: provectuslabs/kafka-ui:local
+    ports:
+      - 8080:8080
+    depends_on:
+      - kafka0 # OMITTED, TAKE UP AN EXAMPLE FROM OTHER COMPOSE FILES
+    environment:
+      KAFKA_CLUSTERS_0_NAME: local
+      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL
+      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka0:29092
+      AUTH_TYPE: OAUTH2_COGNITO
+      AUTH_COGNITO_ISSUER_URI: "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-xxxxxx"
+      AUTH_COGNITO_CLIENT_ID: ""
+      AUTH_COGNITO_CLIENT_SECRET: ""
+      AUTH_COGNITO_SCOPE: "openid"
+      AUTH_COGNITO_USER_NAME_ATTRIBUTE: "username"
+      AUTH_COGNITO_LOGOUT_URI: "https://<domain>.auth.eu-central-1.amazoncognito.com/logout"
\ No newline at end of file
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CognitoOidcLogoutSuccessHandler.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CognitoOidcLogoutSuccessHandler.java
new file mode 100644
index 00000000000..52e366f3abf
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CognitoOidcLogoutSuccessHandler.java
@@ -0,0 +1,53 @@
+package com.provectus.kafka.ui.config;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.web.server.WebSession;
+import org.springframework.web.util.UriComponents;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+
+@RequiredArgsConstructor
+public class CognitoOidcLogoutSuccessHandler implements ServerLogoutSuccessHandler {
+
+  private final String logoutUrl;
+  private final String clientId;
+
+  @Override
+  public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange, final Authentication authentication) {
+    final ServerHttpResponse response = exchange.getExchange().getResponse();
+    response.setStatusCode(HttpStatus.FOUND);
+
+    final var requestUri = exchange.getExchange().getRequest().getURI();
+
+    final var fullUrl = UrlUtils.buildFullRequestUrl(requestUri.getScheme(),
+        requestUri.getHost(), requestUri.getPort(),
+        requestUri.getPath(), requestUri.getQuery());
+
+    final UriComponents baseUrl = UriComponentsBuilder
+        .fromHttpUrl(fullUrl)
+        .replacePath("/")
+        .replaceQuery(null)
+        .fragment(null)
+        .build();
+
+    final var uri = UriComponentsBuilder
+        .fromUri(URI.create(logoutUrl))
+        .queryParam("client_id", clientId)
+        .queryParam("logout_uri", baseUrl)
+        .encode(StandardCharsets.UTF_8)
+        .build()
+        .toUri();
+
+    response.getHeaders().setLocation(uri);
+    return exchange.getExchange().getSession().flatMap(WebSession::invalidate);
+  }
+}
+
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java
index 6bd56a877fe..ae98dfdd7a3 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java
@@ -2,7 +2,7 @@
 
 import com.provectus.kafka.ui.util.EmptyRedirectStrategy;
 import java.net.URI;
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -17,7 +17,7 @@
 @Configuration
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "LOGIN_FORM")
-@Log4j2
+@Slf4j
 public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
   public static final String LOGIN_URL = "/auth";
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/CognitoOAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/CognitoOAuthSecurityConfig.java
new file mode 100644
index 00000000000..9db66e142d7
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/CognitoOAuthSecurityConfig.java
@@ -0,0 +1,80 @@
+package com.provectus.kafka.ui.config.auth;
+
+import com.provectus.kafka.ui.config.CognitoOidcLogoutSuccessHandler;
+import com.provectus.kafka.ui.config.auth.props.CognitoProperties;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrations;
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+
+@Configuration
+@EnableWebFluxSecurity
+@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2_COGNITO")
+@RequiredArgsConstructor
+@Slf4j
+public class CognitoOAuthSecurityConfig extends AbstractAuthSecurityConfig {
+
+  private static final String COGNITO = "cognito";
+
+  @Bean
+  public SecurityWebFilterChain configure(ServerHttpSecurity http, CognitoProperties props) {
+    log.info("Configuring Cognito OAUTH2 authentication.");
+
+    String clientId = props.getClientId();
+    String logoutUrl = props.getLogoutUri();
+
+    final ServerLogoutSuccessHandler logoutHandler = new CognitoOidcLogoutSuccessHandler(logoutUrl, clientId);
+
+    return http.authorizeExchange()
+        .pathMatchers(AUTH_WHITELIST)
+        .permitAll()
+        .anyExchange()
+        .authenticated()
+
+        .and()
+        .oauth2Login()
+
+        .and()
+        .oauth2Client()
+
+        .and()
+        .logout()
+        .logoutSuccessHandler(logoutHandler)
+
+        .and()
+        .csrf().disable()
+        .build();
+  }
+
+  @Bean
+  public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository(CognitoProperties props) {
+    ClientRegistration.Builder builder = ClientRegistrations
+        .fromIssuerLocation(props.getIssuerUri())
+        .registrationId(COGNITO);
+
+    builder.clientId(props.getClientId());
+    builder.clientSecret(props.getClientSecret());
+
+    Optional.ofNullable(props.getScope()).ifPresent(builder::scope);
+    Optional.ofNullable(props.getUserNameAttribute()).ifPresent(builder::userNameAttributeName);
+
+    return new InMemoryReactiveClientRegistrationRepository(builder.build());
+  }
+
+  @Bean
+  @ConfigurationProperties("auth.cognito")
+  public CognitoProperties cognitoProperties() {
+    return new CognitoProperties();
+  }
+
+}
\ No newline at end of file
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java
index d30aa4631bd..4b1cc9a9330 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/DisabledAuthSecurityConfig.java
@@ -1,6 +1,6 @@
 package com.provectus.kafka.ui.config.auth;
 
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.ApplicationContext;
@@ -14,7 +14,7 @@
 @Configuration
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "DISABLED")
-@Log4j2
+@Slf4j
 public class DisabledAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
   @Bean
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
index 9681c36bc9f..62fdde4bf02 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
@@ -1,7 +1,7 @@
 package com.provectus.kafka.ui.config.auth;
 
 import java.util.List;
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
@@ -25,7 +25,7 @@
 @Configuration
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
-@Log4j2
+@Slf4j
 public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
 
   @Value("${spring.ldap.urls}")
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/props/CognitoProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/props/CognitoProperties.java
new file mode 100644
index 00000000000..4eb4508b97e
--- /dev/null
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/props/CognitoProperties.java
@@ -0,0 +1,44 @@
+package com.provectus.kafka.ui.config.auth.props;
+
+import lombok.Data;
+import lombok.ToString;
+import org.jetbrains.annotations.Nullable;
+
+@Data
+@ToString(exclude = "clientSecret")
+public class CognitoProperties {
+
+  String clientId;
+  String logoutUri;
+  String issuerUri;
+  String clientSecret;
+  @Nullable
+  String scope;
+  @Nullable
+  String userNameAttribute;
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getLogoutUri() {
+    return logoutUri;
+  }
+
+  public String getIssuerUri() {
+    return issuerUri;
+  }
+
+  public String getClientSecret() {
+    return clientSecret;
+  }
+
+  public @Nullable String getScope() {
+    return scope;
+  }
+
+  public @Nullable String getUserNameAttribute() {
+    return userNameAttribute;
+  }
+
+}


Test patch: