Skip to main content
Glama
Arize-ai

@arizeai/phoenix-mcp

Official
by Arize-ai
ldap-primer.md16.5 kB
# LDAP Primer for OIDC Engineers **Target Audience**: Engineers familiar with OAuth2/OIDC who need to understand LDAP authentication. --- ## TL;DR: LDAP vs OIDC | Aspect | OIDC | LDAP | |--------|------|------| | **Protocol Type** | Modern REST/JSON API | Binary protocol (ASN.1/BER encoding) | | **Primary Use** | Delegated authentication (3rd party) | Direct authentication (same organization) | | **User Identifier** | Email or UUID (provider-specific) | Distinguished Name (DN) - hierarchical path | | **Authentication Flow** | Browser redirect → authorization code → token exchange | Direct username/password bind to directory | | **Trust Model** | Trust external IdP (Google, Okta, etc.) | Trust internal directory server (Active Directory, OpenLDAP) | | **Deployment** | SaaS or on-prem | Almost always on-prem or private cloud | | **Age** | Modern (2014+) | Legacy (1993+, based on X.500 from 1988) | **When to Use**: - **OIDC**: External users, consumer apps, "Sign in with Google" - **LDAP**: Enterprise internal apps, corporate network integration, existing AD infrastructure --- ## What Is LDAP? **LDAP (Lightweight Directory Access Protocol)** is a protocol for accessing and managing directory services over TCP/IP. Think of it as: - **Database**: Stores user accounts, groups, organizational structure - **Authentication Protocol**: Verifies credentials directly - **Query Language**: Searches directory tree with filters ### Core Concept: The Directory Tree Unlike OIDC (which is stateless HTTP), LDAP organizes data in a **hierarchical tree structure**: ```mermaid graph TD Root["dc=example,dc=com<br/>(root)"] Users["ou=users"] Groups["ou=groups"] Alice["uid=alice"] Bob["uid=bob"] Contractors["ou=contractors"] Charlie["uid=charlie"] Admins["cn=admins"] Devs["cn=developers"] Root --> Users Root --> Groups Users --> Alice Users --> Bob Users --> Contractors Contractors --> Charlie Groups --> Admins Groups --> Devs ``` **Full Distinguished Names (DN)**: - Alice: `uid=alice,ou=users,dc=example,dc=com` - Bob: `uid=bob,ou=users,dc=example,dc=com` - Charlie: `uid=charlie,ou=contractors,ou=users,dc=example,dc=com` - Admins group: `cn=admins,ou=groups,dc=example,dc=com` **OIDC Equivalent**: There is no tree structure - users are flat, identified by email or opaque ID (`sub` claim). --- ## Distinguished Names (DN) **DN = Full path to a user in the directory tree** Example DN: ``` uid=alice,ou=engineering,dc=example,dc=com ``` **Breaking it down**: - `uid=alice` - User identifier (username) - `ou=engineering` - Organizational Unit (department/team) - `dc=example,dc=com` - Domain Components (company domain) **OIDC Equivalent**: Closest analog is the `sub` (subject) claim: ```json { "sub": "auth0|5f8e9a1234567890abcdef12", // OIDC "email": "alice@example.com" } ``` But OIDC's `sub` is: - ✅ **Stable** (like DN) - ✅ **Unique per provider** (like DN per directory) - ❌ **Not hierarchical** (DN encodes organizational structure) - ❌ **Not human-readable** (DN shows department, location, etc.) --- ## Authentication Flow Comparison ### OIDC Flow (What You Know) ```mermaid sequenceDiagram participant Browser participant Phoenix participant Google Browser->>Phoenix: 1. Click "Sign in with Google" Phoenix->>Browser: 2. Redirect to Google<br/>(authorization_endpoint) Browser->>Google: 3. User authenticates at Google Google->>Browser: 4. Redirect back with code Browser->>Phoenix: 5. Send code to Phoenix Phoenix->>Google: 6. Exchange code for tokens Google->>Phoenix: 7. ID token + access token Phoenix->>Browser: 8. Set cookie, return success ``` **Key**: Phoenix **never sees the password**. Google handles authentication. --- ### LDAP Flow (New) ```mermaid sequenceDiagram participant Browser participant Phoenix participant LDAP as LDAP Server Browser->>Phoenix: 1. POST /auth/ldap/login<br/>{username, password} Phoenix->>LDAP: 2. Search for username<br/>filter: (uid=alice) LDAP->>Phoenix: 3. Return DN + attributes<br/>DN: uid=alice,ou=...<br/>email: alice@example.com Phoenix->>LDAP: 4. Bind with user DN + password<br/>(verify credentials) LDAP->>Phoenix: 5. Bind success/failure Phoenix->>LDAP: 6. Query user groups<br/>filter: (member=DN) LDAP->>Phoenix: 7. Return groups<br/>[cn=admins,ou=groups,...] Phoenix->>Browser: 8. Set cookie, return success ``` **Key Differences**: - ✅ Phoenix **receives the password** directly (no redirect) - ✅ Phoenix **queries LDAP server** (not a REST API) - ✅ **Two LDAP operations**: Search (find user) + Bind (verify password) - ✅ **Binary protocol**, not HTTP/JSON --- ## Key LDAP Concepts ### 1. Bind Operation (Authentication) **"Binding"** = Authenticating to the LDAP server. Two types: **Service Account Bind** (search-and-bind pattern): ```python # Phoenix binds with read-only service account connection = Connection( server, user="cn=readonly,dc=example,dc=com", password="service_password", auto_bind=True ) # Then searches for user by username connection.search( search_base="ou=users,dc=example,dc=com", search_filter="(uid=alice)", ) # Returns: uid=alice,ou=engineering,dc=example,dc=com ``` **User Bind** (verify credentials): ```python # Bind as the actual user (proves they know password) user_connection = Connection( server, user="uid=alice,ou=engineering,dc=example,dc=com", password=user_provided_password, auto_bind=True ) if user_connection.bound: print("✓ Password correct") ``` **OIDC Equivalent**: There is no equivalent - OIDC providers handle this internally. --- ### 2. LDAP Filters (Query Language) Filters search the directory tree (like SQL WHERE clauses, but with LISP-style prefix syntax): | Filter | Meaning | OIDC Analogy | |--------|---------|--------------| | `(uid=alice)` | Find user where uid=alice | Search by email | | `(objectClass=user)` | Find all user objects | Filter by type | | `(&(uid=alice)(objectClass=user))` | AND logic | Multiple conditions | | `(\|(uid=alice)(uid=bob))` | OR logic | Match any | | `(uid=a*)` | Wildcard | Prefix search | | `(memberOf=cn=admins,ou=groups,...)` | Group membership | Roles/claims | **Example: Active Directory user search**: ```ldap (&(objectClass=user)(sAMAccountName=alice)) ``` **Phoenix escapes user input** to prevent LDAP injection (like SQL injection): ```python from ldap3.utils.conv import escape_filter_chars # User input: alice escaped = escape_filter_chars("alice") filter_str = f"(&(objectClass=user)(uid={escaped}))" ``` --- ### 3. Attributes (User Fields) LDAP users have **attributes** (like JSON fields, but schema-defined): Common attributes: | Attribute | Purpose | OIDC Claim Equivalent | |-----------|---------|----------------------| | `uid` | Username | `preferred_username` | | `mail` | Email address | `email` | | `displayName` | Full name | `name` | | `memberOf` | Group memberships (AD) | `groups` or `roles` | | `cn` | Common Name | `name` | | `sn` | Surname | `family_name` | | `givenName` | First name | `given_name` | | `userPassword` | Password hash (not readable) | N/A (handled by IdP) | **Phoenix queries attributes**: ```python # After finding user, read their attributes email = user_entry.mail.value display_name = user_entry.displayName.value groups = user_entry.memberOf.values # List of DNs ``` --- ### 4. Groups & Role Mapping Two group membership patterns: ```mermaid graph LR subgraph Pattern 1: Active Directory User1["User Entry<br/>uid=alice,ou=users,...<br/>mail: alice@example.com<br/>memberOf: cn=admins,...<br/>memberOf: cn=devs,..."] Group1["Group: cn=admins"] Group2["Group: cn=devs"] User1 -.->|references| Group1 User1 -.->|references| Group2 end subgraph Pattern 2: POSIX/OpenLDAP User2["User Entry<br/>uid=alice,ou=users,...<br/>mail: alice@example.com"] Group3["Group Entry<br/>cn=admins,ou=groups,...<br/>member: uid=alice,ou=users,..."] Group4["Group Entry<br/>cn=devs,ou=groups,...<br/>member: uid=alice,ou=users,..."] Group3 -.->|references| User2 Group4 -.->|references| User2 end ``` **Pattern 1 (Active Directory)**: - User entry has `memberOf` attribute listing group DNs - Phoenix reads `memberOf` directly (efficient, 1 query) **Pattern 2 (POSIX/OpenLDAP)**: - Group entries have `member` attribute listing user DNs - Phoenix must search groups where `(member=<user_dn>)` (slower, N queries) **Phoenix Configuration**: ```bash # Map LDAP groups → Phoenix roles PHOENIX_LDAP_GROUP_ROLE_MAPPINGS='[ {"group_dn": "cn=admins,ou=groups,dc=example,dc=com", "role": "ADMIN"}, {"group_dn": "cn=developers,ou=groups,dc=example,dc=com", "role": "MEMBER"} ]' ``` **OIDC Equivalent**: ```json { "groups": ["admins", "developers"], // Flat list in ID token "roles": ["admin"] // Or roles claim } ``` **Key Difference**: OIDC groups are **in the token** (client-side). LDAP groups are **queried on every login** (server-side, always fresh). --- ## Common LDAP Implementations ### 1. Active Directory (Microsoft) **What**: Microsoft's LDAP implementation (most common in enterprises) **Unique Features**: - `sAMAccountName` instead of `uid` for username - `memberOf` attribute for group membership (efficient) - `userPrincipalName` as email alternative - Global Catalog for multi-domain searches **Phoenix Config for AD**: ```bash PHOENIX_LDAP_HOST="ad.corp.example.com" PHOENIX_LDAP_PORT=389 PHOENIX_LDAP_USER_SEARCH_FILTER="(&(objectClass=user)(sAMAccountName=%s))" PHOENIX_LDAP_ATTR_EMAIL="mail" PHOENIX_LDAP_ATTR_DISPLAY_NAME="displayName" PHOENIX_LDAP_ATTR_MEMBER_OF="memberOf" ``` --- ### 2. OpenLDAP **What**: Open-source LDAP server (common in Linux environments) **Unique Features**: - `uid` for username - POSIX groups (requires separate group search) - More RFC-compliant than AD **Phoenix Config for OpenLDAP**: ```bash PHOENIX_LDAP_HOST="ldap.example.com" PHOENIX_LDAP_PORT=389 PHOENIX_LDAP_USER_SEARCH_BASE_DNS='["ou=people,dc=example,dc=com"]' PHOENIX_LDAP_USER_SEARCH_FILTER="(&(objectClass=inetOrgPerson)(uid=%s))" PHOENIX_LDAP_GROUP_SEARCH_BASE_DNS='["ou=groups,dc=example,dc=com"]' PHOENIX_LDAP_GROUP_SEARCH_FILTER="(memberUid=%s)" ``` --- ### 3. 389 Directory Server **What**: Red Hat's LDAP server (OpenLDAP successor) **Unique Features**: - Similar to OpenLDAP - Better performance and replication - Multi-master support --- ## Security Considerations ### TLS/SSL (Encryption) **Two modes** (like HTTPS vs HTTP): ```mermaid sequenceDiagram participant Phoenix participant LDAP as LDAP Server Note over Phoenix,LDAP: STARTTLS (port 389) Phoenix->>LDAP: 1. Connect (plaintext) Phoenix->>LDAP: 2. STARTTLS command LDAP->>Phoenix: 3. Ready for TLS Phoenix->>LDAP: 4. TLS handshake Note over Phoenix,LDAP: Encrypted from here Phoenix->>LDAP: 5. Bind + search (encrypted) Note over Phoenix,LDAP: --- Note over Phoenix,LDAP: LDAPS (port 636) Phoenix->>LDAP: 1. TLS handshake (immediate) Note over Phoenix,LDAP: Encrypted from start Phoenix->>LDAP: 2. Bind + search (encrypted) ``` **STARTTLS (port 389)**: - Connect in plaintext, then upgrade to TLS - **Phoenix default** (matches HTTP→HTTPS upgrade pattern) **LDAPS (port 636)**: - TLS from connection start (like HTTPS) **OIDC Equivalent**: Always HTTPS (TLS from start). **Phoenix Config**: ```bash # Default: STARTTLS on port 389 PHOENIX_LDAP_TLS_MODE="starttls" # Or: LDAPS on port 636 PHOENIX_LDAP_TLS_MODE="ldaps" PHOENIX_LDAP_PORT=636 ``` --- ### LDAP Injection (Like SQL Injection) **Attack**: Inject special characters into username to manipulate search filters. Example: ```python # Vulnerable code (DON'T DO THIS) username = request.username # "admin)(uid=*" filter_str = f"(uid={username})" # Result: (uid=admin)(uid=*) ← Matches all users! ``` **Phoenix Mitigation**: ```python from ldap3.utils.conv import escape_filter_chars username = escape_filter_chars(request.username) # Escapes special chars filter_str = f"(uid={username})" ``` **OIDC Equivalent**: Not applicable - OIDC uses HTTP/JSON, not custom query language. --- ## Phoenix LDAP Implementation Highlights ### DN-Based Identification (Stable Primary Key) Phoenix stores **DN** (not email) as primary identifier: ```mermaid graph LR subgraph LDAP Directory User["User in LDAP<br/>DN: uid=alice,ou=eng,...<br/>email: alice@example.com"] end subgraph Phoenix Database DBUser["User Record<br/>oauth2_user_id: uid=alice,ou=eng,...<br/>email: alice@example.com"] end User -->|Login| DBUser subgraph Email Change Scenario User2["User in LDAP<br/>DN: uid=alice,ou=eng,...<br/>email: alice@newdomain.com"] DBUser2["Same User Record<br/>oauth2_user_id: uid=alice,ou=eng,...<br/>email: alice@newdomain.com"] User2 -->|Next login<br/>DN matches,<br/>email updated| DBUser2 end ``` **Why DN?** - ✅ Stable across email changes - ✅ Unique per LDAP directory - ✅ Matches Grafana's approach **OIDC Equivalent**: Store `sub` claim (stable), sync email on each login. --- ### RFC 4514 Canonicalization (Prevent Duplicates) DNs are **case-insensitive** and allow whitespace variations: ``` Same user, different representations: - "UID=Alice,OU=Users,DC=Example,DC=Com" - "uid=alice, ou=users, dc=example, dc=com" - "uid=alice,ou=users,dc=example,dc=com" ``` Phoenix **canonicalizes** before storing: ```python from phoenix.server.ldap import canonicalize_dn dn_raw = "UID=Alice, OU=Users, DC=Example, DC=Com" dn_canonical = canonicalize_dn(dn_raw) # Result: "uid=alice,ou=users,dc=example,dc=com" ``` **OIDC Equivalent**: Not needed - `sub` is always normalized by IdP. --- ### Email Fallback for Admin-Provisioned Users Phoenix supports **pre-provisioning** users before first LDAP login: ```python # Admin creates user at startup (DN unknown) user = models.User( email="alice@example.com", oauth2_client_id=LDAP_CLIENT_ID_MARKER, oauth2_user_id=None, # DN unknown yet ) # First LDAP login: # 1. DN lookup fails (oauth2_user_id=NULL) # 2. Email fallback finds user # 3. Phoenix upgrades: oauth2_user_id = canonical_dn # 4. Future logins use DN ``` **OIDC Equivalent**: Similar pattern - provision user by email, link on first login via `sub` claim. --- ## When to Choose LDAP vs OIDC ### Choose LDAP When: ✅ **Enterprise internal apps** - Existing Active Directory infrastructure - All users in same organization - Corporate network (VPN required) ✅ **Legacy integration requirements** - Must integrate with existing LDAP-based systems - No budget for IdP migration ✅ **Fine-grained group management** - Complex organizational structure in directory - Groups managed by IT (not developers) ### Choose OIDC When: ✅ **Modern architecture** - Cloud-native apps - Microservices - External users (customers, partners) ✅ **SaaS convenience** - Managed IdP (Okta, Auth0, Keycloak) - Multi-tenant apps - "Sign in with Google/GitHub" patterns ✅ **Stateless authentication** - JWT tokens (no session server) - API-first design ### Use Both (Phoenix's Approach): Phoenix supports **both LDAP and OIDC simultaneously**: - Internal users → LDAP (corporate directory) - External users → OIDC (Google, GitHub, etc.) - Admins → Local (password) ```bash # Enable all three PHOENIX_ENABLE_AUTH="true" PHOENIX_DISABLE_BASIC_AUTH="false" # Local password PHOENIX_OAUTH2_CLIENT_ID="..." # OIDC PHOENIX_LDAP_HOST="ldap.corp.com" # LDAP ``` --- ## Further Reading **RFCs (Standards)**: - [RFC 4511](https://www.rfc-editor.org/rfc/rfc4511.html) - LDAP Protocol - [RFC 4514](https://www.rfc-editor.org/rfc/rfc4514.html) - Distinguished Names (DN) - [RFC 4515](https://www.rfc-editor.org/rfc/rfc4515.html) - Search Filters - [RFC 4516](https://www.rfc-editor.org/rfc/rfc4516.html) - LDAP URLs **OIDC for Comparison**: - [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) - [OAuth 2.0 RFC 6749](https://www.rfc-editor.org/rfc/rfc6749.html) **Phoenix LDAP Docs**: - [Main Specification](../ldap-authentication.md) - [Configuration Reference](./configuration.md) - [Security Deep-Dive](./security.md) - [Grafana Comparison](./grafana-comparison.md) ---

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