LDAP bind

LDAP bind is the right factor for enterprise deployments where the authoritative user store is Active Directory, OpenLDAP, or a similar directory server. The application does not own user passwords; the directory does. The verification mechanism is a simple bind against the directory with the user's distinguished name and password; if the bind succeeds, the user has authenticated.

The feature flag is ldap (off by default), enabled with features = ["ldap"] on the axess facade.

When LDAP fits

LDAP fits when three conditions hold:

  • The authoritative user identities live in an LDAP directory you can reach.
  • The directory administrators have agreed to allow simple binds from your deployment network.
  • The directory speaks LDAP, rather than a protocol that wraps LDAP semantics. SAML and OIDC route through the OAuth factor instead.

When those conditions hold, LDAP gives you authentication as a service from the directory, and you never store a user password. New employees added to the directory can log in immediately; departed employees removed from the directory lose access immediately. The directory is the source of truth.

When those conditions do not hold (a SaaS deployment where users come from many organisations, a directory you cannot reach over a stable network, an authoritative store that is not LDAP), the right answer is OAuth or OIDC against an IdP that the organisation does support.

Configuration

LdapProviderConfig carries the connection details:

pub struct LdapProviderConfig {
    pub url: String,                          // ldaps://ad.example.com:636
    pub bind_dn_template: String,             // "uid={user},ou=people,dc=example,dc=com"
    pub starttls: bool,                       // upgrade ldap:// to TLS via STARTTLS
    pub connection_timeout: Duration,         // typical 5-10 seconds
    pub group_search: Option<LdapGroupSearch>,
}

url is the directory's URL. The ldaps:// scheme means TLS is established at the transport layer (port 636 by default); the ldap:// scheme means cleartext, possibly upgraded to TLS via STARTTLS. Cleartext without STARTTLS is acceptable only on a private network where the directory traffic does not leave a trusted segment; production deployments use one of the encrypted forms.

bind_dn_template is the pattern axess uses to construct a user's distinguished name from their login identifier. The string {user} in the template is replaced with the identifier the user typed. The example above turns the username alice into the DN uid=alice,ou=people,dc=example,dc=com, which is then used in the bind request.

starttls triggers a STARTTLS upgrade after the initial cleartext connection establishes. The mechanism is widely supported and is the right choice when the directory accepts both cleartext and TLS on the same port (usually 389). When the directory exposes a separate TLS port (usually 636), use ldaps:// instead and leave this false.

connection_timeout bounds how long a bind attempt may take. Five to ten seconds is typical. Longer timeouts admit slow failure modes into the login path; shorter timeouts produce spurious failures when the directory is briefly slow. Tune to match the directory's observed latency.

group_search is optional. When set, after a successful bind axess performs an additional search to enumerate the user's group memberships. The result is returned alongside the bind outcome and can be used to populate the user's authorisation attributes.

pub struct LdapGroupSearch {
    pub base_dn: String,           // "ou=groups,dc=example,dc=com"
    pub filter_template: String,   // "(member={dn})"
    pub group_attr: String,        // "cn" -- attribute identifying the group
}

filter_template interpolates {dn} (the bound user's DN) or {user} (the original identifier) into an LDAP filter. The example filter (member={dn}) matches groups that list the user's DN in their member attribute, which is the OpenLDAP convention. Active Directory typically uses memberOf on the user record itself instead, in which case the group search is unnecessary because the groups are already attributes of the user.

The verification flow

The user submits a username and password to your handler, which calls RequestAuthnService::verify_factor with the LDAP bind credential; axess expands the bind DN template with the username, opens a TLS connection to the directory, performs a simple bind with the constructed DN and the user's password, optionally searches for groups, and unbinds.

A successful bind transitions the session as any factor would: the state machine calls advance_factor, which returns Completed if LDAP was the last required factor or StillAuthenticating if more factors are required. A failed bind returns FactorOutcome::InvalidCredential, and the user sees the standard failed-login message.

The connection model is per-attempt. Each bind opens a fresh TLS connection, performs the bind, and closes. There is no connection pooling. The trade-off is operational simplicity (no pool to size, no idle-connection management) against per-attempt latency (a TLS handshake on each login). For most deployments the latency is acceptable; busy directories with thousands of binds per second benefit from a connection pool at the network layer (HAProxy, nginx) rather than in your process.

Mixing LDAP with other factors

LDAP can be the only factor in a method (the directory's bind is the entire authentication), or it can be one factor in a chain.

A common shape in enterprise deployments is LDAP followed by TOTP. The user enters their LDAP credentials, the directory verifies them, and then axess prompts for the user's TOTP code. The TOTP secret is stored in axess's own factor store (not in LDAP), under the user's scope. The combination gives directory-managed passwords with an application-managed second factor; the directory does not need to know about TOTP, and you do not need to know about the password.

A variation is LDAP followed by AnyOf(vec![Totp, Fido2]), allowing the user to register a passkey alongside or instead of TOTP. The flow is otherwise unchanged.

Threat model

LDAP bind is robust against the same attacks any second-factor method is robust against: credential reuse from other services, local password lists, offline brute-forcing of a stolen hash (the hash never leaves the directory).

It is weak against attacks the directory itself is weak against. A directory that allows anonymous binds is vulnerable to attribute enumeration. A directory whose bind path is misconfigured to accept empty passwords for any DN is catastrophically vulnerable. The defence is operational: configure the directory correctly, audit periodically, and treat the LDAP factor's security as a function of the directory's security posture.

The application also has to be careful about what it logs. The bind password should never appear in application logs at any level, including trace. Axess does not log it; adopters' own login handlers need to make the same guarantee. The standard pattern is to mark the password field as zeroized and to route it directly into the verifier without touching it again.

Troubleshooting

SymptomUsuallyWhat to do
"Invalid credentials" for passwords you know are goodThe bind DN template. Active Directory usually expects userPrincipalName (an email address) or sAMAccountName (a short login name), not a constructed DNTry {user}@example.com in place of uid={user},ou=people,dc=example,dc=com
The connection opens, then the bind times outThe directory is loaded, or a middlebox is inspecting the connection and buffering slowly. The timeout fires and the user sees a generic failureInspect the network path
The group search comes back emptyA wrong filter_template, or a bound user without permission to read group membership. OpenLDAP often wants an explicit ACL for a user to enumerate their own groups; Active Directory usually grants itRun the same search through a known-good LDAP client
TLS fails on certificate validationThe directory's certificate is signed by a private CA your trust store does not carryAdd the CA through SSL_CERT_FILE or SSL_CERT_DIR

Further reading

Factors and methods covers the composition machinery this chapter exercises. Identity store implementation covers how user records referenced by LDAP get provisioned in your identity store (typically just-in-time on first successful LDAP login). Multi-tenancy covers the case where different tenants federate to different directories.