Repository: goauthentik/authentik

Issue:
LDAP Source Bind from Federation & Social login is Broken Since Commit 1ca8feb
**Describe the bug**
LDAP Sources are broken since commit 1ca8feb. This is due to a double bind that is attempted on the Connection object. The second bind consistently causes a LDAPInvalidCredentailResult exception even if it is successful the first bind. I found this bug after updating Authentik to 2023.5.3 wondering why LDAP logins no longer worked. The LDAP backend is consistently changing passwords due to TOTP so a cached password was never hit always forcing a LDAP bind. Which is what made me find this issue.

**To Reproduce**
1. Set up a LDAP Source that works
2. Login in as any user using the LDAP Password for that users THATS NOT CACHED
3. User Always gets Invalid Credentials

**Expected behavior**
User should successfully login 

**Logs**
None Provided

**Version and Deployment (please complete the following information):**

-   authentik version: 2023.5.3
-   Deployment: docker-compose


**Additional context**
I confirmed this bug on my own stack by creating multiple logging lines using LOGGER and checking DEBUG output. For some reason a second bind always causes this Exception with Authentik if the first was successful. I'm not sure if the state is reset per successful connection which is causing this. But by removing the extra bind performed in /authentik/sources/ldap/auth.py I no longer get erroneous invalid credential exceptions.



Discussion:
I can confirm that this regression is present in 2023.5.3 whereas 2023.1.2 is working. We are seeing the same behaviour.
Yeah that extra bind here https://github.com/goauthentik/authentik/commit/1ca8feb5fcd7070a1825ef28c7b2c4baaa2b245a#diff-8e72f28797d7b000f7675c5e282aed66bb14164300c9537fcb7cee8e0bfaa413R66 should be removed, however I wasn't able to reproduce any error testing against Samba

Resolving patch:
diff --git a/authentik/sources/ldap/auth.py b/authentik/sources/ldap/auth.py
--- a/authentik/sources/ldap/auth.py
+++ b/authentik/sources/ldap/auth.py
@@ -57,13 +57,13 @@ def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> Op
         # Try to bind as new user
         LOGGER.debug("Attempting to bind as user", user=user)
         try:
-            temp_connection = source.connection(
+            # source.connection also attempts to bind
+            source.connection(
                 connection_kwargs={
                     "user": user.attributes.get(LDAP_DISTINGUISHED_NAME),
                     "password": password,
                 }
             )
-            temp_connection.bind()
             return user
         except LDAPInvalidCredentialsResult as exc:
             LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py
--- a/authentik/sources/ldap/models.py
+++ b/authentik/sources/ldap/models.py
@@ -145,7 +145,9 @@ def connection(
         if self.start_tls:
             connection.start_tls(read_server_info=False)
         try:
-            connection.bind()
+            successful = connection.bind()
+            if successful:
+                return connection
         except LDAPSchemaError as exc:
             # Schema error, so try connecting without schema info
             # See https://github.com/goauthentik/authentik/issues/4590
@@ -153,7 +155,7 @@ def connection(
                 raise exc
             server_kwargs["get_info"] = NONE
             return self.connection(server_kwargs, connection_kwargs)
-        return connection
+        return RuntimeError("Failed to bind")
 
     class Meta:
         verbose_name = _("LDAP Source")


Test patch:
diff --git a/authentik/sources/ldap/tests/test_auth.py b/authentik/sources/ldap/tests/test_auth.py
--- a/authentik/sources/ldap/tests/test_auth.py
+++ b/authentik/sources/ldap/tests/test_auth.py
@@ -29,6 +29,37 @@ def setUp(self):
             additional_group_dn="ou=groups",
         )
 
+    def test_auth_direct_user_ad(self):
+        """Test direct auth"""
+        self.source.property_mappings.set(
+            LDAPPropertyMapping.objects.filter(
+                Q(managed__startswith="goauthentik.io/sources/ldap/default-")
+                | Q(managed__startswith="goauthentik.io/sources/ldap/ms-")
+            )
+        )
+        raw_conn = mock_ad_connection(LDAP_PASSWORD)
+        bind_mock = Mock(wraps=raw_conn.bind)
+        raw_conn.bind = bind_mock
+        connection = MagicMock(return_value=raw_conn)
+        with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
+            user_sync = UserLDAPSynchronizer(self.source)
+            user_sync.sync()
+
+            user = User.objects.get(username="user0_sn")
+            # auth_user_by_bind = Mock(return_value=user)
+            backend = LDAPBackend()
+            self.assertEqual(
+                backend.authenticate(None, username="user0_sn", password=LDAP_PASSWORD),
+                user,
+            )
+            connection.assert_called_with(
+                connection_kwargs={
+                    "user": "cn=user0,ou=users,dc=goauthentik,dc=io",
+                    "password": LDAP_PASSWORD,
+                }
+            )
+            bind_mock.assert_not_called()
+
     def test_auth_synced_user_ad(self):
         """Test Cached auth"""
         self.source.property_mappings.set(