We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `authentik/sources/ldap/models.py`
Content:
```
1 """authentik LDAP Models"""
2 from ssl import CERT_REQUIRED
3 from typing import Optional
4 
5 from django.db import models
6 from django.utils.translation import gettext_lazy as _
7 from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
8 from ldap3.core.exceptions import LDAPSchemaError
9 from rest_framework.serializers import Serializer
10 
11 from authentik.core.models import Group, PropertyMapping, Source
12 from authentik.crypto.models import CertificateKeyPair
13 from authentik.lib.config import CONFIG
14 from authentik.lib.models import DomainlessURLValidator
15 
16 LDAP_TIMEOUT = 15
17 
18 
19 class MultiURLValidator(DomainlessURLValidator):
20     """Same as DomainlessURLValidator but supports multiple URLs separated with a comma."""
21 
22     def __call__(self, value: str):
23         if "," in value:
24             for url in value.split(","):
25                 super().__call__(url)
26         else:
27             super().__call__(value)
28 
29 
30 class LDAPSource(Source):
31     """Federate LDAP Directory with authentik, or create new accounts in LDAP."""
32 
33     server_uri = models.TextField(
34         validators=[MultiURLValidator(schemes=["ldap", "ldaps"])],
35         verbose_name=_("Server URI"),
36     )
37     peer_certificate = models.ForeignKey(
38         CertificateKeyPair,
39         on_delete=models.SET_DEFAULT,
40         default=None,
41         null=True,
42         help_text=_(
43             "Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair."
44         ),
45     )
46 
47     bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True)
48     bind_password = models.TextField(blank=True)
49     start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS"))
50 
51     base_dn = models.TextField(verbose_name=_("Base DN"))
52     additional_user_dn = models.TextField(
53         help_text=_("Prepended to Base DN for User-queries."),
54         verbose_name=_("Addition User DN"),
55         blank=True,
56     )
57     additional_group_dn = models.TextField(
58         help_text=_("Prepended to Base DN for Group-queries."),
59         verbose_name=_("Addition Group DN"),
60         blank=True,
61     )
62 
63     user_object_filter = models.TextField(
64         default="(objectClass=person)",
65         help_text=_("Consider Objects matching this filter to be Users."),
66     )
67     group_membership_field = models.TextField(
68         default="member", help_text=_("Field which contains members of a group.")
69     )
70     group_object_filter = models.TextField(
71         default="(objectClass=group)",
72         help_text=_("Consider Objects matching this filter to be Groups."),
73     )
74     object_uniqueness_field = models.TextField(
75         default="objectSid", help_text=_("Field which contains a unique Identifier.")
76     )
77 
78     property_mappings_group = models.ManyToManyField(
79         PropertyMapping,
80         default=None,
81         blank=True,
82         help_text=_("Property mappings used for group creation/updating."),
83     )
84 
85     sync_users = models.BooleanField(default=True)
86     sync_users_password = models.BooleanField(
87         default=True,
88         help_text=_(
89             "When a user changes their password, sync it back to LDAP. "
90             "This can only be enabled on a single LDAP source."
91         ),
92     )
93     sync_groups = models.BooleanField(default=True)
94     sync_parent_group = models.ForeignKey(
95         Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT
96     )
97 
98     @property
99     def component(self) -> str:
100         return "ak-source-ldap-form"
101 
102     @property
103     def serializer(self) -> type[Serializer]:
104         from authentik.sources.ldap.api import LDAPSourceSerializer
105 
106         return LDAPSourceSerializer
107 
108     def server(self, **kwargs) -> Server:
109         """Get LDAP Server/ServerPool"""
110         servers = []
111         tls_kwargs = {}
112         if self.peer_certificate:
113             tls_kwargs["ca_certs_data"] = self.peer_certificate.certificate_data
114             tls_kwargs["validate"] = CERT_REQUIRED
115         if ciphers := CONFIG.y("ldap.tls.ciphers", None):
116             tls_kwargs["ciphers"] = ciphers.strip()
117         server_kwargs = {
118             "get_info": ALL,
119             "connect_timeout": LDAP_TIMEOUT,
120             "tls": Tls(**tls_kwargs),
121         }
122         server_kwargs.update(kwargs)
123         if "," in self.server_uri:
124             for server in self.server_uri.split(","):
125                 servers.append(Server(server, **server_kwargs))
126         else:
127             servers = [Server(self.server_uri, **server_kwargs)]
128         return ServerPool(servers, RANDOM, active=True, exhaust=True)
129 
130     def connection(
131         self, server_kwargs: Optional[dict] = None, connection_kwargs: Optional[dict] = None
132     ) -> Connection:
133         """Get a fully connected and bound LDAP Connection"""
134         server_kwargs = server_kwargs or {}
135         connection_kwargs = connection_kwargs or {}
136         connection_kwargs.setdefault("user", self.bind_cn)
137         connection_kwargs.setdefault("password", self.bind_password)
138         connection = Connection(
139             self.server(**server_kwargs),
140             raise_exceptions=True,
141             receive_timeout=LDAP_TIMEOUT,
142             **connection_kwargs,
143         )
144 
145         if self.start_tls:
146             connection.start_tls(read_server_info=False)
147         try:
148             connection.bind()
149         except LDAPSchemaError as exc:
150             # Schema error, so try connecting without schema info
151             # See https://github.com/goauthentik/authentik/issues/4590
152             if server_kwargs.get("get_info", ALL) == NONE:
153                 raise exc
154             server_kwargs["get_info"] = NONE
155             return self.connection(server_kwargs, connection_kwargs)
156         return connection
157 
158     class Meta:
159         verbose_name = _("LDAP Source")
160         verbose_name_plural = _("LDAP Sources")
161 
162 
163 class LDAPPropertyMapping(PropertyMapping):
164     """Map LDAP Property to User or Group object attribute"""
165 
166     object_field = models.TextField()
167 
168     @property
169     def component(self) -> str:
170         return "ak-property-mapping-ldap-form"
171 
172     @property
173     def serializer(self) -> type[Serializer]:
174         from authentik.sources.ldap.api import LDAPPropertyMappingSerializer
175 
176         return LDAPPropertyMappingSerializer
177 
178     def __str__(self):
179         return str(self.name)
180 
181     class Meta:
182         verbose_name = _("LDAP Property Mapping")
183         verbose_name_plural = _("LDAP Property Mappings")
184
```

Path: `authentik/sources/ldap/auth.py`
Content:
```
1 """authentik LDAP Authentication Backend"""
2 from typing import Optional
3 
4 from django.http import HttpRequest
5 from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
6 from structlog.stdlib import get_logger
7 
8 from authentik.core.auth import InbuiltBackend
9 from authentik.core.models import User
10 from authentik.sources.ldap.models import LDAPSource
11 
12 LOGGER = get_logger()
13 LDAP_DISTINGUISHED_NAME = "distinguishedName"
14 
15 
16 class LDAPBackend(InbuiltBackend):
17     """Authenticate users against LDAP Server"""
18 
19     def authenticate(self, request: HttpRequest, **kwargs):
20         """Try to authenticate a user via ldap"""
21         if "password" not in kwargs:
22             return None
23         for source in LDAPSource.objects.filter(enabled=True):
24             LOGGER.debug("LDAP Auth attempt", source=source)
25             user = self.auth_user(source, **kwargs)
26             if user:
27                 self.set_method("ldap", request, source=source)
28                 return user
29         return None
30 
31     def auth_user(self, source: LDAPSource, password: str, **filters: str) -> Optional[User]:
32         """Try to bind as either user_dn or mail with password.
33         Returns True on success, otherwise False"""
34         users = User.objects.filter(**filters)
35         if not users.exists():
36             return None
37         user: User = users.first()
38         if LDAP_DISTINGUISHED_NAME not in user.attributes:
39             LOGGER.debug("User doesn't have DN set, assuming not LDAP imported.", user=user)
40             return None
41         # Either has unusable password,
42         # or has a password, but couldn't be authenticated by ModelBackend.
43         # This means we check with a bind to see if the LDAP password has changed
44         if self.auth_user_by_bind(source, user, password):
45             # Password given successfully binds to LDAP, so we save it in our Database
46             LOGGER.debug("Updating user's password in DB", user=user)
47             user.set_password(password, signal=False)
48             user.save()
49             return user
50         # Password doesn't match
51         LOGGER.debug("Failed to bind, password invalid")
52         return None
53 
54     def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> Optional[User]:
55         """Attempt authentication by binding to the LDAP server as `user`. This
56         method should be avoided as its slow to do the bind."""
57         # Try to bind as new user
58         LOGGER.debug("Attempting to bind as user", user=user)
59         try:
60             temp_connection = source.connection(
61                 connection_kwargs={
62                     "user": user.attributes.get(LDAP_DISTINGUISHED_NAME),
63                     "password": password,
64                 }
65             )
66             temp_connection.bind()
67             return user
68         except LDAPInvalidCredentialsResult as exc:
69             LOGGER.debug("invalid LDAP credentials", user=user, exc=exc)
70         except LDAPException as exc:
71             LOGGER.warning("failed to bind to LDAP", exc=exc)
72         return None
73
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
         # 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 @@
         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 @@
                 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")

{"golden_diff": "diff --git a/authentik/sources/ldap/auth.py b/authentik/sources/ldap/auth.py\n--- a/authentik/sources/ldap/auth.py\n+++ b/authentik/sources/ldap/auth.py\n@@ -57,13 +57,13 @@\n         # Try to bind as new user\n         LOGGER.debug(\"Attempting to bind as user\", user=user)\n         try:\n-            temp_connection = source.connection(\n+            # source.connection also attempts to bind\n+            source.connection(\n                 connection_kwargs={\n                     \"user\": user.attributes.get(LDAP_DISTINGUISHED_NAME),\n                     \"password\": password,\n                 }\n             )\n-            temp_connection.bind()\n             return user\n         except LDAPInvalidCredentialsResult as exc:\n             LOGGER.debug(\"invalid LDAP credentials\", user=user, exc=exc)\ndiff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py\n--- a/authentik/sources/ldap/models.py\n+++ b/authentik/sources/ldap/models.py\n@@ -145,7 +145,9 @@\n         if self.start_tls:\n             connection.start_tls(read_server_info=False)\n         try:\n-            connection.bind()\n+            successful = connection.bind()\n+            if successful:\n+                return connection\n         except LDAPSchemaError as exc:\n             # Schema error, so try connecting without schema info\n             # See https://github.com/goauthentik/authentik/issues/4590\n@@ -153,7 +155,7 @@\n                 raise exc\n             server_kwargs[\"get_info\"] = NONE\n             return self.connection(server_kwargs, connection_kwargs)\n-        return connection\n+        return RuntimeError(\"Failed to bind\")\n \n     class Meta:\n         verbose_name = _(\"LDAP Source\")\n", "issue": "LDAP Source Bind from Federation & Social login is Broken Since Commit 1ca8feb\n**Describe the bug**\r\nLDAP 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.\r\n\r\n**To Reproduce**\r\n1. Set up a LDAP Source that works\r\n2. Login in as any user using the LDAP Password for that users THATS NOT CACHED\r\n3. User Always gets Invalid Credentials\r\n\r\n**Expected behavior**\r\nUser should successfully login \r\n\r\n**Logs**\r\nNone Provided\r\n\r\n**Version and Deployment (please complete the following information):**\r\n\r\n-   authentik version: 2023.5.3\r\n-   Deployment: docker-compose\r\n\r\n\r\n**Additional context**\r\nI 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.\r\n\n", "before_files": [{"content": "\"\"\"authentik LDAP Models\"\"\"\nfrom ssl import CERT_REQUIRED\nfrom typing import Optional\n\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\nfrom ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls\nfrom ldap3.core.exceptions import LDAPSchemaError\nfrom rest_framework.serializers import Serializer\n\nfrom authentik.core.models import Group, PropertyMapping, Source\nfrom authentik.crypto.models import CertificateKeyPair\nfrom authentik.lib.config import CONFIG\nfrom authentik.lib.models import DomainlessURLValidator\n\nLDAP_TIMEOUT = 15\n\n\nclass MultiURLValidator(DomainlessURLValidator):\n    \"\"\"Same as DomainlessURLValidator but supports multiple URLs separated with a comma.\"\"\"\n\n    def __call__(self, value: str):\n        if \",\" in value:\n            for url in value.split(\",\"):\n                super().__call__(url)\n        else:\n            super().__call__(value)\n\n\nclass LDAPSource(Source):\n    \"\"\"Federate LDAP Directory with authentik, or create new accounts in LDAP.\"\"\"\n\n    server_uri = models.TextField(\n        validators=[MultiURLValidator(schemes=[\"ldap\", \"ldaps\"])],\n        verbose_name=_(\"Server URI\"),\n    )\n    peer_certificate = models.ForeignKey(\n        CertificateKeyPair,\n        on_delete=models.SET_DEFAULT,\n        default=None,\n        null=True,\n        help_text=_(\n            \"Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair.\"\n        ),\n    )\n\n    bind_cn = models.TextField(verbose_name=_(\"Bind CN\"), blank=True)\n    bind_password = models.TextField(blank=True)\n    start_tls = models.BooleanField(default=False, verbose_name=_(\"Enable Start TLS\"))\n\n    base_dn = models.TextField(verbose_name=_(\"Base DN\"))\n    additional_user_dn = models.TextField(\n        help_text=_(\"Prepended to Base DN for User-queries.\"),\n        verbose_name=_(\"Addition User DN\"),\n        blank=True,\n    )\n    additional_group_dn = models.TextField(\n        help_text=_(\"Prepended to Base DN for Group-queries.\"),\n        verbose_name=_(\"Addition Group DN\"),\n        blank=True,\n    )\n\n    user_object_filter = models.TextField(\n        default=\"(objectClass=person)\",\n        help_text=_(\"Consider Objects matching this filter to be Users.\"),\n    )\n    group_membership_field = models.TextField(\n        default=\"member\", help_text=_(\"Field which contains members of a group.\")\n    )\n    group_object_filter = models.TextField(\n        default=\"(objectClass=group)\",\n        help_text=_(\"Consider Objects matching this filter to be Groups.\"),\n    )\n    object_uniqueness_field = models.TextField(\n        default=\"objectSid\", help_text=_(\"Field which contains a unique Identifier.\")\n    )\n\n    property_mappings_group = models.ManyToManyField(\n        PropertyMapping,\n        default=None,\n        blank=True,\n        help_text=_(\"Property mappings used for group creation/updating.\"),\n    )\n\n    sync_users = models.BooleanField(default=True)\n    sync_users_password = models.BooleanField(\n        default=True,\n        help_text=_(\n            \"When a user changes their password, sync it back to LDAP. \"\n            \"This can only be enabled on a single LDAP source.\"\n        ),\n    )\n    sync_groups = models.BooleanField(default=True)\n    sync_parent_group = models.ForeignKey(\n        Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT\n    )\n\n    @property\n    def component(self) -> str:\n        return \"ak-source-ldap-form\"\n\n    @property\n    def serializer(self) -> type[Serializer]:\n        from authentik.sources.ldap.api import LDAPSourceSerializer\n\n        return LDAPSourceSerializer\n\n    def server(self, **kwargs) -> Server:\n        \"\"\"Get LDAP Server/ServerPool\"\"\"\n        servers = []\n        tls_kwargs = {}\n        if self.peer_certificate:\n            tls_kwargs[\"ca_certs_data\"] = self.peer_certificate.certificate_data\n            tls_kwargs[\"validate\"] = CERT_REQUIRED\n        if ciphers := CONFIG.y(\"ldap.tls.ciphers\", None):\n            tls_kwargs[\"ciphers\"] = ciphers.strip()\n        server_kwargs = {\n            \"get_info\": ALL,\n            \"connect_timeout\": LDAP_TIMEOUT,\n            \"tls\": Tls(**tls_kwargs),\n        }\n        server_kwargs.update(kwargs)\n        if \",\" in self.server_uri:\n            for server in self.server_uri.split(\",\"):\n                servers.append(Server(server, **server_kwargs))\n        else:\n            servers = [Server(self.server_uri, **server_kwargs)]\n        return ServerPool(servers, RANDOM, active=True, exhaust=True)\n\n    def connection(\n        self, server_kwargs: Optional[dict] = None, connection_kwargs: Optional[dict] = None\n    ) -> Connection:\n        \"\"\"Get a fully connected and bound LDAP Connection\"\"\"\n        server_kwargs = server_kwargs or {}\n        connection_kwargs = connection_kwargs or {}\n        connection_kwargs.setdefault(\"user\", self.bind_cn)\n        connection_kwargs.setdefault(\"password\", self.bind_password)\n        connection = Connection(\n            self.server(**server_kwargs),\n            raise_exceptions=True,\n            receive_timeout=LDAP_TIMEOUT,\n            **connection_kwargs,\n        )\n\n        if self.start_tls:\n            connection.start_tls(read_server_info=False)\n        try:\n            connection.bind()\n        except LDAPSchemaError as exc:\n            # Schema error, so try connecting without schema info\n            # See https://github.com/goauthentik/authentik/issues/4590\n            if server_kwargs.get(\"get_info\", ALL) == NONE:\n                raise exc\n            server_kwargs[\"get_info\"] = NONE\n            return self.connection(server_kwargs, connection_kwargs)\n        return connection\n\n    class Meta:\n        verbose_name = _(\"LDAP Source\")\n        verbose_name_plural = _(\"LDAP Sources\")\n\n\nclass LDAPPropertyMapping(PropertyMapping):\n    \"\"\"Map LDAP Property to User or Group object attribute\"\"\"\n\n    object_field = models.TextField()\n\n    @property\n    def component(self) -> str:\n        return \"ak-property-mapping-ldap-form\"\n\n    @property\n    def serializer(self) -> type[Serializer]:\n        from authentik.sources.ldap.api import LDAPPropertyMappingSerializer\n\n        return LDAPPropertyMappingSerializer\n\n    def __str__(self):\n        return str(self.name)\n\n    class Meta:\n        verbose_name = _(\"LDAP Property Mapping\")\n        verbose_name_plural = _(\"LDAP Property Mappings\")\n", "path": "authentik/sources/ldap/models.py"}, {"content": "\"\"\"authentik LDAP Authentication Backend\"\"\"\nfrom typing import Optional\n\nfrom django.http import HttpRequest\nfrom ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.auth import InbuiltBackend\nfrom authentik.core.models import User\nfrom authentik.sources.ldap.models import LDAPSource\n\nLOGGER = get_logger()\nLDAP_DISTINGUISHED_NAME = \"distinguishedName\"\n\n\nclass LDAPBackend(InbuiltBackend):\n    \"\"\"Authenticate users against LDAP Server\"\"\"\n\n    def authenticate(self, request: HttpRequest, **kwargs):\n        \"\"\"Try to authenticate a user via ldap\"\"\"\n        if \"password\" not in kwargs:\n            return None\n        for source in LDAPSource.objects.filter(enabled=True):\n            LOGGER.debug(\"LDAP Auth attempt\", source=source)\n            user = self.auth_user(source, **kwargs)\n            if user:\n                self.set_method(\"ldap\", request, source=source)\n                return user\n        return None\n\n    def auth_user(self, source: LDAPSource, password: str, **filters: str) -> Optional[User]:\n        \"\"\"Try to bind as either user_dn or mail with password.\n        Returns True on success, otherwise False\"\"\"\n        users = User.objects.filter(**filters)\n        if not users.exists():\n            return None\n        user: User = users.first()\n        if LDAP_DISTINGUISHED_NAME not in user.attributes:\n            LOGGER.debug(\"User doesn't have DN set, assuming not LDAP imported.\", user=user)\n            return None\n        # Either has unusable password,\n        # or has a password, but couldn't be authenticated by ModelBackend.\n        # This means we check with a bind to see if the LDAP password has changed\n        if self.auth_user_by_bind(source, user, password):\n            # Password given successfully binds to LDAP, so we save it in our Database\n            LOGGER.debug(\"Updating user's password in DB\", user=user)\n            user.set_password(password, signal=False)\n            user.save()\n            return user\n        # Password doesn't match\n        LOGGER.debug(\"Failed to bind, password invalid\")\n        return None\n\n    def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> Optional[User]:\n        \"\"\"Attempt authentication by binding to the LDAP server as `user`. This\n        method should be avoided as its slow to do the bind.\"\"\"\n        # Try to bind as new user\n        LOGGER.debug(\"Attempting to bind as user\", user=user)\n        try:\n            temp_connection = source.connection(\n                connection_kwargs={\n                    \"user\": user.attributes.get(LDAP_DISTINGUISHED_NAME),\n                    \"password\": password,\n                }\n            )\n            temp_connection.bind()\n            return user\n        except LDAPInvalidCredentialsResult as exc:\n            LOGGER.debug(\"invalid LDAP credentials\", user=user, exc=exc)\n        except LDAPException as exc:\n            LOGGER.warning(\"failed to bind to LDAP\", exc=exc)\n        return None\n", "path": "authentik/sources/ldap/auth.py"}], "after_files": [{"content": "\"\"\"authentik LDAP Models\"\"\"\nfrom ssl import CERT_REQUIRED\nfrom typing import Optional\n\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\nfrom ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls\nfrom ldap3.core.exceptions import LDAPSchemaError\nfrom rest_framework.serializers import Serializer\n\nfrom authentik.core.models import Group, PropertyMapping, Source\nfrom authentik.crypto.models import CertificateKeyPair\nfrom authentik.lib.config import CONFIG\nfrom authentik.lib.models import DomainlessURLValidator\n\nLDAP_TIMEOUT = 15\n\n\nclass MultiURLValidator(DomainlessURLValidator):\n    \"\"\"Same as DomainlessURLValidator but supports multiple URLs separated with a comma.\"\"\"\n\n    def __call__(self, value: str):\n        if \",\" in value:\n            for url in value.split(\",\"):\n                super().__call__(url)\n        else:\n            super().__call__(value)\n\n\nclass LDAPSource(Source):\n    \"\"\"Federate LDAP Directory with authentik, or create new accounts in LDAP.\"\"\"\n\n    server_uri = models.TextField(\n        validators=[MultiURLValidator(schemes=[\"ldap\", \"ldaps\"])],\n        verbose_name=_(\"Server URI\"),\n    )\n    peer_certificate = models.ForeignKey(\n        CertificateKeyPair,\n        on_delete=models.SET_DEFAULT,\n        default=None,\n        null=True,\n        help_text=_(\n            \"Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair.\"\n        ),\n    )\n\n    bind_cn = models.TextField(verbose_name=_(\"Bind CN\"), blank=True)\n    bind_password = models.TextField(blank=True)\n    start_tls = models.BooleanField(default=False, verbose_name=_(\"Enable Start TLS\"))\n\n    base_dn = models.TextField(verbose_name=_(\"Base DN\"))\n    additional_user_dn = models.TextField(\n        help_text=_(\"Prepended to Base DN for User-queries.\"),\n        verbose_name=_(\"Addition User DN\"),\n        blank=True,\n    )\n    additional_group_dn = models.TextField(\n        help_text=_(\"Prepended to Base DN for Group-queries.\"),\n        verbose_name=_(\"Addition Group DN\"),\n        blank=True,\n    )\n\n    user_object_filter = models.TextField(\n        default=\"(objectClass=person)\",\n        help_text=_(\"Consider Objects matching this filter to be Users.\"),\n    )\n    group_membership_field = models.TextField(\n        default=\"member\", help_text=_(\"Field which contains members of a group.\")\n    )\n    group_object_filter = models.TextField(\n        default=\"(objectClass=group)\",\n        help_text=_(\"Consider Objects matching this filter to be Groups.\"),\n    )\n    object_uniqueness_field = models.TextField(\n        default=\"objectSid\", help_text=_(\"Field which contains a unique Identifier.\")\n    )\n\n    property_mappings_group = models.ManyToManyField(\n        PropertyMapping,\n        default=None,\n        blank=True,\n        help_text=_(\"Property mappings used for group creation/updating.\"),\n    )\n\n    sync_users = models.BooleanField(default=True)\n    sync_users_password = models.BooleanField(\n        default=True,\n        help_text=_(\n            \"When a user changes their password, sync it back to LDAP. \"\n            \"This can only be enabled on a single LDAP source.\"\n        ),\n    )\n    sync_groups = models.BooleanField(default=True)\n    sync_parent_group = models.ForeignKey(\n        Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT\n    )\n\n    @property\n    def component(self) -> str:\n        return \"ak-source-ldap-form\"\n\n    @property\n    def serializer(self) -> type[Serializer]:\n        from authentik.sources.ldap.api import LDAPSourceSerializer\n\n        return LDAPSourceSerializer\n\n    def server(self, **kwargs) -> Server:\n        \"\"\"Get LDAP Server/ServerPool\"\"\"\n        servers = []\n        tls_kwargs = {}\n        if self.peer_certificate:\n            tls_kwargs[\"ca_certs_data\"] = self.peer_certificate.certificate_data\n            tls_kwargs[\"validate\"] = CERT_REQUIRED\n        if ciphers := CONFIG.y(\"ldap.tls.ciphers\", None):\n            tls_kwargs[\"ciphers\"] = ciphers.strip()\n        server_kwargs = {\n            \"get_info\": ALL,\n            \"connect_timeout\": LDAP_TIMEOUT,\n            \"tls\": Tls(**tls_kwargs),\n        }\n        server_kwargs.update(kwargs)\n        if \",\" in self.server_uri:\n            for server in self.server_uri.split(\",\"):\n                servers.append(Server(server, **server_kwargs))\n        else:\n            servers = [Server(self.server_uri, **server_kwargs)]\n        return ServerPool(servers, RANDOM, active=True, exhaust=True)\n\n    def connection(\n        self, server_kwargs: Optional[dict] = None, connection_kwargs: Optional[dict] = None\n    ) -> Connection:\n        \"\"\"Get a fully connected and bound LDAP Connection\"\"\"\n        server_kwargs = server_kwargs or {}\n        connection_kwargs = connection_kwargs or {}\n        connection_kwargs.setdefault(\"user\", self.bind_cn)\n        connection_kwargs.setdefault(\"password\", self.bind_password)\n        connection = Connection(\n            self.server(**server_kwargs),\n            raise_exceptions=True,\n            receive_timeout=LDAP_TIMEOUT,\n            **connection_kwargs,\n        )\n\n        if self.start_tls:\n            connection.start_tls(read_server_info=False)\n        try:\n            successful = connection.bind()\n            if successful:\n                return connection\n        except LDAPSchemaError as exc:\n            # Schema error, so try connecting without schema info\n            # See https://github.com/goauthentik/authentik/issues/4590\n            if server_kwargs.get(\"get_info\", ALL) == NONE:\n                raise exc\n            server_kwargs[\"get_info\"] = NONE\n            return self.connection(server_kwargs, connection_kwargs)\n        return RuntimeError(\"Failed to bind\")\n\n    class Meta:\n        verbose_name = _(\"LDAP Source\")\n        verbose_name_plural = _(\"LDAP Sources\")\n\n\nclass LDAPPropertyMapping(PropertyMapping):\n    \"\"\"Map LDAP Property to User or Group object attribute\"\"\"\n\n    object_field = models.TextField()\n\n    @property\n    def component(self) -> str:\n        return \"ak-property-mapping-ldap-form\"\n\n    @property\n    def serializer(self) -> type[Serializer]:\n        from authentik.sources.ldap.api import LDAPPropertyMappingSerializer\n\n        return LDAPPropertyMappingSerializer\n\n    def __str__(self):\n        return str(self.name)\n\n    class Meta:\n        verbose_name = _(\"LDAP Property Mapping\")\n        verbose_name_plural = _(\"LDAP Property Mappings\")\n", "path": "authentik/sources/ldap/models.py"}, {"content": "\"\"\"authentik LDAP Authentication Backend\"\"\"\nfrom typing import Optional\n\nfrom django.http import HttpRequest\nfrom ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult\nfrom structlog.stdlib import get_logger\n\nfrom authentik.core.auth import InbuiltBackend\nfrom authentik.core.models import User\nfrom authentik.sources.ldap.models import LDAPSource\n\nLOGGER = get_logger()\nLDAP_DISTINGUISHED_NAME = \"distinguishedName\"\n\n\nclass LDAPBackend(InbuiltBackend):\n    \"\"\"Authenticate users against LDAP Server\"\"\"\n\n    def authenticate(self, request: HttpRequest, **kwargs):\n        \"\"\"Try to authenticate a user via ldap\"\"\"\n        if \"password\" not in kwargs:\n            return None\n        for source in LDAPSource.objects.filter(enabled=True):\n            LOGGER.debug(\"LDAP Auth attempt\", source=source)\n            user = self.auth_user(source, **kwargs)\n            if user:\n                self.set_method(\"ldap\", request, source=source)\n                return user\n        return None\n\n    def auth_user(self, source: LDAPSource, password: str, **filters: str) -> Optional[User]:\n        \"\"\"Try to bind as either user_dn or mail with password.\n        Returns True on success, otherwise False\"\"\"\n        users = User.objects.filter(**filters)\n        if not users.exists():\n            return None\n        user: User = users.first()\n        if LDAP_DISTINGUISHED_NAME not in user.attributes:\n            LOGGER.debug(\"User doesn't have DN set, assuming not LDAP imported.\", user=user)\n            return None\n        # Either has unusable password,\n        # or has a password, but couldn't be authenticated by ModelBackend.\n        # This means we check with a bind to see if the LDAP password has changed\n        if self.auth_user_by_bind(source, user, password):\n            # Password given successfully binds to LDAP, so we save it in our Database\n            LOGGER.debug(\"Updating user's password in DB\", user=user)\n            user.set_password(password, signal=False)\n            user.save()\n            return user\n        # Password doesn't match\n        LOGGER.debug(\"Failed to bind, password invalid\")\n        return None\n\n    def auth_user_by_bind(self, source: LDAPSource, user: User, password: str) -> Optional[User]:\n        \"\"\"Attempt authentication by binding to the LDAP server as `user`. This\n        method should be avoided as its slow to do the bind.\"\"\"\n        # Try to bind as new user\n        LOGGER.debug(\"Attempting to bind as user\", user=user)\n        try:\n            # source.connection also attempts to bind\n            source.connection(\n                connection_kwargs={\n                    \"user\": user.attributes.get(LDAP_DISTINGUISHED_NAME),\n                    \"password\": password,\n                }\n            )\n            return user\n        except LDAPInvalidCredentialsResult as exc:\n            LOGGER.debug(\"invalid LDAP credentials\", user=user, exc=exc)\n        except LDAPException as exc:\n            LOGGER.warning(\"failed to bind to LDAP\", exc=exc)\n        return None\n", "path": "authentik/sources/ldap/auth.py"}]}