How to Do Password Authentication for a User Using Ldap

How to do password authentication for a user using LDAP?

This is not really the right way to perform a password check on LDAP, what you should do is attempt to bind using the dn obtained from the first search and the password supplied.

i.e. you perform a second bind to verify the password. If the bind fails then the password is incorrect.

Something akin to:

    if ( (dn = ldap_get_dn( ld, e )) != NULL ) {
printf( "dn: %s\n", dn );
/* rebind */
ldap_initialize(&ld2, LDAP_SERVER);
rc = ldap_simple_bind_s(ld2, dn, "secret");
printf("%d\n", rc);
if (rc != 0) {
printf("Failed.\n");
} else {
printf("Works.\n");
ldap_unbind(ld2);
}
ldap_memfree( dn );
}

For security reasons indicating that the username is incorrect (i.e. the search for the user account fails) is generally considered excessive disclosure, and should be avoided.

How to safely authenticate a user using LDAP?

Is attempting to bind to the LDAP server enough to verify credentials?

From the LDAP protocol side, yes, and many systems already rely on this behavior (e.g. pam_ldap for Linux OS-level authentication against an LDAP server). I've never heard of any server where the bind result would be deferred until another operation.

From the ldap3 module side I'd be more worried, as in my experience initializing a Connection did not attempt to connect – much less bind – to the server until I explicitly called .bind() (or unless I specified auto_bind=True), but if your example works then I assume that using a with block does this correctly.

In old code (which holds a persistent connection, no 'with') I've used this, but it may be outdated:

conn = ldap3.Connection(server, raise_exceptions=True)
conn.bind()

(For some apps I use Apache as a reverse proxy and its mod_auth_ldap handles LDAP authentication for me, especially when "is authenticated" is sufficient.)

Am I opening myself up to injection attacks? If so, how to properly mitigate them?

Well, kind of, but not in a way that would be easily exploitable. The bind DN is not a free-form query – it's only a weird-looking "user name" field and it must exactly match an existing entry; you can't put wildcards in it.

(It's in the LDAP server's best interests to be strict about what the "bind" operation accepts, because it's literally the user-facing operation for logging into an LDAP server before anything else is done – it's not just a "password check" function.)

For example, if you have some users at OU=Ops and some at OU=Superops,OU=Ops, then someone could specify Foo,OU=Superops as their username resulting in UID=Foo,OU=Superops,OU=Ops, as the DN – but they'd still have to provide the correct password for that account anyway; they cannot trick the server into using one account's privileges while checking another account's password.

However, it's easy to avoid injection regardless. DN component values can be escaped using:

  • ldap3: ldap3.utils.dn.escape_rdn(string)
  • python-ldap: ldap.dn.escape_dn_chars(string)

That being said, I dislike "DN template" approach for a completely different reason – its rather limited usefulness; it only works when all of your accounts are under the same OU (flat hierarchy) and only when they're named after the uid attribute.

That may be the case for a purpose-built LDAP directory, but on a typical Microsoft Active Directory server (or, I believe, on some FreeIPA servers as well) the user account entries are named after their full name (the cn attribute) and can be scattered across many OUs. A two-step approach is more common:

  1. Bind using your app's service credentials, then search the directory for any "user" entries that have the username in their uid attribute, or similar, and verify that you found exactly one entry;
  2. Unbind (optional?), then bind again with the user's found DN and the provided password.

