Browse Source

Cognito logout + revamp (#1063)

* application yml example

* Update SSO guide

* Add cognito logout handler

* Fix annotations

* impl a separate config file for cognito

* cleanup

* Rollback auth.type change

* Add compose example

* Auth revamp

* Review suggestions

* Use configurationProperties, rename urls to uris
Roman Zabaluev 2 years ago
parent
commit
5db2c17994

+ 1 - 0
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).
 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.
 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.
 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

+ 22 - 0
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"

+ 53 - 0
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);
+  }
+}
+

+ 2 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/BasicAuthSecurityConfig.java

@@ -2,7 +2,7 @@ package com.provectus.kafka.ui.config.auth;
 
 
 import com.provectus.kafka.ui.util.EmptyRedirectStrategy;
 import com.provectus.kafka.ui.util.EmptyRedirectStrategy;
 import java.net.URI;
 import java.net.URI;
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
@@ -17,7 +17,7 @@ import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter;
 @Configuration
 @Configuration
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "LOGIN_FORM")
 @ConditionalOnProperty(value = "auth.type", havingValue = "LOGIN_FORM")
-@Log4j2
+@Slf4j
 public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig {
 public class BasicAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
 
   public static final String LOGIN_URL = "/auth";
   public static final String LOGIN_URL = "/auth";

+ 80 - 0
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();
+  }
+
+}

+ 2 - 2
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;
 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.SpringApplication;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
@@ -14,7 +14,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
 @Configuration
 @Configuration
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "DISABLED")
 @ConditionalOnProperty(value = "auth.type", havingValue = "DISABLED")
-@Log4j2
+@Slf4j
 public class DisabledAuthSecurityConfig extends AbstractAuthSecurityConfig {
 public class DisabledAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
 
   @Bean
   @Bean

+ 2 - 2
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;
 package com.provectus.kafka.ui.config.auth;
 
 
 import java.util.List;
 import java.util.List;
-import lombok.extern.log4j.Log4j2;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
@@ -25,7 +25,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
 @Configuration
 @Configuration
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
 @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
-@Log4j2
+@Slf4j
 public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
 public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
 
 
   @Value("${spring.ldap.urls}")
   @Value("${spring.ldap.urls}")

+ 44 - 0
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;
+  }
+
+}