diff --git a/documentation/compose/DOCKER_COMPOSE.md b/documentation/compose/DOCKER_COMPOSE.md index c5fc1f1476..d019427d74 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 0000000000..a905c9c2ee --- /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://.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 0000000000..52e366f3ab --- /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 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 6bd56a877f..ae98dfdd7a 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 @@ package com.provectus.kafka.ui.config.auth; 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 @@ import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter; @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 0000000000..9db66e142d --- /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 d30aa4631b..4b1cc9a933 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 @@ import org.springframework.security.web.server.SecurityWebFilterChain; @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 9681c36bc9..62fdde4bf0 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 @@ import org.springframework.security.web.server.SecurityWebFilterChain; @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 0000000000..4eb4508b97 --- /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; + } + +}