> ## Documentation Index
> Fetch the complete documentation index at: https://docs.strata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# LDAP

LDAP service extensions let you customize how the Orchestrator handles directory operations when running as an LDAP provider. Use these hooks to implement custom search logic or authenticate users against external systems.

## Request Lifecycle

The LDAP provider operates over TCP using the LDAP protocol (not HTTP). Each operation type has its own processing flow and hook point.

### Search

```mermaid theme={null}
%%{init: {'theme': 'neutral', 'themeVariables': {'edgeWidth': 4}}}%%
flowchart TD
    REQ[LDAP search request] --> ROOT{Root DSE query?}
    ROOT -- Yes --> DSE[Return Root DSE attributes]
    ROOT -- No --> BOUND{Connection authenticated?}
    BOUND -- No --> ERR1[Return operations error]
    BOUND -- Yes --> SE{searchSE configured?}
    SE -- No --> ERR2[Return error — no search handler]
    SE -- Yes --> SEARCH[searchSE]
    SEARCH -- error --> ERR3[Return LDAP error]
    SEARCH -- success --> RES[Return search result entries]

    classDef hook stroke:#6A3EC8,stroke-width:6px
    class SEARCH hook
```

### Simple Bind Authentication

```mermaid theme={null}
%%{init: {'theme': 'neutral', 'themeVariables': {'edgeWidth': 4}}}%%
flowchart TD
    REQ[LDAP Bind request] --> ENABLED{Simple auth enabled?}
    ENABLED -- No --> ERR1[Return auth method not supported]
    ENABLED -- Yes --> UNAUTH{Password provided?}
    UNAUTH -- No --> ERR2[Return error — password required]
    UNAUTH -- Yes --> SE[authenticateSE]
    SE -- error --> ERR3[Return error]
    SE -- valid --> OK[Bind success — connection authenticated]
    SE -- invalid --> DENY[Return invalid credentials]

    classDef hook stroke:#6A3EC8,stroke-width:6px
    class SE hook
```

## Hooks

### `searchSE`

Handle LDAP search requests by returning directory entries that match the query. Each entry maps a distinguished name (DN) to its attributes. Use this to implement custom search logic, query external directories, filter results, or build virtual directory entries from non-LDAP sources like databases or REST APIs.

**Signature:**

```go theme={null}
func Search(api orchestrator.Orchestrator, baseDN string, filter string, attrs []string) (map[string]map[string]interface{}, error)
```

**App types:** LDAP Provider

**Config location:** `ldapProvider.search.searchSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `baseDN` | `string` | The base distinguished name for the search |
| `filter` | `string` | The LDAP search filter expression |
| `attrs` | `[]string` | The list of attribute names to return |

**Returns:**

* `map[string]map[string]interface{}` -- a map of DNs to attribute maps, where each attribute map contains the requested attribute name-value pairs
* `error` -- return `nil` on success, or an error if the search fails

***

### `authenticateSE`

Authenticate a user via LDAP simple bind. Return `true` if the credentials are valid, or `false` to deny authentication. Use this to validate credentials against an external system, implement custom password policies, or bridge LDAP authentication to a non-LDAP identity store.

**Signature:**

```go theme={null}
func Authenticate(api orchestrator.Orchestrator, dn string, password string) (bool, error)
```

**App types:** LDAP Provider

**Config location:** `ldapProvider.authentication.methods.simple.authenticateSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `dn` | `string` | The distinguished name of the user attempting to bind |
| `password` | `string` | The password provided by the user |

**Returns:**

* `bool` -- `true` if the credentials are valid, `false` otherwise
* `error` -- return `nil` on success, or an error if the authentication process fails

**Examples:**

