Repository: provectus/kafka-ui
Issue: LDAP/LDAPS authentication with Microsoft Active Directory
Labels: type/enhancement
scope/backend
status/accepted

### Is your proposal related to a problem?

<!--
  Provide a clear and concise description of what the problem is.
  For example, "I'm always frustrated when..."
-->

LDAP/LDAPS authentication with Microsoft Active Directory seems not yet supported by Kafka-UI.

### Describe the solution you'd like

<!--
  Provide a clear and concise description of what you want to happen.
-->

It would be nice if Kafka-UI also support LDAP and LDAPS authentications with Microsoft Active Directory.
According to the  [Spring documentation](https://docs.spring.io/spring-security/site/docs/5.0.7.RELEASE/reference/html/ldap.html#ldap-active-directory) this is supported by the framework (but maybe will require one or more extra options). 

### Describe alternatives you've considered

<!--
  Let us know about other solutions you've tried or researched.
-->

No alternatives available.

### Additional context

<!--
  Is there anything else you can add about the proposal?
  You might want to link to related issues here, if you haven't already.
-->

Our Kafka-UI service resides in a Kubernetes cluster and has been deployed by the official helm chart (v0.3.2).
The configuration for LDAP we use is the following one (modulo server and domain names, that have been obfuscated):
```
kafka-ui:
  envs:
    config:
      AUTH_TYPE: "LDAP"
      SPRING_LDAP_URLS: "ldaps://ad01.domain.com:636"
      SPRING_LDAP_DN_PATTERN: "OU=people,DC=ad,DC=domain,DC=com"
```
The login windows reappears in a loop when I enter the credentials and no messages are printed in the pod's logs.
I think it would also be useful to see something logged in case of user logins in the Kafka-UI interface.

This issue has been briefly discussed with @Haarolean on the Kafka-UI discord channel.

Issue comments:
Hello there madrisan! 👋

Thank you and congratulations 🎉 for opening your very first issue in this project! 💖

In case you want to claim this issue, please comment down below! We will try to get back to you as soon as we can. 👀
Thanks, we can actually schedule it for 0.4, I'll let you know if we'll need to test it out
Ok. Thanks! I'll be glad to help testing this feature.
I add a feature request about it: our Kafka cluster uses Active Directory for authn/authz and the user should be able to do what its user is granted to. The kafka-ui should use the provided credential to connect to the cluster using SASL_SSL.
@angeloxx thanks for the suggestion, but that's totally another feature: #753 
The #753 plans to manage RBAC on the tool, this request is to pass-thru authentication and use providede credential to access to the cluster.
@angeloxx ah, got you. Please raise a new issue
Please try [this test build](https://s3.haarolean.dev/public/kafka-ui/issues-1466/kafka-ui_AD.tar.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=6RDSCSYBXHZ9QERQC9S5%2F20220602%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220602T090247Z&X-Amz-Expires=604800&X-Amz-Security-Token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI2UkRTQ1NZQlhIWjlRRVJRQzlTNSIsImV4cCI6MTY1NDE2NDE0NywicGFyZW50IjoiaGFhcm9sZWFuIn0.ffmVfc0n0eJE7iJi5_SGMzFt4f7Z_dp_E1OzSrb5SsrBeKuyW0NCmiqbxfyKdJh5TqZCBAqc2QRdaGlpljlICg&X-Amz-SignedHeaders=host&versionId=null&X-Amz-Signature=b63d3f328ed6ba58284342b8000978824379efecf71602b4548af807db124480):

Import it via `docker load < file.tar.gz`.
Image name is `provectuslabs/kafka-ui:ldap_ad`.
Additionally to other LDAP settings add these:
`OAUTH2_LDAP_AD=true`
`OAUTH2_LDAP_AD_DOMAIN=google.com`
The link is available for 7 days. Let me know how it goes.
I'll try to test it today, thanks.
@madrisan updated the link. It'll be active for 7 days.
Sorry. I'm unable to bypass the image arch issue
```
$ docker inspect --format='{{.Os}}/{{.Architecture}}/{{.Variant}}' provectuslabs/kafka-ui:ldap_ad
linux/arm64/v8
```
that ends up in a runtime error (even when running with `--platform "linux/amd64"`)
```
image was found but does not match the specified platform: wanted linux/amd64, actual: linux/arm64/v8
```
I've tried with `docker`, `docker` + `export DOCKER_BUILDKIT=1` and `podman`.
Oh well, let's try [this one](https://s3.haarolean.dev/public/kafka-ui/issues-1466/kafka-ui_AD.tar.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=702E1PC51NRF95FEIVWD%2F20220602%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220602T115406Z&X-Amz-Expires=604800&X-Amz-Security-Token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI3MDJFMVBDNTFOUkY5NUZFSVZXRCIsImV4cCI6MTY1NDE3NDM3NiwicGFyZW50IjoiaGFhcm9sZWFuIn0.p8weQrU0NsZlTmHHSQdloLmdoDMHK_0eRmFcR2ZfhCbJbJ0IOwjq2mLQY5842as28AfOGheM0y9ssW5YrWsa6A&X-Amz-SignedHeaders=host&versionId=null&X-Amz-Signature=4d54eb9ed6ac18841024166a8157bd9b17092e6f04ae8e2cf9529728b383554b), I've built it for amd64 this time.
Thanks.
I got an UNAUTHORIZED login error:
```
2022-06-02 16:19:01,029 DEBUG [boundedElastic-1] o.s.s.w.s.a.AuthenticationWebFilter: Authentication failed: simple bind failed: 10.100.16.38:636; nested exception is javax.naming.CommunicationException: simple bind failed: 10.10.1.16:636 [Root exception is java.net.SocketException: Connection or outbound has closed]
2022-06-02 16:19:01,030 DEBUG [boundedElastic-1] o.s.s.w.s.DelegatingServerAuthenticationEntryPoint: Trying to match using org.springframework.security.config.web.server.ServerHttpSecurity$HttpBasicSpec$$Lambda$700/0x0000000801085840@11a68372
2022-06-02 16:19:01,030 DEBUG [boundedElastic-1] o.s.s.w.s.DelegatingServerAuthenticationEntryPoint: No match found. Using default entry point org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint@2ab53c6a
2022-06-02 16:19:01,030 DEBUG [boundedElastic-1] o.s.w.s.a.HttpWebHandlerAdapter: [382d1f76-2] Completed 401 UNAUTHORIZED
```
I need to investigate more bit I'll be off for some days.
Feels like it might be due to SSL. Do you use LDAPS by any chance?
If that's the case, let's try this:
https://github.com/provectus/kafka-ui/blob/master/documentation/compose/kafka-ui-jmx-secured.yml#L27
Pass the truststore param, do a volume bind and add your LDAP's server SSL certificate into the keystore. Let me know how it goes.

Yes, we use LDAPS and I thought also of a probable certificate issue. It's a recurrent issue. Thanks for the documentation! I'll try to test this setup next week.
I was able to import our SSL certificate into the Java keystore but now I got all the time a login authentication error.
The cause is likely to be an incorrect configuration on my part.
Unfortunately I don't have access to the AD logs so it's very difficult to get the root cause and fix it: p
> I was able to import our SSL certificate into the Java keystore but now I got all the time a login authentication error. The cause is likely to be an incorrect configuration on my part. Unfortunately I don't have access to the AD logs so it's very difficult to get the root cause and fix it: p

guess it's safe to merge then? :)
I think so. This project has not reached the 1.0 version so one can expect some minor issues, and having this feature available will permit other people to test it (or use it if it work just fine). IMHO.
Hi @madrisan I faced issue while authentication with LDAPS. I already added certificate of ldaps to keystore. But I still couldn't login. Do you know what I were missing?
My docker-compose.yml
```
version: '3.4'
services:
  kafka-ui:
    container_name: kafka-ui
    image: provectuslabs/kafka-ui:v0.7.0
    ports:
      - 8080:8080
    environment:
      LOGGING_LEVEL_ROOT: DEBUG
      AUTH_TYPE: "LDAP"
      SPRING_LDAP_URLS: "ldaps://ldap001.as1.domain.net:636"
      SPRING_LDAP_USERFILTER_SEARCHBASE: "OU=Users,OU=Evidence.com,DC=as1,DC=domain,DC=net"
      SPRING_LDAP_ADMINUSER: "CN=ldap_user,OU=Service Accounts,OU=ABC.com,DC=as1,DC=domain,DC=net"
      SPRING_LDAP_ADMINPASSWORD: "xxxx"
      SPRING_LDAP_USERFILTER_SEARCHFILTER: "(&(uid={0})(objectClass=inetOrgPerson))"
      SPRING_LDAP_DN_PATTERN: "cn={0},OU=Users,OU=ABC.com,DC=as1,DC=domain,DC=net"
      OAUTH2.LDAP.ACTIVEDIRECTORY: "true"
      OAUTH2.LDAP.AСTIVEDIRECTORY.DOMAIN: "as1.domain.net"
      KAFKA_CLUSTERS_0_NAME: "kafka"
      KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SSL
      KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_LOCATION: "/ssl/cacerts"
      KAFKA_CLUSTERS_0_PROPERTIES_SSL_KEYSTORE_PASSWORD: "pass"
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: "kafka01:9091"
      KAFKA_CLUSTERS_0_SSL_TRUSTSTORELOCATION: "/ssl/cacerts"
      KAFKA_CLUSTERS_0_SSL_TRUSTSTOREPASSWORD: "pass"
      KAFKA_CLUSTERS_0_PROPERTIES_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: ''
      DYNAMIC_CONFIG_ENABLED: 'true'
    volumes:
      - path/compose/ssl:/ssl
```
Error logs:
```
kafka-ui  | 2023-05-12 04:44:26,153 DEBUG [reactor-http-epoll-8] o.s.w.s.a.HttpWebHandlerAdapter: [660a4958-10] HTTP POST "/login"
kafka-ui  | 2023-05-12 04:44:26,154 DEBUG [reactor-http-epoll-8] o.s.s.w.s.u.m.OrServerWebExchangeMatcher: Trying to match using PathMatcherServerWebExchangeMatcher{pattern='/login', method=POST}
kafka-ui  | 2023-05-12 04:44:26,155 DEBUG [reactor-http-epoll-8] o.s.s.w.s.u.m.PathPatternParserServerWebExchangeMatcher: Checking match of request : '/login'; against '/login'
kafka-ui  | 2023-05-12 04:44:26,155 DEBUG [reactor-http-epoll-8] o.s.s.w.s.u.m.OrServerWebExchangeMatcher: matched
kafka-ui  | 2023-05-12 04:44:26,155 DEBUG [reactor-http-epoll-8] r.n.c.FluxReceive: [660a4958-3, L:/172.19.0.2:8080 - R:/172.19.0.1:37802] [terminated=false, cancelled=false, pending=0, error=null]: subscribing inbound receiver
kafka-ui  | 2023-05-12 04:44:26,155 DEBUG [reactor-http-epoll-8] o.s.h.c.FormHttpMessageReader: [660a4958-10] Read form fields [username, password] (content masked)
kafka-ui  | 2023-05-12 04:44:26,762 DEBUG [boundedElastic-3] o.s.s.w.s.a.AuthenticationWebFilter: Authentication failed: Connection to LDAP server failed
kafka-ui  | 2023-05-12 04:44:26,763 DEBUG [boundedElastic-3] o.s.s.w.s.DefaultServerRedirectStrategy: Redirecting to '/login?error'
```
I just wanted to add my comments for anyone else who might be facing issues with setting up Ldaps while running Provectus UI in a container.

While the above settings are right, I was still facing issues and it seemed that adding the ldaps certs (server and ca) to the truststore was not enough. Because I was getting an error:

```
DEBUG [reactor-http-epoll-2] o.s.h.c.FormHttpMessageReader: [41223826-4] Read form fields [username, password] (content masked)
DEBUG [boundedElastic-1] o.s.s.w.s.a.AuthenticationWebFilter: Authentication failed: Connection to LDAP server failed

```

Uncertain why this error occurs, i checked with tcpdump/Wireshark the ldaps port comms, and turned out to be a cert error:
```
7  0.013859 source.ip.address ldaps.ip.address TLSv1.2  63   Alert (Level: Fatal, Description: Certificate Unknown)
```

Got help regarding this on Discord (many thanks), and adding another parameter helped, ldaps worked - have to specify a JAVA_OPTS environment variable inside the container (so -e when spinning up a container), where I specified the truststore / pwd of the truststore containing the ldaps certs.

```
-e JAVA_OPTS='-Djavax.net.ssl.trustStore=/app/truststore.jks -Djavax.net.ssl.trustStorePassword=truststore-pwd' 
```
You could do it with JAVA_OPTS environment variable also.

Pull request review comments:
I would use `activedeirectory` instead of `ac`
pls use description naming for field like `activeDirectoryDomain`

Patch that resolved the issue:
diff --git a/documentation/compose/auth-ldap.yaml b/documentation/compose/auth-ldap.yaml
index 7c25adce5dd..12069639f24 100644
--- a/documentation/compose/auth-ldap.yaml
+++ b/documentation/compose/auth-ldap.yaml
@@ -29,14 +29,19 @@ services:
       AUTH_TYPE: "LDAP"
       SPRING_LDAP_URLS: "ldap://ldap:10389"
       SPRING_LDAP_DN_PATTERN: "cn={0},ou=people,dc=planetexpress,dc=com"
-#     USER SEARCH FILTER INSTEAD OF DN
+
+#     ===== USER SEARCH FILTER INSTEAD OF DN =====
+
 #     SPRING_LDAP_USERFILTER_SEARCHBASE: "dc=planetexpress,dc=com"
 #     SPRING_LDAP_USERFILTER_SEARCHFILTER: "(&(uid={0})(objectClass=inetOrgPerson))"
 #     LDAP ADMIN USER
 #     SPRING_LDAP_ADMINUSER: "cn=admin,dc=planetexpress,dc=com"
 #     SPRING_LDAP_ADMINPASSWORD: "GoodNewsEveryone"
 
+#     ===== ACTIVE DIRECTORY =====
 
+#      OAUTH2.LDAP.ACTIVEDIRECTORY: true
+#      OAUTH2.LDAP.AСTIVEDIRECTORY.DOMAIN: "memelord.lol"
 
   ldap:
     image: rroemhild/test-openldap:latest
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 0d629a88360..9681c36bc9f 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
@@ -14,8 +14,10 @@
 import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
 import org.springframework.security.web.server.SecurityWebFilterChain;
@@ -39,6 +41,11 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
   @Value("${spring.ldap.userFilter.searchFilter:#{null}}")
   private String userFilterSearchFilter;
 
+  @Value("${oauth2.ldap.activeDirectory:false}")
+  private boolean isActiveDirectory;
+  @Value("${oauth2.ldap.aсtiveDirectory.domain:#{null}}")
+  private String activeDirectoryDomain;
+
   @Bean
   public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
     BindAuthenticator ba = new BindAuthenticator(contextSource);
@@ -51,9 +58,15 @@ public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSo
       ba.setUserSearch(userSearch);
     }
 
-    LdapAuthenticationProvider lap = new LdapAuthenticationProvider(ba);
+    AbstractLdapAuthenticationProvider authenticationProvider;
+    if (!isActiveDirectory) {
+      authenticationProvider = new LdapAuthenticationProvider(ba);
+    } else {
+      authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(activeDirectoryDomain, ldapUrls);
+      authenticationProvider.setUseAuthenticationRequestCredentials(true);
+    }
 
-    AuthenticationManager am = new ProviderManager(List.of(lap));
+    AuthenticationManager am = new ProviderManager(List.of(authenticationProvider));
 
     return new ReactiveAuthenticationManagerAdapter(am);
   }
@@ -71,6 +84,9 @@ public BaseLdapPathContextSource contextSource() {
   @Bean
   public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {
     log.info("Configuring LDAP authentication.");
+    if (isActiveDirectory) {
+      log.info("Active Directory support for LDAP has been enabled.");
+    }
 
     http
         .authorizeExchange()


Test patch: