LDAP Authentication — Complete Enterprise Directory Guide
In this tutorial, you will learn about LDAP Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.
LDAP (Lightweight Directory Access Protocol) is a protocol for accessing and maintaining distributed directory information services, commonly used for centralized authentication against Active Directory, OpenLDAP, and other directory servers in enterprise environments.
What You'll Learn
By the end of this lesson, you will implement LDAP authentication against Active Directory and OpenLDAP, understand the directory information tree, construct proper LDAP queries, prevent LDAP injection, and handle LDAP search and bind operations.
Why It Matters
LDAP is the backbone of enterprise identity management. Most large organizations store user credentials in Active Directory or OpenLDAP. Durga Antivirus Pro integrates with corporate LDAP directories for enterprise customer SSO, allowing employees to authenticate with their existing corporate credentials.
Real-World Use
An employee signs into their company's expense reporting system. The system binds to the corporate Active Directory using a service account, searches for the user by their email, and then performs a bind as the user to verify their password. The expense tool never stores passwords.
LDAP Auth Flow
sequenceDiagram
participant User
participant App
participant LDAP as LDAP Server (AD/OpenLDAP)
User->>App: Enter username/password
App->>LDAP: Bind (service account)
LDAP-->>App: Bind successful
App->>LDAP: Search for user (mail=user@company.com)
LDAP-->>App: User DN: CN=Alice Smith,OU=Users,DC=company,DC=com
App->>LDAP: Bind as user DN with user's password
LDAP-->>App: Bind successful (password verified)
App-->>User: Authenticated
LDAP Authentication (Python)
from ldap3 import Server, Connection, ALL, core
import ssl
class LDAPAuthenticator:
def __init__(self, ldap_host, ldap_port=389, use_ssl=False, base_dn="DC=company,DC=com"):
self.server = Server(
ldap_host,
port=ldap_port if not use_ssl else 636,
use_ssl=use_ssl,
get_info=ALL,
)
self.base_dn = base_dn
def authenticate(self, username, password, search_filter=None):
try:
conn = Connection(
self.server,
user=self._get_service_account(),
password=self._get_service_password(),
auto_bind=True,
)
print(f"[LDAP] Connected to {self.server.host}")
search_filter = search_filter or f"(mail={username})"
conn.search(
search_base=self.base_dn,
search_filter=search_filter,
attributes=["cn", "mail", "dn", "memberOf"],
)
if len(conn.entries) == 0:
print(f"[LDAP] User not found: {username}")
return None
user_dn = conn.entries[0].entry_dn
user_attrs = conn.entries[0]
print(f"[LDAP] Found user: {user_dn}")
conn.unbind()
user_conn = Connection(
self.server,
user=user_dn,
password=password,
auto_bind=True,
)
print(f"[LDAP] Password verification: SUCCESS")
user_conn.unbind()
return {
"dn": user_dn,
"cn": str(user_attrs.cn),
"email": str(user_attrs.mail) if user_attrs.mail else username,
"groups": [str(g) for g in user_attrs.memberOf] if user_attrs.memberOf else [],
}
except core.exceptions.LDAPBindError as e:
print(f"[LDAP] Authentication failed: {e}")
return None
except Exception as e:
print(f"[LDAP] Error: {e}")
return None
def _get_service_account(self):
return "CN=svc_ldap,OU=Service Accounts,DC=company,DC=com"
def _get_service_password(self):
return "service-account-password"
LDAP with Node.js (Active Directory)
const ActiveDirectory = require("activedirectory2");
const config = {
url: "ldap://ad.company.com",
baseDN: "DC=company,DC=com",
username: "svc_ldap@company.com",
password: "service-account-password",
};
const ad = new ActiveDirectory(config);
function authenticateUser(username, password) {
return new Promise((resolve, reject) => {
ad.authenticate(username, password, (err, auth) => {
if (err) {
console.log(`[LDAP] Auth error: ${err.message}`);
return reject(err);
}
if (!auth) {
console.log(`[LDAP] Auth failed: ${username}`);
return resolve(null);
}
console.log(`[LDAP] Authenticated: ${username}`);
ad.findUser(username, (err, user) => {
if (err) {
return resolve({ authenticated: true });
}
resolve({
authenticated: true,
dn: user.dn,
cn: user.cn[0],
email: user.mail,
groups: user.memberOf || [],
enabled: !user.userAccountControl || !(user.userAccountControl & 2),
});
});
});
});
}
async function login(username, password) {
const result = await authenticateUser(username, password);
if (!result) {
return { error: "Invalid credentials" };
}
if (result.enabled === false) {
return { error: "Account is disabled" };
}
return { user: result };
}
LDAP Search and Attribute Mapping
class LDAPGroupChecker:
def __init__(self, auth):
self.auth = auth
def get_user_groups(self, username):
conn = Connection(
self.auth.server,
user=self.auth._get_service_account(),
password=self.auth._get_service_password(),
auto_bind=True,
)
conn.search(
search_base=self.auth.base_dn,
search_filter=f"(mail={username})",
attributes=["memberOf"],
)
if conn.entries:
return [str(g) for g in conn.entries[0].memberOf]
return []
def is_user_in_group(self, username, group_name):
groups = self.get_user_groups(username)
return any(group_name.lower() in g.lower() for g in groups)
def list_users_in_group(self, group_dn):
conn = Connection(
self.auth.server,
user=self.auth._get_service_account(),
password=self.auth._get_service_password(),
auto_bind=True,
)
conn.search(
search_base=self.auth.base_dn,
search_filter=f"(memberOf={group_dn})",
attributes=["cn", "mail"],
)
users = []
for entry in conn.entries:
users.append({
"cn": str(entry.cn),
"mail": str(entry.mail) if entry.mail else "",
})
return users
Common Mistakes
- Not using LDAPS (LDAP over SSL) exposes credentials in transit on the network.
- Constructing LDAP search filters with string concatenation leads to LDAP injection attacks.
- Using the user's credentials for the initial search bind instead of a service account.
- Not handling account lockout or disabled account status properly.
- Assuming all directory servers have the same schema and attribute names.
- Failing to close LDAP connections after authentication leads to connection pool exhaustion.
Practice Questions
- What is the difference between LDAP bind and LDAP search?
Bind authenticates to the LDAP server (verifies credentials). Search queries the directory for user records. The two-step Process: first bind as service account to search, then bind as the user to verify their password.
- What is LDAP injection and how do you prevent it?
LDAP injection is similar to SQL Injection — an attacker manipulates search filter input to modify the query logic. Prevent by using parameterized search filters or escaping special characters (*, (), &, |, !) in user input.
- How do you map LDAP attributes to application roles?
First, query the user's memberOf attribute during authentication. Then, map groups to roles in your application using a configuration file: {"CN=Admins,OU=Groups,...": "admin", "CN=Users,...": "user"}.
- Challenge: Build a complete LDAP authentication system with service account bind, user search, password verification, group-to-role mapping, LDAP injection prevention, connection pooling, and TLS encryption.
FAQ
Mini Project: LDAP Browser CLI
Build a CLI tool that connects to an LDAP server, allows browsing the directory tree, searching for users and groups, and testing authentication.
import sys
from ldap3 import Server, Connection, ALL
class LDAPBrowser:
def __init__(self, host, base_dn, username, password, use_ssl=True):
port = 636 if use_ssl else 389
server = Server(host, port=port, use_ssl=use_ssl, get_info=ALL)
self.conn = Connection(server, user=username, password=password, auto_bind=True)
self.base_dn = base_dn
def search(self, filter_str, attributes=None):
attrs = attributes or ["cn", "mail", "dn"]
self.conn.search(self.base_dn, filter_str, attributes=attrs)
for entry in self.conn.entries:
print(f"DN: {entry.entry_dn}")
for attr in attrs:
if attr != "dn" and entry[attr]:
print(f" {attr}: {entry[attr]}")
print()
def tree(self, dn=None):
dn = dn or self.base_dn
self.conn.search(dn, "(objectClass=*)", attributes=["ou", "cn", "objectClass"])
for entry in self.conn.entries:
indent = " " * (entry.entry_dn.count(",") - self.base_dn.count(","))
name = entry.ou.value if entry.ou else entry.cn.value if entry.cn else entry.entry_dn
print(f"{indent}{name}")
if "organizationalUnit" in entry.objectClass or "container" in entry.objectClass:
self.tree(entry.entry_dn)
def test_auth(self, username, password):
try:
test_conn = Connection(
Server(self.conn.server.host),
user=username, password=password, auto_bind=True
)
test_conn.unbind()
print("Auth: SUCCESS")
except Exception as e:
print(f"Auth: FAILED ({e})")
if __name__ == "__main__":
browser = LDAPBrowser(
sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
)
browser.search("(mail=*@company.com)")
What's Next
Compare SAML vs OAuth to understand when to use each protocol, then explore token storage for secure client-side token management.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro