Skip to main content
Glama
Arize-ai

@arizeai/phoenix-mcp

Official
by Arize-ai
ldap-authentication.md40.8 kB
# LDAP Authentication Design Proposal for Phoenix > **New to LDAP?** Start with the [LDAP Primer for OIDC Engineers](./ldap-authentication/ldap-primer.md) - explains LDAP fundamentals using OIDC as a reference point. ## Executive Summary LDAP authentication for Phoenix enabling corporate user adoption. Supports Active Directory/OpenLDAP, group-based role mapping, multi-server failover, and Grafana-compatible configuration. → *Full details*: [Architecture Overview](#architecture-overview) | [Security & Risks](#security--risks) | [Implementation Plan](#implementation-plan) | [Grafana Compatibility](./ldap-authentication/grafana-comparison.md) ### The Decision Phoenix's `users` table CHECK constraint (`auth_method IN ('LOCAL', 'OAUTH2')`) requires choosing between: **Approach 1: Zero-Migration (Adoption-First)** *Choose when: Unblocking corporate users is the immediate priority, code consistency can be deferred* - **Mechanism**: Reuse OAuth2 columns with Unicode marker (`\ue000LDAP(stopgap)`) - **Advantage**: Ships immediately, no coordination needed. Validates LDAP implementation with real users before committing to schema changes - **Tradeoff**: Cannot use SQLAlchemy polymorphism (no `LDAPUser` class, must use convention-based helpers). Database semantics mismatched (stores as OAuth2, code treats as LDAP) until migration - **Reversibility**: Standard Alembic migration to Approach 2 available. Any production insights or edge cases discovered can be addressed during migration ([see migration plan](./ldap-authentication/migration-plan.md)) → *Technical details*: [Approach 1 Implementation](#approach-1-zero-migration-strategy) | [Database Schema](./ldap-authentication/database-schema.md) **Approach 2: Schema Migration (Architecture-First)** *Choose when: Code consistency from the start outweighs time-to-adoption* - **Mechanism**: Update CHECK constraint to allow `auth_method='LDAP'` (user identification by email/unique_id unchanged) - **Advantage**: Clean schema, full polymorphism, no technical debt, complete OAuth2/LDAP separation - **Tradeoff**: Requires database migration before release (self-hosted users must upgrade, timing must be coordinated) - **Reversibility**: N/A (this is the target state) → *Technical details*: [Approach 2 Implementation](#approach-2-schema-migration) | [Database Schema](./ldap-authentication/database-schema.md) **Both approaches deliver identical**: Functionality, security, performance, user experience. → *See comparison*: [Full Approach Comparison](#approach-comparison) | [Decision Reversibility Analysis](./ldap-authentication/decision-reversibility.md) ### Irreversible Commitments Four critical decisions are **one-way doors** (cannot be changed without breaking user configurations): 1. **Unicode marker format** (`\ue000LDAP(stopgap)`) - stored in production databases *Safe because*: Collision-proof (Unicode PUA vs OAuth2 ASCII-only spec), cross-database compatible → *Validation*: [Collision Prevention Analysis](./ldap-authentication/collision-prevention.md) | [Migration to Approach 2](./ldap-authentication/migration-plan.md) 2. **Environment variable names** (`PHOENIX_LDAP_*`) - public configuration API *Safe because*: Matches Grafana behavior, follows Phoenix conventions → *Full specification*: [Configuration Reference](./ldap-authentication/configuration.md) 3. **JSON structure for group mappings** - `group_dn`, `role` field names and role values *Safe because*: Field name `group_dn` matches Grafana (using `role` instead of `org_role` since Phoenix has no org concept), role values match Phoenix's existing system → *Complete analysis*: [Decision Reversibility Analysis](./ldap-authentication/decision-reversibility.md) 4. **`allow_sign_up` default** (`true`) - security/access control behavior *Safe because*: Matches Grafana default, explicit opt-out available via `PHOENIX_LDAP_ALLOW_SIGN_UP="false"` → *Rationale*: Changing default later breaks existing deployments expecting auto-sign-up **Note**: Additional behavioral contracts (wildcards, case-insensitive matching, fallback logic) documented in [Decision Reversibility Analysis](./ldap-authentication/decision-reversibility.md). **Important**: Choosing Approach 1 vs. 2 is a *two-way door* - Approach 1 allows validating the implementation with production workloads before schema commitment, with migration to Approach 2 available to address any discovered requirements ([Migration Plan](./ldap-authentication/migration-plan.md)). ### Authentication Framework Phoenix supports three authentication methods: LOCAL, OAuth2, and LDAP. All valid permutations are supported with proper validation to prevent invalid configurations (e.g., disabling basic auth without configuring alternatives). → *Implementation details*: [Authentication Method Framework](#authentication-method-framework) | [Frontend Types](#frontend-types-both-approaches) ### User Identification LDAP users are identified by email (default) or immutable unique ID (objectGUID/entryUUID if configured via `PHOENIX_LDAP_ATTR_UNIQUE_ID`). DN is NOT used for identification (DNs change too frequently in enterprise environments). → *Full rationale*: [User Identification Strategy](./ldap-authentication/user-identification-strategy.md) --- ## Table of Contents ### 🚀 Quick Start (5-10 minutes) - [Executive Summary](#executive-summary) - What we're building and why - [Approach Comparison](#approach-comparison) - Zero-migration vs. schema migration table - [Recommendation](#recommendation) - Which approach to choose - [Architecture Overview](#architecture-overview) - High-level design and auth flow - [Security & Risks](#security--risks) - Threat model and mitigations (2 pages) ### 📋 Implementation Guide (20-30 minutes) - [Implementation Plan](#implementation-plan) - Phase-by-phase breakdown - [Phase 0: Grafana Verification](#phase-0-grafana-compatibility-verification-️) *(See [Grafana Comparison](./ldap-authentication/grafana-comparison.md) for full research)* - [Phase 1: Core LDAP Authentication](#phase-1-core-ldap-authentication) - [Phase 2: Testing & Hardening](#phase-2-testing--hardening) - [Phase 3: Frontend & Documentation](#phase-3-frontend--documentation) ### 📚 Detailed Reference (Appendices) All appendices have been extracted to separate files. See [ldap-authentication/README.md](./ldap-authentication/README.md) for the complete index. **Most Frequently Used**: - [Configuration Reference](./ldap-authentication/configuration.md) - Environment variables, Phoenix vs. Grafana comparison - [Protocol Compliance](./ldap-authentication/protocol-compliance.md) - STARTTLS security, group DN matching, Docker testing - [Security Deep-Dive](./ldap-authentication/security.md) - Threat model, LDAP injection prevention - [Code Examples](./ldap-authentication/code-examples.md) - LDAPConfig, LDAPAuthenticator, API endpoints - [Future Improvements](./ldap-authentication/future-improvements.md) - Post-MVP enhancements (password policy, metrics, nested groups) --- ### 📖 Reading Paths **For Engineering Managers** (5 min): 1. [Executive Summary](#executive-summary) 2. [Approach Comparison](#approach-comparison) 3. [Recommendation](#recommendation) 4. [Decision Reversibility](./ldap-authentication/decision-reversibility.md) **For Backend Engineers** (20 min): 1. [Architecture Overview](#architecture-overview) 2. [Implementation Plan](#implementation-plan) 3. [Database Schema](./ldap-authentication/database-schema.md) 4. [Code Examples](./ldap-authentication/code-examples.md) 5. [Grafana Research](./ldap-authentication/grafana-comparison.md) **For Security Reviewers** (15 min): 1. [Security & Risks](#security--risks) 2. [Security Deep-Dive](./ldap-authentication/security.md) 3. [Collision Prevention](./ldap-authentication/collision-prevention.md) **For Frontend Engineers** (10 min): 1. [Frontend Types](#frontend-types-both-approaches) 2. [Frontend Implementation](./ldap-authentication/frontend-implementation.md) --- ### References - GitHub Issue: https://github.com/Arize-ai/phoenix/issues/10379 - Grafana LDAP Docs: https://grafana.com/docs/grafana/latest/setup-grafana/configure-access/configure-authentication/ldap/ - Grafana Source Code: https://github.com/grafana/grafana/tree/main/pkg/services/ldap ### Configuration Note Phoenix uses environment variables for configuration (consistent with Phoenix patterns), while Grafana uses TOML files. This means: - ✅ **Behavior compatibility**: Group matching, role mapping, authentication flow match Grafana exactly - ⚠️ **Config format divergence**: Users cannot directly reuse Grafana TOML configs (need to convert to env vars) - ⚠️ **Multi-server limitation**: MVP only supports replica servers with identical config (Grafana supports heterogeneous servers) - ✅ **Future-proof**: TOML file support can be added later without breaking env var users --- ## Authentication Method Framework Phoenix supports three authentication methods (**LOCAL**, **OAuth2**, **LDAP**) with validation to prevent invalid configurations. **Key Features**: 9 configuration permutations | Cross-auth security | Smart frontend rendering | Admin provisioning → *Full details*: [Authentication Framework (Appendix)](./ldap-authentication/authentication-framework.md) --- ## LDAP User Identification Phoenix identifies LDAP users by email (default) or immutable unique ID (if `PHOENIX_LDAP_ATTR_UNIQUE_ID` is configured). **Key Design**: Email for most deployments | Unique ID (objectGUID/entryUUID) for email change resilience | DN NOT used (changes too frequently) → *Full details*: [User Identification Strategy](./ldap-authentication/user-identification-strategy.md) --- ## Approach Comparison | Aspect | Approach 1: Adoption-First | Approach 2: Architecture-First | |--------|---------------------------|------------------------------| | **Availability** | ✅ Immediate, unblocks corporate users | ⚠️ Coordination delay | | **Schema** | ⚠️ `auth_method='OAUTH2'` for LDAP | ✅ `auth_method='LDAP'` | | **Polymorphism** | ❌ No `LDAPUser` class | ✅ Full `LDAPUser` class | | **Code Pattern** | ⚠️ Convention-based | ✅ Native ORM | | **Security/Performance/Features** | ✅ Identical | ✅ Identical | | **Reversibility** | ✅ Migration path to Approach 2* | N/A | *Standard Alembic migration ([Migration Plan](./ldap-authentication/migration-plan.md)) --- ### Frontend Types (Both Approaches) **Question**: Can we add `"LDAP"` to the frontend `AuthMethod` enum? **Answer**: **Yes**, for both approaches! **Approach 1 Strategy**: GraphQL Resolver Translation ```python # GraphQL resolver detects LDAP users and translates for frontend @strawberry.field def auth_method(self) -> AuthMethod: """Return semantic auth method (translated from database).""" if self._user.auth_method == "OAUTH2": if is_ldap_user(self._user): # Check for \ue000LDAP(stopgap) prefix return AuthMethod.LDAP # ✅ Frontend sees "LDAP" return AuthMethod.OAUTH2 # Real OAuth2 return AuthMethod(self._user.auth_method) # LOCAL ``` ```typescript // Frontend TypeScript (generated from GraphQL) type AuthMethod = "LOCAL" | "OAUTH2" | "LDAP"; // ✅ Has LDAP! // Clean usage in components if (user.authMethod === "LDAP") { // No password reset for LDAP users } ``` **Tradeoff**: Adds one translation layer in backend, but frontend code stays clean. --- **Approach 2 Strategy**: Direct Mapping (No Translation) ```python # GraphQL resolver directly returns database value @strawberry.field def auth_method(self) -> AuthMethod: return AuthMethod(self._user.auth_method) # Database has 'LDAP' ``` ```typescript // Frontend TypeScript (same result!) type AuthMethod = "LOCAL" | "OAUTH2" | "LDAP"; // ✅ Has LDAP! // Clean usage (identical to Approach 1) if (user.authMethod === "LDAP") { // No password reset for LDAP users } ``` **Tradeoff**: No translation needed, perfect alignment. --- **Recommendation**: Use GraphQL resolver translation in Approach 1 so frontend gets clean types. The one-line translation is worth avoiding frontend workarounds. --- ### Approach 1: Zero-Migration Strategy **Adoption-First Implementation** - Reuse existing columns to unblock corporate users immediately, defer architectural refinement. **Storage Strategy**: ``` Existing columns: New usage: - auth_method = 'OAUTH2' → Satisfies CHECK constraint - oauth2_client_id = ? → '\ue000LDAP(stopgap)' (marker) - oauth2_user_id = ? → Unique ID (if configured) or NULL (email-based) ``` **✅ Strengths**: - Zero coordination overhead with self-hosted users - Removes adoption barriers in corporate environments immediately - Full LDAP functionality (security, performance identical to Approach 2) - Clear migration path when architectural refinement becomes priority **⚠️ Known Limitations**: - **No SQLAlchemy polymorphism**: Cannot create `LDAPUser` class (shared discriminator with OAuth2) - Convention-based detection: `if oauth2_client_id == '\ue000LDAP(stopgap)'` - Requires documentation for future maintainers - GraphQL resolver needed for frontend type translation **Best For**: When unblocking corporate users is the immediate priority. --- ### Approach 2: Schema Migration **Architecture-First Implementation** - Invest in clean schema upfront, optimizing for long-term maintainability. **Schema Changes**: ```sql ALTER TABLE users ADD COLUMN ldap_username VARCHAR NOT NULL; ALTER TABLE users ADD CONSTRAINT CHECK (auth_method IN ('LOCAL', 'OAUTH2', 'LDAP')); ``` **Enables Full Polymorphism**: ```python class LDAPUser(User): """Type-safe class with native ORM support.""" __mapper_args__ = {"polymorphic_identity": "LDAP"} # Native queries ldap_users = session.query(LDAPUser).all() if isinstance(user, LDAPUser): ... ``` **✅ Strengths**: - Consistent with existing `LocalUser`/`OAuth2User` patterns - Self-documenting schema (`auth_method='LDAP'` is explicit) - Zero technical debt - No conventions to learn for new engineers **⚠️ Known Limitations**: - Requires coordinating with self-hosted users for migration - Upfront architectural commitment before usage data available **Best For**: When code consistency outweighs time-to-adoption concerns. --- ## Recommendation **Approach 1**: Zero-migration, adoption-first strategy **Approach 2**: Migration-first, clean architecture from start ### Critical Tradeoff: Polymorphism Approach 1 cannot use SQLAlchemy polymorphism (shared `auth_method='OAUTH2'` discriminator): ```python # Approach 1: Convention-based users = session.query(User).filter(User.oauth2_client_id == '\ue000LDAP(stopgap)') # Approach 2: Native ORM ldap_users = session.query(LDAPUser).all() # Type-safe ``` Phoenix uses `LocalUser`/`OAuth2User` polymorphism throughout. Approach 1 temporarily diverges from this pattern. ### Decision Framework | Optimize For | Choose | Rationale | |--------------|--------|-----------| | Immediate availability for corporate users | Approach 1 | Removes adoption barriers in enterprise environments quickly | | Architectural consistency | Approach 2 | Clean codebase from start, full polymorphism | **Strategic Flexibility**: Approach 1 → 2 migration is straightforward (standard Alembic, [Migration Plan](./ldap-authentication/migration-plan.md)). This allows deferring the polymorphism investment until adoption patterns clarify architectural priorities. **Recommendation**: Approach 1 for initial release, migrate to Approach 2 when: - LDAP usage grows significantly (100+ users) - Team prioritizes LDAP-specific feature development (requires polymorphism) - Technical debt reduction becomes higher priority than new features --- ## Architecture Overview ### System Architecture ```mermaid graph TB User[User Browser] Login[Login Page] LDAPAuth[LDAP Authenticator] LDAP[LDAP Server<br/>Active Directory / OpenLDAP] DB[(Phoenix Database)] User -->|1. Enter credentials| Login Login -->|2. POST /auth/ldap/login| LDAPAuth LDAPAuth -->|3. Bind & authenticate| LDAP LDAP -->|4. Success + attributes| LDAPAuth LDAPAuth -->|5. Query groups| LDAP LDAP -->|6. Group DNs| LDAPAuth LDAPAuth -->|7. Map groups to roles| LDAPAuth LDAPAuth -->|8. Create/update user| DB LDAPAuth -->|9. Issue JWT tokens| Login Login -->|10. Navigate to app| User ``` ### Authentication Flow (Simplified) 1. **User submits LDAP username + password** 2. **Connect to LDAP server** (with TLS) 3. **Bind with service account** (if configured) 4. **Search for user** by username 5. **Authenticate user** (bind with user's credentials) 6. **Retrieve user attributes** (email, display name) 7. **Query user's groups** (for role mapping) 8. **Map LDAP groups → Phoenix roles** (ADMIN, MEMBER, VIEWER) 9. **Create or update user in database** 10. **Issue JWT tokens** (access + refresh) 11. **Return to application** **Fallback**: If LDAP unavailable, existing LOCAL/OAuth2 users can still login. --- ### Key Features #### Multiple LDAP Servers (Replica Failover) ```bash PHOENIX_LDAP_HOST="dc1.corp.com,dc2.corp.com,dc3.corp.com" ``` - Automatic failover on connection failures - Tries servers in order, first success wins **MVP Limitation**: All servers must be replicas with identical configuration (same bind DN, search base, group mappings). For heterogeneous LDAP forests with different configurations per server, see [Grafana's TOML approach](https://raw.githubusercontent.com/grafana/grafana/84a07be6e4e1acd8f064c3b390c30188d5703afc/conf/ldap_multiple.toml). Phoenix can add TOML file support post-MVP for this use case. - Load balancing across servers #### Group-Based Role Mapping ```bash PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='{ "admin": ["CN=Phoenix Admins,OU=Groups,DC=corp,DC=com"], "member": ["CN=Phoenix Users,OU=Groups,DC=corp,DC=com"], "viewer": ["CN=Phoenix Viewers,OU=Groups,DC=corp,DC=com"] }' ``` - Supports both Active Directory (memberOf) and POSIX groups - Wildcard mappings: `"*": ["CN=All Users,DC=corp,DC=com"]` #### Flexible Configuration - Environment variables (Phoenix style) - TLS/SSL support (StartTLS or LDAPS) - Customizable attribute mapping - Multiple authentication strategies --- ## Security & Risks ### Security Essentials | Threat | Mitigation | Implementation Status | |--------|-----------|----------------------| | **LDAP Injection** | Escape all user inputs, use parameterized filters | ✅ Implemented (ldap3.escape_filter_chars) | | **Credential Exposure** | TLS required, credentials never logged | ✅ Implemented (error-only logging, no PII) | | **Timing Attacks** | Constant-time comparisons, generic error messages | ✅ Generic errors implemented | | **Username Enumeration** | Generic error messages for all failures | ✅ **Enhanced** - all cross-auth errors are generic | | **Cross-Auth Hijacking** | Prevent OAuth2 from hijacking LDAP users | ✅ **New** - explicit checks implemented | | **Duplicate Accounts** | Prevent LDAP from creating OAuth2 duplicates | ✅ **New** - email collision detection | | **Event Loop DoS** | Thread pool isolation for blocking LDAP operations | ✅ Implemented (anyio.to_thread.run_sync) | | **Socket Leaks** | Unconditional cleanup in finally blocks | ✅ Implemented (unbind() always called) | | **Brute Force** | Rate limiting (10 attempts/minute) | ✅ Implemented via rate limiter | | **MITM** | TLS certificate validation enforced | ✅ Configured (requires real LDAP testing) | **Details**: See [Security Deep-Dive](./ldap-authentication/security.md) --- ### Key Risks & Mitigations | Risk | Impact | Likelihood | Mitigation | Status | |------|--------|-----------|------------|--------| | LDAP server unavailable | Users cannot login | Medium | Fallback to LOCAL/OAuth2, multiple servers | ✅ Implemented | | Misconfigured group mappings | Wrong roles assigned | Medium | Validation on startup | ✅ Implemented | | Event loop blocking | Service unavailability | Medium | Thread pool isolation, timeouts | ✅ Implemented | | Socket leaks on failed auth | File descriptor exhaustion | Medium | Unconditional `unbind()` in finally block | ✅ Implemented | | Performance issues | Slow login | Low | Connection timeout, async operations | ✅ Implemented | | Username enumeration | Security information leak | Low | Generic error messages | ✅ Implemented | | Cross-authentication attacks | Account hijacking | Low | Explicit auth method checks | ✅ Implemented | | Regex complexity (CVE-2024-47764) | Performance degradation | Very Low | Use ldap3 (not python-ldap) | ✅ Using ldap3 | **Details**: See full threat model in [Security Deep-Dive](./ldap-authentication/security.md) --- ### Success Criteria **Functional**: - ✅ Active Directory authentication works end-to-end - ✅ OpenLDAP authentication works end-to-end - ✅ Group-based role mapping assigns correct Phoenix roles - ✅ Multiple LDAP servers with failover functions correctly - ✅ Coexists with LOCAL and OAuth2 authentication **Non-Functional**: - ✅ P95 login latency < 2 seconds - ✅ No security vulnerabilities (LDAP injection, timing attacks) - ✅ Graceful degradation when LDAP unavailable - ✅ Comprehensive audit logging **Code Quality**: - ✅ Unit test coverage > 80% - ✅ Integration tests with in-process mock LDAP server - ✅ Clear error messages for troubleshooting --- ## Implementation Plan **Overview**: Implementation divided into three phases: backend, testing, and frontend/documentation. **Current Status** (as of implementation): - ✅ **Phase 0**: Grafana Compatibility Verification - COMPLETE - ✅ **Phase 1**: Core LDAP Authentication - COMPLETE - ✅ **Phase 2**: Testing & Hardening - COMPLETE (22/22 integration tests passing) - ✅ **Phase 3**: Frontend & Documentation - COMPLETE **Implementation Approach**: **Approach 1 (Zero-Migration)** - Using Unicode marker `\ue000LDAP(stopgap)` in `oauth2_client_id` column. **Frontend Scope Summary**: All UI changes implemented - ✅ New component: `LDAPLoginForm.tsx` (end-user login) - ✅ New component: `NewUserDialog.tsx` with LDAP tab (admin user creation) - ✅ Backend config: `window.Config.ldapEnabled` exposed - ✅ GraphQL resolver translation for `AuthMethod.LDAP` --- ### Phase 0: Grafana Compatibility Verification ⚠️ **Status**: ✅ **COMPLETE** - All ONE-WAY door decisions have been verified against Grafana source code. **Quick Summary**: - ✅ Verified group role mapping structure adapted from Grafana (`group_dn`, `role`) - ✅ Confirmed case-insensitive DN matching, wildcard `"*"` support - ✅ Verified role values: "Admin", "Editor", "Viewer" (capitalized) - ✅ Analyzed Grafana's two-table design (`user` + `user_auth`) vs Phoenix's single-table discriminator - ✅ Documented configuration divergence: Grafana uses TOML files, Phoenix uses env vars - ✅ Reviewed LDAP injection prevention, error handling, TLS, background sync **For full details, see**: [Grafana Source Code Research](./ldap-authentication/grafana-comparison.md) **Critical Requirements** (adapted from Grafana): 1. **Group Role Mapping JSON** - Must use `{"group_dn": "...", "role": "..."}` format (Phoenix uses `role`, not Grafana's `org_role`, since Phoenix has no org concept) 2. **Case-Insensitive Matching** - Use `strings.EqualFold()` for DN comparison 3. **Wildcard Support** - `"*"` matches all users (checked first) 4. **Role Values** - Use Phoenix roles directly: "ADMIN", "MEMBER", "VIEWER" (not Grafana's "Admin", "Editor", "Viewer") 5. **LDAP Injection** - Must escape user input with `ldap.EscapeFilter()` 6. **Default Timeout** - 10 seconds for connection attempts **Implementation Note**: Phoenix uses its internal role names directly in the configuration, removing Grafana's role names as an intermediary. This simplifies the implementation and makes the configuration more Phoenix-native while maintaining Grafana's behavior patterns (wildcards, case-insensitive matching, first-match-wins). **Checklist**: - [x] Reviewed Grafana source code (6 files) - [x] Documented JSON structure compatibility - [x] Verified DN matching logic - [x] Analyzed database schema differences - [x] Reviewed security patterns (injection, TLS) - [x] Documented configuration method divergence --- ### Phase 1: Core LDAP Authentication ✅ **COMPLETE** **Objective**: Implement core LDAP authentication with group-based role mapping **Milestone 1.1: Configuration & Data Models** ✅ - [x] Add `LDAPConfig` to `src/phoenix/config.py` - [x] Add environment variable parsing - [x] Update `AuthSettings` to include LDAP - [x] Add validation for required fields **Milestone 1.2: LDAP Authenticator** ✅ - [x] Create `src/phoenix/server/ldap.py` - [x] Implement `LDAPAuthenticator` class - [x] Server connection with TLS - [x] Service account bind - [x] User search - [x] User authentication - [x] Attribute retrieval - [x] Group membership query - [x] Helper function: `is_ldap_user()` (implemented as `LDAP_CLIENT_ID_MARKER` check) - [x] Note: `get_ldap_client_data()` not needed - using simple marker approach **Milestone 1.3: API Integration** ✅ - [x] Add `/auth/ldap/login` endpoint - [x] Implement user creation/update logic (integrated in ldap_login endpoint) - [x] Add JWT token issuance (using existing token creation) - [x] Rate limiting added to `/auth/ldap/login` - [ ] Update health check to include LDAP status (deferred - not critical for MVP) --- ### Phase 2: Testing & Hardening ✅ **COMPLETE** **Milestone 2.1: Unit Tests** ✅ - [x] Test `LDAPConfig` validation (13 test cases) - [x] Test environment variable parsing - [x] Test JSON validation for group role mappings - [x] Test role validation (ADMIN, MEMBER, VIEWER) - [x] Test TLS configuration validation - [x] Test group search configuration validation **Milestone 2.2: Integration Tests** ✅ - [x] Create in-process mock LDAP server (`tests/integration/_mock_ldap_server.py`) - [x] Active Directory schema support (memberOf attribute) - [x] POSIX/OpenLDAP schema support (group search with member attribute) - [x] **DN validation** - Returns `LDAP_INVALID_DN_SYNTAX` (error code 34) for malformed DNs - [x] **Ambiguous result support** - Can return multiple entries for duplicate usernames - [x] Test end-to-end authentication flow (37 passing tests across multiple test files) - **Active Directory Tests** (22 tests in `test_ldap.py`): - [x] Basic authentication (admin, viewer, no groups) - [x] User creation and attribute updates - [x] Role mapping from memberOf attribute - [x] Error handling (invalid credentials, nonexistent users, missing email) - [x] Special characters in usernames (LDAP filter escaping) - [x] Concurrent requests - [x] `allow_sign_up` configuration (true/false, email fallback) - **POSIX Group Search Tests** (6 tests in `test_ldap_posix.py`): - [x] ADMIN role via group search - [x] Multiple group membership with priority - [x] **DN escaping for LDAP injection prevention** (special characters in DN) - [x] Wildcard role assignment (no groups) - [x] Case-insensitive DN matching (RFC 4514 compliance) - [x] Graceful degradation on group search failure - **Cross-Authentication Security** (4 tests): - [x] LOCAL ↔ LDAP isolation - [x] LDAP ↔ OAuth2 isolation - [x] REST API marker validation - **DN Validation & Ambiguous Results** (4 tests in `test_ldap.py::TestLDAPDNValidation`): - [x] Mock server rejects invalid DN syntax - [x] Duplicate username in different OUs rejected (security) - [x] Test group matching (memberOf, wildcards, case-insensitive) - [x] Test user creation and updates - [x] Test role mapping (ADMIN, MEMBER, VIEWER) - [x] Test error handling (invalid credentials, nonexistent users, missing fields) - [x] Test Phoenix-specific logic (email fallback, group priority, role downgrades) - [x] Test special characters in usernames (LDAP filter escaping) - [x] Test concurrent requests - [x] **Test ambiguous search result rejection** (duplicate usernames across OUs) - [ ] Test multi-server failover (deferred - requires multiple mock servers) - [x] Test TLS/LDAPS connections (validated via Docker TLS profile with real OpenLDAP + adversarial MITM proxy) **Milestone 2.3: Security Hardening** ✅ - [x] Verify LDAP injection prevention (ldap.py uses ldap3's escape_filter_chars) - [x] Test LDAP injection attempts (test_ldap_injection_prevention) - [x] Verify generic error messages (no username enumeration) - [x] **Enhanced**: Replaced informative error messages with generic ones to prevent enumeration - [x] LDAP login with existing OAuth2 email: "Invalid username and/or password" (was: 409 with specific message) - [x] OAuth2 login attempt by LDAP user: "Sign in is not allowed." (was: specific LDAP message) - [x] Rate limiting configured on `/auth/ldap/login` (auth.py) - [x] **Security Enhancement**: Cross-authentication protection implemented - [x] Prevent OIDC from hijacking LDAP users - Detects `LDAP_CLIENT_ID_MARKER` and rejects OAuth2 login - Returns generic "Sign in is not allowed" error - [x] Prevent LDAP from creating duplicate OAuth2 accounts - Checks for existing OAuth2 user with same email - Returns generic "Invalid username and/or password" error (401) - [x] Integration tests for cross-auth security - `test_ldap_user_cannot_login_via_oauth2` - `test_ldap_login_rejected_when_oauth2_user_exists` - [ ] Timing attack mitigations (basic implementation, could be enhanced) - [x] TLS security validation (STARTTLS/LDAPS tested via adversarial MITM proxy with OpenLDAP self-signed certs) - [x] **PII Protection in Logs** (privacy/compliance) - [x] Removed usernames from 6 logging statements in `ldap.py` - [x] Removed user DNs from ambiguous search error logging - [x] Replaced PII with generic messages and configuration hints - [x] Maintains operational troubleshooting capability without exposing identities **Milestone 2.4: Docker Development & Testing Environment** ✅ - [x] Create Docker Compose LDAP profile (`scripts/docker/devops/overrides/ldap.yml`) - [x] Real OpenLDAP server (osixia/openldap:1.5.0) - [x] phpLDAPadmin web interface for LDAP management - [x] Automated seed data loading via ldap-seed container - [x] Phoenix integration with LDAP environment variables - [x] Automated test runner container with Python test suite - [x] Create enhanced LDAP seed data (`scripts/docker/devops/ldap-seed.ldif`) - [x] **11 test users** covering all edge cases (was 4 basic users) - [x] **Organizational structure**: ou=users, ou=IT, ou=HR, ou=groups - [x] **Edge case users**: - [x] `nogroups` - User with NO groups (wildcard fallback) - [x] `multigroup` - User in MULTIPLE groups (role precedence) - [x] `nodisplay` - User with MISSING displayName (fallback logic) - [x] `special(user)` - Username with SPECIAL characters (injection prevention) - [x] `josé` - UNICODE username (UTF-8 support) - [x] `duplicate` (IT & HR) - DUPLICATE usernames in different OUs (ambiguous rejection) - [x] **3 groups**: admins, members, viewers with proper memberOf attributes - [x] Create rigorous test script (`scripts/docker/devops/scripts/test_ldap_integration.py`) - [x] **14 comprehensive test cases** covering all success criteria - [x] Tests all security controls (injection, anonymous bind, ambiguous results) - [x] Tests all edge cases (no groups, multi-groups, special chars, unicode) - [x] Tests error handling (invalid password, nonexistent user, empty credentials) - [x] Automated execution on Docker Compose startup - [x] **Test Results**: 14/14 PASSED ✅ - [x] Integrate into dev.sh workflow - [x] `./dev.sh up --profile ldap` starts full LDAP environment - [x] `docker logs devops-ldap-test` shows test results - [x] LDAP Admin accessible at http://localhost:6443 **Milestone 2.5: TLS Security Testing & Adversarial Validation** ✅ - [x] Create Docker Compose TLS profile (`scripts/docker/devops/overrides/ldap-test.yml`) - [x] OpenLDAP with TLS enabled (auto-generated self-signed certificates) - [x] Phoenix STARTTLS instance (port 6007, STARTTLS mode) - [x] Phoenix LDAPS instance (port 6008, LDAPS mode) - [x] Grafana LDAP instance (port 3000, comparison baseline) - [x] Adversarial MITM proxy (port 3389, credential extraction) - [x] Automated test runner executing comprehensive security validation - [x] Implement adversarial MITM proxy (`scripts/docker/devops/scripts/ldap_mitm_proxy.py`) - [x] LDAP ASN.1/BER protocol parsing for bind request analysis - [x] Credential extraction from Simple Authentication (RFC 4513 §5.1.1) - [x] StartTLS and TLS handshake detection - [x] Credential verification against real LDAP server - [x] Structured JSON logging for automated analysis - [x] Application identification via reverse DNS - [x] Implement comprehensive test suite (`scripts/docker/devops/scripts/test_ldap_tls.py`) - [x] **Phase 1: Baseline** - Direct LDAP connectivity tests (plaintext, STARTTLS, LDAPS) - [x] **Phase 2: Applications** - Phoenix STARTTLS/LDAPS + Grafana STARTTLS via MITM proxy - [x] **Phase 3: Adversarial** - MITM proxy log analysis + credential verification - [x] Exit codes: 0 = all tests passed, 1 = failures detected - [x] **Test Results**: 7/7 PASSED ✅ - [x] Fix STARTTLS vulnerability in Phoenix (`src/phoenix/server/ldap.py`) - [x] Service account binds use `AUTO_BIND_TLS_BEFORE_BIND` for STARTTLS mode - [x] User password verification calls `start_tls()` before `bind()` for STARTTLS - [x] LDAPS mode unaffected (TLS from start) - [x] Validation results documented (`scripts/docker/devops/LDAP-TLS-TESTING.md`) - [x] **Phoenix**: ✅ SECURE - MITM proxy extracted 0 credentials - [x] **Grafana v11.4**: 🚨 VULNERABLE - MITM proxy extracted 2 credentials - [x] Comprehensive architecture diagrams and test flow documentation - [x] Integrate into dev.sh workflow - [x] `./dev.sh up --profile ldap-test` starts full TLS test environment - [x] `docker logs devops-ldap-test` shows automated test results - [x] `docker logs devops-ldap-mitm-proxy` shows adversarial analysis --- ### Phase 3: Frontend & Documentation ✅ **COMPLETE** **Milestone 3.1: Frontend Components** ✅ **COMPLETE** - [x] Create `LDAPLoginForm.tsx` (username + password form, POSTs to `/auth/ldap/login`) - [x] Update `LoginPage.tsx` to include LDAP form with conditional rendering - [x] Update `NewUserDialog.tsx` with LDAP tab for admin user creation - [x] Update `UsersTable.tsx` to display LDAP users correctly (no changes needed - works automatically) - [x] Add `"LDAP"` to GraphQL `AuthMethod` enum - [x] Update `window.Config` to include `ldapEnabled` flag - [x] Update backend `AppConfig` and `index.html` template to expose `ldapEnabled` **Implementation Details**: - **End-User Login**: `LDAPLoginForm.tsx` accepts username/password, sanitizes input, handles errors (401, 429), shows loading state - **Admin User Creation**: `NewUserDialog.tsx` includes tabbed interface (Local/OAuth2/LDAP) with intelligent default tab selection - **Separator Logic**: "or" separators correctly shown between Local/LDAP and OAuth2 sections - **Config Propagation**: Backend → `window.Config.ldapEnabled` → Frontend conditional rendering **Milestone 3.2: Documentation** ✅ **COMPLETE** - [x] Configuration guide (environment variables) - See [Configuration Reference](./ldap-authentication/configuration.md) - [x] Group role mapping examples - See [Configuration Reference](./ldap-authentication/configuration.md) - [x] Database schema documentation - See [Database Schema](./ldap-authentication/database-schema.md) - [x] Security considerations - See [Security Deep-Dive](./ldap-authentication/security.md) - [x] Migration guide (Approach 1 → 2) - See [Migration Plan](./ldap-authentication/migration-plan.md) - [x] Grafana compatibility analysis - See [Grafana Comparison](./ldap-authentication/grafana-comparison.md) - [ ] Active Directory setup guide (user-facing docs - TBD) - [ ] OpenLDAP setup guide (user-facing docs - TBD) - [ ] Troubleshooting guide (user-facing docs - TBD) --- ### Technical Validation Required Before production release: 1. **Spike Phase** (early validation with real LDAP): ✅ **COMPLETE** - [x] Test ldap3 library with real OpenLDAP instance (Docker + 11 test users) - [x] Test group membership queries (memberOf) against real directory - [x] Measure authentication latency in realistic environment (< 2s P95) - [x] Validate configuration patterns work end-to-end (14/14 tests passed) - [x] Validate ambiguous search result handling (duplicate users across OUs) - [x] Verify STARTTLS/LDAPS compatibility with real LDAP server (OpenLDAP with self-signed certs) - [x] **STARTTLS security validated** - Adversarial MITM proxy confirms no plaintext credential transmission - [ ] Test with real Active Directory instance (OpenLDAP validated, AD pending) **Note**: This complements automated integration tests (in-process mock LDAP server in Milestone 2.2). Real OpenLDAP Docker environment validates production scenarios with actual protocol implementation. 2. **Security Review**: ✅ **COMPLETE** - [x] LDAP injection prevention verified (uses ldap3.escape_filter_chars, tested) - [x] Username enumeration prevention verified (generic error messages implemented) - [x] Rate limiting configured (10 attempts/minute on /auth/ldap/login) - [x] Cross-authentication security verified (LDAP↔OAuth2 isolation) - [x] **Ambiguous search result rejection** (non-deterministic auth prevention) - [x] **PII protection** - Removed usernames/DNs from all logs (GDPR/CCPA compliance) - [x] **Anonymous bind prevention** - Explicit empty credential checks (security) - [x] **STARTTLS sequencing validated** - Adversarial MITM proxy with ASN.1/BER parsing confirms TLS upgrade before bind - [x] **DN case-insensitivity** - Normalized storage prevents account duplication/lockout (RFC 4514 compliance) - [x] **TLS mode-aware port defaulting** - LDAPS defaults to 636, STARTTLS defaults to 389 3. **Performance Testing**: ⏳ **PENDING** - [ ] P95 login latency < 2 seconds - [ ] Connection pooling effectiveness measured - [ ] Graceful degradation when LDAP unavailable - [ ] Multi-server failover latency acceptable --- ## Appendices Detailed technical references, design decisions, and implementation guides have been extracted to separate files for easier navigation and maintenance. ### 📚 Complete Appendix Index See **[ldap-authentication/README.md](./ldap-authentication/README.md)** for the full navigation guide. ### Quick Reference **Implementation Guides**: - [Configuration Reference](./ldap-authentication/configuration.md) - Environment variables, Phoenix vs. Grafana comparison, examples - [Code Examples](./ldap-authentication/code-examples.md) - LDAPConfig dataclass, LDAPAuthenticator, API endpoints - [Frontend Implementation](./ldap-authentication/frontend-implementation.md) - React components, LoginPage updates - [Phoenix Integration](./ldap-authentication/phoenix-integration.md) - AuthSettings integration details **Design & Architecture**: - [Database Schema](./ldap-authentication/database-schema.md) - Option 1 (zero-migration) vs. Option 2 (dedicated columns) - [User Identification Strategy](./ldap-authentication/user-identification-strategy.md) - Email/Unique ID identification - [Decision Reversibility](./ldap-authentication/decision-reversibility.md) - One-way vs. two-way door analysis - [Migration Plan](./ldap-authentication/migration-plan.md) - Approach 1 → 2 migration guide (if needed) **Security & Compliance**: - [Security Deep-Dive](./ldap-authentication/security.md) - Threat model, injection prevention, TLS, rate limiting - [Protocol Compliance](./ldap-authentication/protocol-compliance.md) - OpenLDAP validation, STARTTLS security, group DN matching - [Collision Prevention](./ldap-authentication/collision-prevention.md) - Unicode marker safety analysis **Research & Comparison**: - [Grafana Comparison](./ldap-authentication/grafana-comparison.md) - Source code research, compatibility findings - [Background Sync (Future)](./ldap-authentication/background-sync-future.md) - Post-MVP enterprise feature design **Future Roadmap**: - [Future Improvements](./ldap-authentication/future-improvements.md) - Password policy, metrics, nested groups, retry logic --- ## End of Document

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server