The authentication lesson checked a submitted username and password against your own users table. A lot of real enterprise applications don't have their own user table at all, they authenticate against a company's existing directory server, commonly Microsoft Active Directory, over LDAP (Lightweight Directory Access Protocol). cfldap is ColdFusion's interface to that.
Learning Objectives
After completing this lesson, you'll be able to:
- Explain what an LDAP directory and a distinguished name (DN) actually are.
- Query directory entries with cfldap action="query".
- Authenticate a user against Active Directory instead of your own table.
- Add, modify, and delete directory entries.
- Prevent LDAP injection in a filter built from user input with encodeForLDAP().
What an LDAP Directory Actually Is
An LDAP directory is a hierarchical database of entries, think of it like a folder tree, where every entry (a user, a group, a computer) has a unique path called a distinguished name (DN), such as cn=John Smith,ou=Sales,dc=example,dc=com. Reading that right to left: dc=example,dc=com is the domain, ou=Sales is an organizational unit (a folder) inside it, and cn=John Smith is the specific entry.
How a cfldap Query Flows
Build the Filter
e.g. (sAMAccountName=jsmith)
cfldap Connects
server, port, credentials
Directory Searches
starting at the DN in start=
Entries Come Back
as a normal CFML query
Querying Directory Entries
<cfldap server="ldap.example.com"
port="389"
action="query"
name="results"
start="dc=example,dc=com"
scope="subtree"
filter="(sn=Smith)"
attributes="cn,sn,givenName,mail,title">
<cfoutput query="results">
#cn# - #mail#<br>
</cfoutput>Key Attributes
| Attribute | Meaning |
|---|---|
| server | Hostname or IP of the directory server (required) |
| port | Default 389, or 636 for LDAPS (SSL) |
| action | query, add, modify, modifyDN, or delete |
| start | The DN to begin searching from (required for query) |
| scope | onelevel (default), base, or subtree |
| filter | Search criteria, e.g. "(sn=Smith)" |
| attributes | Comma-delimited list of attributes to return, or "*" for all |
| secure | "CFSSL_BASIC" enables SSL-encrypted authentication |
| timeout | Milliseconds to wait before failing (default 60000) |
A Real Example: Authenticating Against Active Directory
Instead of hashing a password and comparing it to your own users table, the query itself IS the authentication check here: if the bind (the connection using the submitted username and password) succeeds and a matching entry comes back, the credentials were correct. This is Adobe's own documented pattern for checking a login against Active Directory.
cfldap(
server = "ldap.mycompany.com",
port = 636,
action = "QUERY",
name = "qLDAPLookup",
secure = "CFSSL_BASIC",
username = "MYDOMAIN\" & encodeForLDAP(arguments.username),
password = arguments.password,
start = "dc=mycompany,dc=com",
attributes = "cn,userPrincipalName,title,mail",
filter = "(sAMAccountName=" & encodeForLDAP(arguments.username) & ")"
);
if (qLDAPLookup.recordCount EQ 1) {
// Credentials were valid, the bind succeeded and found exactly one matching user
cfloginuser(name = arguments.username, password = arguments.password, roles = "employee");
}encodeForLDAP() around any user-supplied value going into a filter is not optional, it's the direct equivalent of cfqueryparam for SQL: without it, someone could inject LDAP filter syntax through the username field.
Adding, Modifying, and Deleting Entries
| action | Required Attributes |
|---|---|
| add | dn, attributes (the new entry's data) |
| modify | dn, attributes, modifytype (add, delete, or replace) |
| modifyDN | dn (changes an entry's distinguished name/location) |
| delete | dn (the entry to remove) |
<cfldap action="modify"
server="ldap.example.com"
dn="cn=John Smith,ou=Sales,dc=example,dc=com"
attributes="title=Senior Sales Manager"
modifytype="replace">Handling Special Characters in Attribute Values
When an attribute value itself contains a comma or semicolon, those characters collide with the characters cfldap normally uses to separate multiple values and multiple attribute pairs. separator and delimiter let you pick different characters instead, exactly for this case, per Adobe's own advanced topics documentation.
<cfldap action="add"
server="ldap.example.com"
dn="cn=Example Inc,ou=Companies,dc=example,dc=com"
attributes="o=Example, Inc.|description=Sales; Marketing"
separator="|">That's a distinct concern from encodeForLDAP(): separator/delimiter reshape how attribute values themselves are parsed, while encodeForLDAP() escapes user input so it can't be interpreted as LDAP filter syntax at all.
Common Beginner Mistakes
Building a filter by concatenating raw user input
filter = "(sAMAccountName=" & arguments.username & ")" without encodeForLDAP() is vulnerable to LDAP injection, the same class of problem as building SQL without cfqueryparam. Always encode user-supplied values going into a filter.
Forgetting that scope="onelevel" only searches one level deep
onelevel searches only the immediate children of start, not the full tree below it. subtree searches everything underneath, which is usually what a "find this user anywhere in the directory" query actually needs.
Assuming a failed bind throws a catchable exception automatically
Wrap authentication attempts in cftry/cfcatch, an invalid username/password combination during the connection itself typically surfaces as an exception rather than an empty query result.
Using plain LDAP (port 389) to send credentials in production
Port 389 is unencrypted. secure="CFSSL_BASIC" with port 636 (LDAPS) encrypts the connection, sending a real password over plain LDAP exposes it on the network exactly like plain HTTP would.
Best Practices
- Always wrap user input going into a filter with encodeForLDAP().
- Use secure="CFSSL_BASIC" (port 636) rather than plain LDAP for any connection carrying credentials.
- Use scope="subtree" when searching needs to cover an entire branch, not just its immediate children.
- Wrap cfldap authentication calls in cftry/cfcatch, since a failed bind commonly throws rather than returning an empty result.
Interview Questions
How would you authenticate a user against Active Directory instead of your own database?
Run a cfldap query using the submitted username and password as the bind credentials, with a filter matching that username (e.g. sAMAccountName). If the bind succeeds and returns a matching entry, the credentials were valid, the directory server did the actual password verification, not your code.
What does encodeForLDAP() protect against, and why does it matter?
LDAP injection, where unescaped user input in a filter string gets interpreted as LDAP filter syntax instead of a literal value, potentially letting an attacker manipulate the search logic (similar in spirit to SQL injection). It's the same category of fix as cfqueryparam, applied to LDAP filters instead of SQL.
What's the difference between scope="onelevel" and scope="subtree"?
onelevel searches only the immediate children of the start DN. subtree searches that entire branch recursively, every level beneath it. A search meant to find an entry anywhere in a organizational unit's structure needs subtree, not onelevel.
Summary
In this lesson, you queried an LDAP directory with cfldap, authenticated a user against Active Directory by using their submitted credentials as the bind itself, added/modified/deleted directory entries, and covered a real security practice: encoding user input with encodeForLDAP() before it goes into a filter, to prevent LDAP injection.
What's Next?
The next lessons cover cfcollection, cfindex, and cfsearch: building and querying a full-text search collection directly from ColdFusion.