When searching, you do have to worry about LDAP filter injection attacks a bit more, as a username like foo)(uid=* might give undesirable results. (But requiring the results to match exactly 1 entry – not "at least 1" – helps with mitigating this as well.)

Filter values can be escaped using:

  • ldap3: ldap3.utils.conv.escape_filter_chars(string)
  • python-ldap: ldap.filter.escape_filter_chars(string)

(python-ldap also has a convenient wrapper ldap.filter.filter_format around this, but it's basically just the_filter % tuple(map(escape_filter_chars, args)).)

The escaping rules for filter values are different from those for RDN values, so you need to use the correct one for the specific context. But at least unlike SQL, they are exactly the same everywhere, so the functions that come with your LDAP client module will work with any server.

Is TLS properly configured?

ldap3/core/tls.py looks good to me – it uses ssl.create_default_context() when supported, loads the system default CA certificates, so no extra configuration should be needed. Although it does implement custom hostname checking instead of relying on the ssl module's check_hostname so that's a bit weird. (Perhaps the LDAP-over-TLS spec defines wildcard matching rules that are slightly incompatible with the usual HTTP-over-TLS ones.)


An alternative approach instead of manually escaping DN templates:

dn = build_dn({"CN": f"{last}, {first} ({username})"},
{"OU": "Faculty of Foo and Bar (XYZF)"},
{"OU": "Staff"},
ad.BASE_DN)
def build_dn(*args):
components = []
for rdn in args:
if isinstance(rdn, dict):
rdn = [(a, ldap.dn.escape_dn_chars(v))
for a, v in rdn.items()]
rdn.sort()
rdn = "+".join(["%s=%s" % av for av in rdn])
components.append(rdn)
elif isinstance(rdn, str):
components.append(rdn)
else:
raise ValueError("Unacceptable RDN type for %r" % (rdn,))
return ",".join(components)

How to Authenticate a user from a LDAP directory with his username and password?

When a client connects to an LDAP directory server, the connection authorization state is set to anonymous. LDAP clients use the BIND request to change the authorization state of a connection. Each BIND request changes the connection authorization state to anonymous, and each successful BIND request changes the authorization state of the connection to the authorization state associated with the successful BIND request, that is, that of the user. Failed BIND requests leave the connection in an anonymous state.

The client constructs a BindRequest (either simple BIND request or a form of a SASL bind request) transmits to the LDAP directory server and interprets the response from the server, including any response controls that might accompany the BIND response. A result code of zero in the BIND response indicates that the LDAP directory server matched the credentials and the user is authenticated.

If the distinguished name of the user is known, then a BIND request can be constructed from the distinguished name. If not known, the LDAP client must construct a search request, transmit it the server and interpret the response. The distinguished name is always included in a successful search response that returns at least one entry. Then use the distinguished returned in the search response to construct the BIND request as above.

In the simplest case:

// exception handling is not shown
final String dn = ....;
final byte[] password = ....;
final BindRequest bindRequest = new SimpleBindRequest(dn,password);
final LDAPConnection ldapConnection = new LDAPConnection(hostname,port);
final BindResult bindResult = ldapConnection.bind(bindRequest);
final ResultCode resultCode = bindResult.getResultCode();
if(resultCode.equals(ResultCode.SUCCESS))
{
// user is authenticated
}
ldapConnection.close();

If you are using Java, you should use the UnboundID LDAP SDK (JNDI should not be used for new code).

see also

  • LDAP: Using Java to BIND
  • LDAP: Programming Practices

authenticate user in LDAP with email and password

Using the UnboundID LDAP SDK, this simple piece of code searches for the entry. If there is one entry that has the known email address, BIND using that DN (the password has to come from someplace else). Nothing happens (authenticated is false) is there is more one entry that matches the search parameters or if no entries match the search parameters. This code assumes that baseObject is dc=example,dc=com, subtree search is required, and the attribute with the email address has an alias mail. The code also assumes there is a bindDN and bindPassword that has sufficient access rights to search for the user with the email address. The email address for which it searches is assumed to be babs.jensen@example.com.

Exceptions are ignored throughout.

String baseObject = "dc=example,dc=com";
String bindDN = "dn-with-permission-to-search";
String bindPassword = "password-of-dn-with-permission-to-search";

// Exceptions ignored.
LDAPConnection ldapConnection =
new LDAPConnection(hostname,port,bindDN,bindPassword);

String emailAddress = "babs.jensen@example.com";
String filterText = String.format("mail=%s",emailAddress);
SearchRequest searchRequest = new SearchRequest(baseObject,
SearchScope.SUB,filterText,"1.1");
SearchResult searchResult = ldapConnection.search(searchRequest);

boolean authenticated = false;
if(searchResult.getEntryCount() == 1)
{
// There is one entry with that email address
SearchResultEntry entry = searchResult.getSearchEntries().get(0);

// Create a BIND request to authenticate. The password has
// has to come from someplace outside this code
BindRequest bindRequest =
new SimpleBindRequest(entry.getDN(),password);
ldapConnection.bind(bindRequest);
authenticated = true;
}
else if(searchResult.getEntryCount() > 1)
{
// more than one entry matches the search parameters
}
else if(searchResult.getEntryCount() == 0)
{
// no entries matched the search parameters
}

Authentification with ldap without user's password

Unauthenticated bind (a seemingly successful bind when you supply a userID and null password) may be enabled in your directory. If you are using OpenLDAP, as the quesstion tags indicate, check slapd.conf for allow bind_anon_cred.

Unless there is a specific need for unauthenticated bind, I disable it on the directory servers. In the rare cases where unauthenticated bind is required, all applications authenticating against the directory need to validate user input before attempting to bind -- that is, verify that the input username and password values are not null.

Able to login using Old password in LDAP

Typically it happens due to default timeout of 60 min for old passwords or due to replication. See more here.

How does the authentication process of applications via LDAP-directory works

You'll want to use Option 1!!!!!

Option 2 has some disadvantages:

  1. You'll need an admin account that can read the passwords to be able to compare them to the one you hashed. That means that the application is able to read the password hashed. I'd consider that a bad idea.
  2. You'll circumvent additional security measurements implemented. The password you are checking might be the right one. But due to other policies the user might be locked and should not be able to login. And failed attempts will not count towards a possible lockout.
  3. The hashing algorithm might change in future to one you haven't implemented yet.

You might be able to circumvent those obstacles but you will need aditional code to implement that. And that effort has already been taken and is readily availabel when you use option 1



Related Topics



Leave a reply



Submit