> **Delegate simple bind to an IdP via ROPC**
>
> When an LDAP client performs a simple bind, this extension extracts the
> username from the bind DN and authenticates the user against an OIDC identity
> provider using the Resource Owner Password Credentials (ROPC) grant. This
> bridges LDAP authentication with a modern IdP without changes to the
> client application.
>
> **Extension**
>
> ```go ldap-authenticate.go theme={null}
> package main
>
> import (
>     "errors"
>     "fmt"
>     "strings"
>
>     "github.com/strata-io/service-extension/idfabric"
>     "github.com/strata-io/service-extension/orchestrator"
> )
>
> func Authenticate(
>     api orchestrator.Orchestrator,
>     dn string,
>     password string,
> ) (bool, error) {
>     logger := api.Logger()
>     logger.Debug("se", "ldap-auth", "msg", "authenticating user", "dn", dn)
>
>     // Extract the username from the bind DN.
>     username, err := parseUsername(dn)
>     if err != nil {
>         logger.Error("se", "ldap-auth", "msg", "failed to parse DN", "error", err.Error())
>         return false, err
>     }
>
>     // Delegate authentication to the OIDC provider via ROPC.
>     idp, err := api.IdentityProvider("oidc")
>     if err != nil {
>         logger.Error("se", "ldap-auth", "msg", "failed to get IdP", "error", err)
>         return false, err
>     }
>
>     var result idfabric.LoginResult
>     idp.Login(
>         nil,
>         nil,
>         idfabric.WithGrantTypeROPC(
>             idfabric.ROPCRequest{Username: username, Password: password},
>             &result,
>         ),
>     )
>
>     if result.Error != nil {
>         logger.Error("se", "ldap-auth", "msg", "authentication failed", "error", result.Error)
>         return false, result.Error
>     }
>
>     return true, nil
> }
>
> // parseUsername extracts the value of the first RDN from a DN.
> // e.g., "cn=jdoe,ou=users,dc=example,dc=com" -> "jdoe"
> func parseUsername(dn string) (string, error) {
>     parts := strings.Split(dn, ",")
>     if len(parts) == 0 {
>         return "", errors.New("invalid DN format")
>     }
>
>     rdn := parts[0]
>     eqIdx := strings.Index(rdn, "=")
>     if eqIdx < 0 || eqIdx == len(rdn)-1 {
>         return "", fmt.Errorf("malformed RDN: %s", rdn)
>     }
>
>     return rdn[eqIdx+1:], nil
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: oidc
>     type: oidc
>     oidcWellKnownURL: https://idp.example.com/.well-known/openid-configuration
>     oauthClientID: my-client
>     oauthClientSecret: <client_secret>
>     scopes: openid profile email
>
> ldapProvider:
>   enabled: true
>   uri: ldaps://0.0.0.0:636
>   authentication:
>     allowAnonymous: false
>     methods:
>       simple:
>         enabled: true
>         authenticateSE:
>           file: /etc/maverics/service-extensions/ldap-authenticate.go
>           funcName: Authenticate
> ```

