# How the LDAP Connection Handler in Doom Manages Connection Failures

> Doom's LDAP connection handler uses a two-stage fallback: unencrypted LDAP then LDAPS. It converts ldap3 exceptions to actionable errors, ensuring robust connection management.

- Repository: [000pp/doom](https://github.com/000pp/doom)
- Tags: internals
- Published: 2026-02-22

---

**Doom's LDAP connection handler implements a resilient two-stage fallback strategy that first attempts unencrypted LDAP on port 389 with NTLM authentication and automatic encryption, then transparently falls back to LDAPS on port 636 when the server demands stronger authentication, converting all low-level `ldap3` exceptions into clear, user-actionable error messages.**

The `000pp/doom` repository contains a robust LDAP integration module located in [`src/doom/protocols/ldap.py`](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py). This implementation demonstrates enterprise-grade connection resilience by handling authentication failures through a structured fallback mechanism rather than failing immediately on the first refused connection.

## Two-Stage Connection Architecture

The handler follows a deliberate two-stage approach designed to maximize compatibility with diverse Active Directory configurations while maintaining security through opportunistic encryption.

### Stage 1: Plain LDAP with Opportunistic Encryption

Initially, the code constructs an `ldap3.Server` instance targeting port 389 without SSL ([lines 22-24](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L22-L24)). It then creates an `ldap3.Connection` using NTLM authentication with explicit `session_security="ENCRYPT"` ([lines 26-34](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L26-L34)). This configuration attempts to establish an encrypted session over the standard LDAP port before considering more restrictive transport layers.

### Stage 2: Automatic LDAPS Fallback

When the initial bind raises an `LDAPBindError` containing the string "strongerAuthRequired", the handler automatically prepares a TLS-wrapped server configuration ([lines 15-21](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L15-L21)) and attempts a second connection on port 636 ([lines 40-44](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L40-L44)). This fallback happens transparently to the caller, ensuring connectivity against servers that reject unsigned binds.

## Granular Error Classification and Exception Translation

Rather than exposing raw `ldap3` exceptions to application layers, the handler implements specific detection logic to categorize failures and raise descriptive Python exceptions.

### Strong Authentication Requirements

If the first stage encounters an `LDAPBindError` that does not indicate "strongerAuthRequired", the code catches this in [lines 40-42](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L40-L42) and re-raises it as a generic `Exception` with a clear "LDAP bind error" message. This distinguishes between connectivity issues and security policy violations.

### Invalid Credentials Handling

Both connection stages explicitly catch `LDAPInvalidCredentialsResult` ([lines 44-45](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L44-L45) for LDAP, [lines 61-62](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L61-L62) for LDAPS). Upon detection, the handler raises `Exception("Invalid credentials provided")`, allowing UI layers to present specific authentication failure messages without parsing LDAP result codes.

### LDAPS-Specific Diagnostic Failures

When the fallback LDAPS attempt fails for reasons other than invalid credentials, the second `except LDAPBindError` block ([lines 64-65](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L64-L65)) captures the error and re-raises it with the prefixed message `LDAPS bind failed: …`. This provides network administrators with precise diagnostic information distinguishing between standard LDAP and secure LDAP transport failures.

## Base DN Extraction Strategies

Upon successful connection, the handler extracts the base distinguished name through protocol-specific methods. For standard LDAP connections, it retrieves `base_dn` from `server.info.naming_contexts` ([lines 37-38](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L37-L38)). When operating via LDAPS, the code accesses the `defaultNamingContext` attribute from the server's `other` dictionary ([lines 58-59](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L58-L59)).

## NTLM Hash Normalization

Before any network activity, the handler inspects the provided password for 32-character hexadecimal strings indicating raw NTLM hashes. When detected, it automatically prefixes the hash with the constant value `aad3b435b51404eeaad3b435b51404ee:` ([lines 12-14](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L12-L14)), forming a properly formatted NTLM hash string compatible with the `ldap3` library's NTLM authentication mechanism.

## Implementation Examples

The following patterns demonstrate safe integration of the LDAP handler in application code.

### Safe Connection Handling

```python
from doom.protocols.ldap import get_ldap_connection, safe_ldap_attr

try:
    conn, base_dn = get_ldap_connection(
        host="ldap.example.com",
        username="jdoe",
        password="Password123",   # or a raw NTLM hash

        domain="EXAMPLE"
    )
    print(f"Connected. Base DN = {base_dn}")

    # Fetch an entry and read attributes robustly

    conn.search(search_base=base_dn,
                search_filter='(sAMAccountName=jdoe)',
                attributes=['displayName', 'mail'])
    entry = conn.entries[0]
    display_name = safe_ldap_attr(entry, "displayName", fallback="N/A")
    email = safe_ldap_attr(entry, "mail", fallback="N/A")
    print(f"{display_name} <{email}>")

except Exception as exc:
    # All connection‑failure cases funnel here with a clear message

    print(f"LDAP error: {exc}")

```

### UI-Level Error Handling

```python
def login(username, password):
    try:
        conn, _ = get_ldap_connection(
            host="ldap.corp.local",
            username=username,
            password=password,
            domain="CORP"
        )
        # Proceed with application logic …

    except Exception as e:
        if "Invalid credentials" in str(e):
            show_error("Wrong username or password.")
        else:
            show_error(f"Could not connect to LDAP: {e}")

```

## Summary

- **Two-stage fallback**: The handler attempts LDAP on port 389 before automatically falling back to LDAPS on port 636 when stronger authentication is required.
- **Exception translation**: Low-level `ldap3` errors are mapped to clear Python exceptions, distinguishing between invalid credentials, policy violations, and transport failures.
- **Automatic hash formatting**: Raw 32-character NTLM hashes are automatically prefixed with the standard `aad3b435b51404eeaad3b435b51404ee:` string before authentication.
- **Flexible base DN retrieval**: The implementation adapts its base DN extraction logic based on whether the connection uses standard LDAP or LDAPS.
- **Located in**: [`src/doom/protocols/ldap.py`](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py) within the `000pp/doom` repository.

## Frequently Asked Questions

### What triggers the LDAPS fallback in Doom's connection handler?

The fallback activates when the initial LDAP connection on port 389 raises an `LDAPBindError` containing the string "strongerAuthRequired". This indicates the server refuses the bind without channel encryption, prompting the handler to retry using TLS-wrapped LDAPS on port 636 according to the logic in [lines 40-44](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L40-L44).

### How does Doom handle invalid LDAP credentials?

The code explicitly catches `LDAPInvalidCredentialsResult` exceptions in both the LDAP and LDAPS connection blocks ([lines 44-45](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L44-L45) and [lines 61-62](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L61-L62)). It translates these into a generic Python `Exception` with the message "Invalid credentials provided", enabling UI layers to display user-friendly authentication errors without importing LDAP-specific constants.

### Can Doom's LDAP handler process raw NTLM hashes?

Yes. Before connection attempts, the handler checks if the password parameter matches a 32-character hexadecimal pattern. When detected, it automatically prefixes the hash with `aad3b435b51404eeaad3b435b51404ee:` ([lines 12-14](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L12-L14)), converting it into the full NTLM format required by the `ldap3` library's NTLM authentication mechanism.

### Where does the handler retrieve the base DN after connection?

For standard LDAP connections, the base DN is extracted from `server.info.naming_contexts` ([lines 37-38](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L37-L38)). When connecting via LDAPS, the handler retrieves it from the `defaultNamingContext` key within the server's `other` attribute dictionary ([lines 58-59](https://github.com/000pp/doom/blob/main/src/doom/protocols/ldap.py#L58-L59)).