Skip to content

Repository files navigation

codecov

fastapi-ldap

Production-ready LDAP authentication and authorization for FastAPI.

Features

  • Async-first design - No blocking LDAP calls in the event loop
  • Security by default - TLS enabled, no anonymous binds, fail-closed behavior
  • Enterprise-ready - LDAP and Active Directory compatible
  • Kubernetes-friendly - Health and readiness checks included
  • FastAPI-native - Idiomatic dependencies and lifespan management
  • Optional caching - Configurable TTL for authentication results
  • Type-safe - Full type hints, Python 3.10+

Installation

pip install fastapi-ldap

Quick Start

fromfastapiimportDepends, FastAPIfromfastapi_ldapimport (
LDAPAuth,
LDAPSettings,
get_current_user,
health_check,
readiness_check,
require_groups,
)
fromfastapi_ldap.modelsimportLDAPUser# Configure LDAP settingssettings=LDAPSettings(
ldap_url="ldaps://ldap.example.com:636",
ldap_base_dn="dc=example,dc=com",
bind_dn="cn=admin,dc=example,dc=com",
bind_password="secret",
)
# Create LDAP auth instanceldap_auth=LDAPAuth(settings)
# Create FastAPI app with lifespanapp=FastAPI(lifespan=ldap_auth.lifespan)
# Health checks@app.get("/health")asyncdefhealth():
returnawaithealth_check()
@app.get("/ready")asyncdefready():
returnawaitreadiness_check()
# Protected route@app.get("/protected")asyncdefprotected_route(user: LDAPUser=Depends(get_current_user)):
return {
"username": user.username,
"email": user.email,
"groups": list(user.groups),
}
# Route requiring specific groups@app.get("/admin")asyncdefadmin_route(
user: LDAPUser=Depends(require_groups("admins", "superusers"))
):
return {"message": f"Welcome, {user.username}!"}

Configuration

Configuration can be provided via environment variables (prefixed with LDAP_) or directly:

fromfastapi_ldapimportLDAPSettingssettings=LDAPSettings(
# Requiredldap_url="ldaps://ldap.example.com:636",
ldap_base_dn="dc=example,dc=com",
# Bind credentials (required unless allow_anonymous=True)bind_dn="cn=admin,dc=example,dc=com",
bind_password="secret",
# TLS (enabled by default)use_tls=True,
tls_ca_cert_file="/path/to/ca.crt",
# Search configurationuser_search_filter="(uid={username})", # Default for OpenLDAP# user_search_filter="(sAMAccountName={username})", # For Active Directory# Connection poolpool_size=10,
pool_timeout=30.0,
pool_max_idle_seconds=1800.0, # Discard idle connections after 30 min (AD/LDAP stale fix)# Caching (optional)cache_enabled=True,
cache_ttl=300, # 5 minutes
)

Environment Variables

All settings can be configured via environment variables:

export LDAP_URL="ldaps://ldap.example.com:636"export LDAP_BASE_DN="dc=example,dc=com"export LDAP_BIND_DN="cn=admin,dc=example,dc=com"export LDAP_BIND_PASSWORD="secret"export LDAP_CACHE_ENABLED="true"export LDAP_CACHE_TTL="300"export LDAP_POOL_MAX_IDLE_SECONDS="1800"

Important: Passwords with Special Characters

If your password contains special characters (e.g., $, %, &, *, (, ), \, etc.), make sure to properly quote the environment variable to prevent shell interpretation:

# ✅ Correct - use single quotes to prevent shell interpretationexport LDAP_BIND_PASSWORD='pass$word%test&more'# ✅ Also correct - use double quotes and escape special charsexport LDAP_BIND_PASSWORD="pass\$word%test&more"# ❌ Wrong - shell will interpret $word as a variableexport LDAP_BIND_PASSWORD=pass$word

Note: LDAP passwords can contain any characters, including special ones and Unicode. The fastapi-ldap library accepts passwords as-is without validation, as LDAP servers handle password validation. The only requirement is that passwords are properly set in environment variables to avoid shell interpretation issues.