> **Add TOTP multi-factor authentication to simple bind**
>
> Applications that authenticate via LDAP simple bind have no built-in way
> to support multi-factor authentication. This extension works around that
> limitation by treating the password field as a concatenation of the real
> password and a TOTP code (e.g., `myP@ssword12345678`). The password portion
> is verified against an IdP via ROPC, and the TOTP code is verified against
> an external MFA API.
>
> **Extension**
>
> ```go ldap-authenticate-totp.go theme={null}
> package main
>
> import (
>     "encoding/base64"
>     "fmt"
>     "io"
>     "net/http"
>     "net/url"
>     "strings"
>
>     "github.com/strata-io/service-extension/idfabric"
>     "github.com/strata-io/service-extension/orchestrator"
> )
>
> const (
>     // Number of characters at the end of the password that
>     // contain the TOTP code.
>     totpCodeLength = 8
> )
>
> func Authenticate(
>     api orchestrator.Orchestrator,
>     dn string,
>     password string,
> ) (bool, error) {
>     logger := api.Logger()
>     logger.Debug("se", "ldap-auth", "msg", "authenticating user", "dn", dn)
>
>     // Split the password into the real password and appended TOTP code.
>     if len(password) < totpCodeLength {
>         logger.Debug("se", "ldap-auth", "msg", "password too short to contain TOTP code")
>         return false, nil
>     }
>     actualPassword := password[:len(password)-totpCodeLength]
>     totpCode := password[len(password)-totpCodeLength:]
>
>     // Step 1: Verify the password via ROPC against the IdP.
>     idp, err := api.IdentityProvider("oidc")
>     if err != nil {
>         logger.Error("se", "ldap-auth", "msg", "failed to get IdP", "error", err)
>         return false, err
>     }
>
>     username, err := parseUsername(dn)
>     if err != nil {
>         return false, err
>     }
>
>     var loginResult idfabric.LoginResult
>     idp.Login(
>         nil,
>         nil,
>         idfabric.WithGrantTypeROPC(
>             idfabric.ROPCRequest{Username: username, Password: actualPassword},
>             &loginResult,
>         ),
>     )
>
>     if loginResult.Error != nil {
>         logger.Error("se", "ldap-auth", "msg", "password verification failed", "error", loginResult.Error)
>         return false, loginResult.Error
>     }
>
>     // Step 2: Verify the TOTP code against the MFA provider.
>     verified, err := verifyTOTP(api, totpCode)
>     if err != nil {
>         logger.Error("se", "ldap-auth", "msg", "TOTP verification error", "error", err)
>         return false, err
>     }
>
>     if !verified {
>         logger.Debug("se", "ldap-auth", "msg", "TOTP verification failed")
>         return false, nil
>     }
>
>     return true, nil
> }
>
> // verifyTOTP calls an external MFA provider API to verify the TOTP code.
> func verifyTOTP(api orchestrator.Orchestrator, totpCode string) (bool, error) {
>     sp := api.SecretProvider()
>     accountSID, err := sp.GetString("mfa-account-sid")
>     if err != nil {
>         return false, fmt.Errorf("failed to get MFA account SID: %w", err)
>     }
>     authToken, err := sp.GetString("mfa-auth-token")
>     if err != nil {
>         return false, fmt.Errorf("failed to get MFA auth token: %w", err)
>     }
>     serviceID, err := sp.GetString("mfa-service-id")
>     if err != nil {
>         return false, fmt.Errorf("failed to get MFA service ID: %w", err)
>     }
>     entityID, err := sp.GetString("mfa-entity-id")
>     if err != nil {
>         return false, fmt.Errorf("failed to get MFA entity ID: %w", err)
>     }
>
>     apiURL := fmt.Sprintf(
>         "https://verify.example.com/v2/Services/%s/Entities/%s/Challenges",
>         serviceID, entityID,
>     )
>
>     formData := url.Values{}
>     formData.Set("AuthPayload", totpCode)
>
>     req, err := http.NewRequest(http.MethodPost, apiURL, strings.NewReader(formData.Encode()))
>     if err != nil {
>         return false, fmt.Errorf("failed to create request: %w", err)
>     }
>
>     req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
>     auth := base64.StdEncoding.EncodeToString([]byte(accountSID + ":" + authToken))
>     req.Header.Set("Authorization", "Basic "+auth)
>
>     resp, err := api.HTTP().DefaultClient().Do(req)
>     if err != nil {
>         return false, fmt.Errorf("failed to call MFA API: %w", err)
>     }
>     defer resp.Body.Close()
>
>     body, _ := io.ReadAll(resp.Body)
>     api.Logger().Debug("se", "ldap-auth", "msg", "MFA API response", "status", resp.StatusCode, "body", string(body))
>
>     return resp.StatusCode == http.StatusCreated, nil
> }
>
> // parseUsername extracts the value of the first RDN from a DN.
> func parseUsername(dn string) (string, error) {
>     parts := strings.Split(dn, ",")
>     if len(parts) == 0 {
>         return "", fmt.Errorf("invalid DN format")
>     }
>
>     rdn := parts[0]
>     eqIdx := strings.Index(rdn, "=")
>     if eqIdx < 0 || eqIdx == len(rdn)-1 {
>         return "", fmt.Errorf("malformed RDN: %s", rdn)
>     }
>
>     return rdn[eqIdx+1:], nil
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: oidc
>     type: oidc
>     oidcWellKnownURL: https://idp.example.com/.well-known/openid-configuration
>     oauthClientID: my-client
>     oauthClientSecret: <client_secret>
>     scopes: openid profile email
>
> ldapProvider:
>   enabled: true
>   uri: ldaps://0.0.0.0:636
>   authentication:
>     allowAnonymous: false
>     methods:
>       simple:
>         enabled: true
>         authenticateSE:
>           file: /etc/maverics/service-extensions/ldap-authenticate-totp.go
>           funcName: Authenticate
> ```

## Related Pages

> **[Service Extensions Overview](/reference/orchestrator/service-extensions)**
>
> Configuration, SDK reference, and best practices

> **[LDAP Provider Mode](/reference/modes/ldap-provider)**
>
> LDAP provider mode configuration and setup
