Connecting web applications to the directory: LDAP bind in PHP, Python, Java, Node.js, and Go, Nginx and Apache authentication, and enterprise SSO with CAS, SAML, and Keycloak.

In episode 20 you connected the mail server to LDAP. Web applications are the most common consumers in a real organization — from a company portal to internal dashboards. Episode 21 shows LDAP integration from several angles: the bind-and-search pattern in the five most popular language stacks, server-level authentication in Nginx and Apache, and the enterprise single sign-on protocols that put LDAP behind a unified login.
Almost every application integrates with LDAP through the same two-step pattern:
cn=admin,dc=example,dc=com) or as the user directly.(uid=username).userPassword.The application rarely does raw ldapsearch — it uses a library that speaks LDAP and hides the protocol.
ldapsearch -x -D cn=admin,dc=example,dc=com -W \
-b ou=people,dc=example,dc=com "(uid=budi)" uid userPasswordPHP has a mature LDAP extension (php-ldap on Debian and Ubuntu). The classic flow:
<?php
$ldapconn = ldap_connect("ldap://ldap.example.com");
ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
$ldapbind = ldap_bind($ldapconn, "cn=admin,dc=example,dc=com", "secret");
$sr = ldap_search($ldapconn, "ou=people,dc=example,dc=com", "(uid=" . ldap_escape($username, "", LDAP_ESCAPE_FILTER) . ")");
$info = ldap_get_entries($ldapconn, $sr);
if (ldap_bind($ldapconn, $info[0]["dn"], $password)) {
echo "Authenticated as " . $info[0]["dn"];
} else {
echo "Authentication failed";
}
ldap_close($ldapconn);
?>The bind-then-verify flow above authenticates the user's own credentials against their own DN — the correct way to check a password. Note ldap_escape used on the filter input to prevent LDAP injection.
Python uses the python3-ldap package (or its modern successor ldap3):
import ldap3
server = ldap3.Server("ldap://ldap.example.com")
conn = ldap3.Connection(server, user="cn=admin,dc=example,dc=com", password="secret")
conn.bind()
conn.search("ou=people,dc=example,dc=com", "(uid=budi)", attributes=["uid", "cn", "mail"])
print(conn.entries)
conn.unbind()For user authentication, bind directly with the user's DN and password:
conn = ldap3.Connection(server, user="uid=budi,ou=people,dc=example,dc=com", password="password")
if conn.bind():
print("Authenticated")Java's standard solution is JNDI (Java Naming and Directory Interface). A minimal search:
import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://ldap.example.com");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=example,dc=com");
env.put(Context.SECURITY_CREDENTIALS, "secret");
DirContext ctx = new InitialDirContext(env);
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<?> results = ctx.search("ou=people,dc=example,dc=com", "(uid=budi)", ctls);
while (results.hasMore()) {
SearchResult sr = (SearchResult) results.next();
System.out.println(sr.getNameInNamespace());
}
ctx.close();Node.js uses libraries like ldapjs or ldapts. With ldapjs:
const ldap = require('ldapjs');
const client = ldap.createClient({ url: 'ldap://ldap.example.com' });
client.bind('cn=admin,dc=example,dc=com', 'secret', (err) => {
if (err) throw err;
const opts = { filter: '(uid=budi)', scope: 'sub' };
client.search('ou=people,dc=example,dc=com', opts, (err, res) => {
res.on('searchEntry', (entry) => {
console.log(entry.object);
client.unbind();
});
});
});Go uses the github.com/go-ldap/ldap/v3 module:
package main
import (
"crypto/tls"
"fmt"
"log"
ldap "github.com/go-ldap/ldap/v3"
)
func main() {
l, err := ldap.DialURL("ldap://ldap.example.com")
if err != nil {
log.Fatal(err)
}
defer l.Close()
err = l.Bind("cn=admin,dc=example,dc=com", "secret")
if err != nil {
log.Fatal(err)
}
search := ldap.NewSearchRequest(
"ou=people,dc=example,dc=com",
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
"(uid=budi)",
[]string{"uid", "cn", "mail"},
nil,
)
sr, err := l.Search(search)
if err != nil {
log.Fatal(err)
}
for _, entry := range sr.Entries {
fmt.Println(entry.GetAttributeValue("mail"))
}
}For applications that can't integrate LDAP themselves, authentication happens at the server:
ngx_http_auth_ldap (a third-party module) or by forwarding to an authenticating backend like LDAP-aware auth_request.location /protected/ {
auth_request /ldap-check;
proxy_pass http://backend;
}
location = /ldap-check {
internal;
proxy_pass http://127.0.0.1:8080/check;
}mod_authnz_ldap provides full LDAP authentication and authorization:<Location /private/>
AuthType Basic
AuthName "LDAP Login"
AuthBasicProvider ldap
AuthLDAPUrl "ldap://ldap.example.com/ou=people,dc=example,dc=com?uid"
AuthLDAPBindDN "cn=admin,dc=example,dc=com"
AuthLDAPBindPassword "secret"
Require valid-user
</Location>Apache is simpler for small deployments — no extra code, just configuration. Nginx plus a small LDAP-checking service scales more easily into a microservice architecture.
LDAP alone means every application implements its own login page. Enterprise SSO centralizes that:
In all cases, LDAP becomes the directory and the IdP becomes the guard — applications never see the password again.
cn=config admin, for application lookups.In this episode 21 you integrated web applications with LDAP: the bind-and-search pattern; LDAP libraries in PHP, Python, Java, Node.js, and Go; server-level authentication with Apache mod_authnz_ldap and Nginx auth_request; and enterprise SSO with CAS, SAML, and Keycloak.
Key takeaways:
In the next episode, episode 22, we step into the other major directory ecosystem: Active Directory & LDAP — how AD compares to OpenLDAP, and how to make Linux systems join an AD domain.