rustlavel-ldap 0.5.2

LDAP for Rustlavel: bind, search, and authenticating a user against a directory
Documentation

rustlavel-ldap: authenticate a person against a directory.

LDAP is how most organisations answer "who is this and what is their password", and an application that needs that answer should not need to learn ASN.1 to get it. So the package has two faces: [directory], which is one call, and [protocol] and [ber] underneath it for anything that call does not cover.

use rustlavel_ldap::prelude::*;

let directory = Directory::new("ldaps://dc.example.test")?
    .service_account("cn=svc,dc=example,dc=test", &service_password)
    .base_dn("ou=people,dc=example,dc=test")
    .username_attribute("uid")
    .filter(Filter::equals("objectClass", "inetOrgPerson"))
    .attributes(["cn", "mail"]);

let user = directory.authenticate(&username, &password).await?;
println!("{} is {}", user.dn(), user.value("cn").unwrap_or("unnamed"));

The protocol is written here

LDAP v3 (RFC 4511) is ASN.1 BER over TCP. BER is a serialisation format rather than cryptography, so rule one applies and it is written in [ber]: a small, strict subset that refuses everything RFC 4511 says a directory may not send. TLS is the exception, and comes from rustls with the aws_lc_rs provider and prefer-post-quantum, like every other TLS in this framework.

Three things this package is firm about

A simple bind sends the password in the clear. That is what a simple bind is — RFC 4511 §4.2 puts the password in an octet string with nothing around it. So one is refused over an unencrypted connection unless the caller has said, in as many words, that it is acceptable ([LdapConfig::allow_plaintext_password]). This is the most common LDAP deployment mistake there is, and it is invisible from the outside because everything works.

An empty password must not authenticate. LDAP treats a simple bind with a zero-length password as an anonymous bind, which succeeds — so a login form with a blank password field would report a successful login for any username that exists. This has been a real vulnerability in real products more than once. It is refused in [Operation::simple_bind], where the bytes are built, and again in [Directory::authenticate] before any I/O happens.

A username is a value, not text. The classic LDAP injection is a filter template with a name pasted into it: a username of * turns a lookup into a wildcard that matches every account. [Filter] is a type, and [Filter::encode] writes each value as a length-delimited BER octet string where * and ) have no syntax at all. [escape_filter_value] exists for the cases that genuinely need the RFC 4515 string form.