Active Directory Example

bind_dn / bind_password are for a service account (connection pool and LDAP searches). End users authenticate with their own username and password via HTTP Basic Auth on protected routes — do not use the service password for user login.

settings=LDAPSettings(
ldap_url="ldaps://ad.example.com:636",
ldap_base_dn="dc=example,dc=com",
# Service account (UPN format recommended)bind_dn="ldap-service@example.com",
bind_password="service-account-password",
user_search_filter="(sAMAccountName={username})",
user_search_base="DC=example,DC=com", # Use domain root unless you intentionally restrict OUsgroup_search_filter="(member:1.2.840.113556.1.4.1941:={user_dn})", # Recursive group searchgroup_attribute="cn",
pool_max_idle_seconds=900, # Refresh idle connections; AD often drops them after ~15 min
)

HTTP Basic login accepts plain username, UPN (user@example.com), or NetBIOS (DOMAIN\user) — the client normalizes these for sAMAccountName search.

Connection Pool and Long-Running Services

LDAP and Active Directory often close idle TCP connections. Without refresh, a backend that runs for hours may start returning 401 Invalid credentials for valid users until restart.

fastapi-ldap mitigates this by:

  • Rebinding to the service account on every pool checkout and return
  • Discarding connections idle longer than pool_max_idle_seconds (default 1800 / 30 minutes)
  • Replacing dead connections and retrying authentication once on connection errors

For AD deployments with short idle timeouts, lower the idle limit:

LDAPSettings(
...
pool_max_idle_seconds=900, # 15 minutes
)

Set pool_max_idle_seconds=0 to disable age-based discard (not recommended for production AD).

API Reference

Dependencies

  • get_current_user - Get the current authenticated user (HTTP Basic Auth)
  • require_groups(*groups) - Require user to belong to at least one of the specified groups
  • require_roles(*roles) - Alias for require_groups (semantic clarity)

Models

  • LDAPUser - Immutable user object with:
    • dn - Distinguished Name
    • username - Username
    • email - Email address (optional)
    • display_name - Display name (optional)
    • groups - Frozen set of group names (from group search)
    • attributes - Additional LDAP attributes (str or list[str] for multi-valued fields such as AD memberOf)
    • has_group(group) - Check if user belongs to a group
    • has_any_group(groups) - Check if user belongs to any group
    • has_all_groups(groups) - Check if user belongs to all groups

Health Checks

  • health_check() - Basic health check (does not verify LDAP)
  • readiness_check() - Verifies LDAP connectivity (for Kubernetes probes)

Security Considerations

  • TLS is enabled by default - Disable only for testing in secure environments
  • Anonymous binds are disabled by default - Must be explicitly enabled
  • Use a dedicated service account for bind_dn with least-privilege read access; never use Domain Admin
  • Authentication failures are indistinguishable - Prevents user enumeration
  • No credential logging - Passwords are never logged
  • Fail-closed behavior - If LDAP is unavailable, authentication fails securely (503 for infra errors, 401 for bad credentials)

Architecture

The module follows a layered architecture:

  1. LDAP Client Layer (client.py) - Isolated from FastAPI, handles connections, retries, timeouts, TLS
  2. Auth Layer (auth.py) - FastAPI dependencies, converts LDAP errors to HTTP errors
  3. Models (models.py) - Immutable LDAP user objects
  4. Cache Layer (cache.py) - Optional caching with configurable TTL
  5. Configuration (config.py) - Explicit, documented settings

Development

# Install development dependencies
pip install -e ".[dev]"# Run tests
pytest
# Format code
black fastapi_ldap tests
# Lint code
ruff check fastapi_ldap tests
# Type check
mypy fastapi_ldap

License

MIT

About

Production-ready, FastAPI-native LDAP authentication and authorization. Async-first, secure by default, with pooling, caching, observability, and Kubernetes-friendly design.

Resources

Stars

15 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages