> ## 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.

# Proxy

Proxy service extensions let you customize how the Orchestrator handles requests to your protected web applications. Use these hooks to control authentication, enforce authorization policies, enrich requests with user attributes, inject headers, modify requests and responses, and automate login to upstream applications.

## Request Lifecycle

The following diagram shows the Orchestrator's request processing pipeline for proxy apps. Each service extension hook is shown at the point where it executes in the flow.

```mermaid theme={null}
%%{init: {'theme': 'neutral', 'themeVariables': {'edgeWidth': 4}}}%%
flowchart TD
    REQ[Incoming request] --> MATCH{Request matches a policy?}
    MATCH -- No --> DENY1[Return 403 Forbidden]
    MATCH -- Yes --> OPEN{Public access allowed?}
    OPEN -- Yes --> ISLOGGED
    OPEN -- No --> ISAUTH{isAuthenticatedSE}
    ISAUTH -- "true" --> LOADATTR[loadAttrsSE]
    ISAUTH -- "false" --> AUTH[authenticateSE]
    AUTH --> REDIR[Redirect to identity provider]
    LOADATTR --> AUTHZ{isAuthorizedSE}
    AUTHZ -- authorized --> HEADERS[createHeaderSE]
    AUTHZ -- denied --> UNAUTH[handleUnauthorizedSE]
    HEADERS --> ISLOGGED{isLoggedInSE}
    ISLOGGED -- "true" --> MODREQ[modifyRequestSE]
    ISLOGGED -- "false" --> LOGIN[loginSE]
    LOGIN --> UPLOGIN[Log in to upstream app]
    MODREQ --> PROXY[Forward request to upstream app]
    PROXY --> MODRESP[modifyResponseSE]
    MODRESP --> RESP[Return response to user]

    classDef hook stroke:#6A3EC8,stroke-width:6px
    class ISAUTH,AUTH,LOADATTR,AUTHZ,HEADERS,UNAUTH,ISLOGGED,LOGIN,MODREQ,MODRESP hook
```

When a request arrives, the Orchestrator matches it against configured policies. For protected policies, the flow proceeds through authentication, attribute loading, authorization, header injection, optional upstream login, and request/response modification before the response is returned to the user. The authorization step evaluates `isAuthorizedSE` if configured, along with any declarative authorization rules defined on the policy.

> [!NOTE]
>
> Policy decisions (authorization result and headers) are cached in the user's session. On subsequent requests, cached decisions skip directly to the upstream login check, bypassing the authentication, attribute loading, authorization, and header building steps.

## Hooks

### `isAuthenticatedSE`

Determine whether the current user is already authenticated. Return `true` to skip the login flow, or `false` to send the user through authentication. Use this when you need custom logic to check authentication status -- for example, validating an external session token or checking a cookie from another system.

**Signature:**

```go theme={null}
func IsAuthenticated(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
```

**Config location:** `apps[].policies[].authentication.isAuthenticatedSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer for the current request |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:** `bool` -- `true` if the user is authenticated, `false` otherwise.

**Examples:**

> **Build an IdP selector for multi-provider authentication**
>
> See the [full example under `authenticateSE`](#authenticatese) for a combined
> implementation that uses both `isAuthenticatedSE` and `authenticateSE` to let
> users choose which identity provider to authenticate with.

> **Bridge HTTP Basic Auth to LDAP authentication**
>
> See the [full example under `authenticateSE`](#authenticatese) for a combined
> implementation that uses both `isAuthenticatedSE` and `authenticateSE` to
> authenticate users against an LDAP directory over TLS.

***

### `authenticateSE`

Handle authentication when the user has not yet logged in. Use this to redirect to an external login page, validate credentials directly, or start a custom authentication flow.

**Signature:**

```go theme={null}
func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request)
```

**Config location:** `apps[].policies[].authentication.authenticateSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer -- use to redirect or return an error response |
| `req` | `*http.Request` | The incoming HTTP request |

**Examples:**

> **Build an IdP selector for multi-provider authentication**
>
> When multiple identity providers are available, you can give users a
> choice of which one to authenticate with. This extension pairs
> `isAuthenticatedSE` and `authenticateSE` to implement an IdP selector.
> The `IsAuthenticated` function checks the session to determine if the user
> has already authenticated with any configured IdP, while `Authenticate`
> renders a selector form and delegates login to the chosen provider.
>
> **isAuthenticatedSE**
>
> ```go idp-selector.go theme={null}
> package main
>
> import (
>     "fmt"
>     "strings"
>     "net/http"
>
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/session"
> )
>
> // IsAuthenticated checks whether the user has an active session with
> // any of the configured identity providers. The list of IdP names is
> // read from the service extension's metadata.
> func IsAuthenticated(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) bool {
>     logger := api.Logger()
>     logger.Info("se", "determining if user is authenticated")
>
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         logger.Error("se", "unable to retrieve session", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return false
>     }
>
>     // Read the comma-separated list of IdP names from the SE metadata.
>     metadata := api.Metadata()
>     idpNames := strings.Split(metadata["idps"].(string), ",")
>
>     for _, idpName := range idpNames {
>         authenticated, err := sess.GetBool(idpName + ".authenticated")
>         if err != nil {
>             logger.Error(
>                 "se", fmt.Sprintf("unable to retrieve session value '%s.authenticated'", idpName),
>                 "error", err.Error(),
>             )
>         }
>         if authenticated {
>             logger.Info("se", fmt.Sprintf("user is authenticated with '%s'", idpName))
>             return true
>         }
>     }
>
>     return false
> }
> ```
>
> **authenticateSE**
>
> ```go idp-selector.go theme={null}
> package main
>
> import (
>     "fmt"
>     "net/http"
>
>     "github.com/strata-io/service-extension/orchestrator"
> )
>
> // Authenticate renders an IdP selector form on GET and delegates
> // authentication to the selected provider on POST.
> func Authenticate(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) {
>     logger := api.Logger()
>     logger.Info("se", "authenticating user")
>
>     if req.Method == http.MethodGet {
>         // Render the IdP selector form. In production, this markup can
>         // be loaded from a template file, styled to match the application,
>         // or dynamically generated from the configured IdP list.
>         _, _ = rw.Write([]byte(idpForm))
>         return
>     }
>
>     if req.Method != http.MethodPost {
>         http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
>         return
>     }
>
>     err := req.ParseForm()
>     if err != nil {
>         logger.Error("se", "failed to parse form from request", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>
>     selectedIDP := req.Form.Get("idp")
>     logger.Info("se", fmt.Sprintf("user selected '%s' for authentication", selectedIDP))
>
>     idp, err := api.IdentityProvider(selectedIDP)
>     if err != nil {
>         logger.Error("se", "unable to lookup IdP", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>
>     idp.Login(rw, req)
> }
>
> const idpForm = `
> <!DOCTYPE html>
> <html lang="en">
> <head>
>     <title>IdP Selector</title>
>     <meta charset="utf-8">
> </head>
> <body>
>     <form method="post">
>         <label for="idp">Select identity provider:</label>
>         <select id="idp" name="idp">
>             <option value="entra">Entra</option>
>             <option value="okta">Okta</option>
>         </select>
>         <input type="submit" value="Continue">
>     </form>
> </body>
> </html>`
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: entra
>     type: oidc
>     oidcWellKnownURL: https://login.microsoftonline.com/<tenant_id>/v2.0/.well-known/openid-configuration
>     oauthClientID: <entra_client_id>
>     oauthClientSecret: <entra_client_secret>
>     scopes: openid profile email
>
>   - name: okta
>     type: oidc
>     oidcWellKnownURL: https://dev-123456.okta.com/.well-known/openid-configuration
>     oauthClientID: <okta_client_id>
>     oauthClientSecret: <okta_client_secret>
>     scopes: openid profile email
>
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     policies:
>       - location: /
>         authentication:
>           idps:
>             - entra
>             - okta
>           isAuthenticatedSE:
>             funcName: IsAuthenticated
>             file: /etc/maverics/extensions/idp-selector.go
>             metadata:
>               idps: entra,okta
>           authenticateSE:
>             funcName: Authenticate
>             file: /etc/maverics/extensions/idp-selector.go
> ```

> **Force re-authentication on every login**
>
> Some applications require the user to actively re-authenticate at the
> identity provider on every login -- for example, when returning after
> logging out of a sensitive application -- rather than silently reusing an
> existing IdP session. This extension pairs `isAuthenticatedSE` and
> `authenticateSE`: `IsAuthenticated` checks whether the SAML connector has
> already authenticated the user, and `Authenticate` starts login with the
> `WithForceAuthentication` option, which sets `ForceAuthn="true"` on the SAML
> AuthnRequest so the IdP prompts for credentials again.
>
> > [!NOTE]
> >
> > Forced re-authentication is driven entirely by the service extension. Call
> > `idp.Login` with `idfabric.WithForceAuthentication()` on every login to
> > always re-prompt, or apply it conditionally -- for example, only for step-up
> > scenarios -- to decide per request.
>
> **isAuthenticatedSE**
>
> ```go force-authn.go theme={null}
> package main
>
> import (
>     "net/http"
>
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/session"
> )
>
> // IsAuthenticated reports whether the user has already authenticated
> // with the "saml" connector. The connector records this as the session
> // boolean "<connector>.authenticated".
> func IsAuthenticated(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) bool {
>     logger := api.Logger()
>
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         logger.Error("se", "unable to retrieve session", "error", err.Error())
>         return false
>     }
>
>     authenticated, _ := sess.GetBool("saml.authenticated")
>     return authenticated
> }
> ```
>
> **authenticateSE**
>
> ```go force-authn.go theme={null}
> package main
>
> import (
>     "net/http"
>
>     "github.com/strata-io/service-extension/idfabric"
>     "github.com/strata-io/service-extension/orchestrator"
> )
>
> // Authenticate starts login against the "saml" connector, forcing the
> // IdP to actively re-authenticate the user even if they already have an
> // IdP session. WithForceAuthentication sets ForceAuthn="true" on the
> // SAML AuthnRequest.
> func Authenticate(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) {
>     logger := api.Logger()
>
>     idp, err := api.IdentityProvider("saml")
>     if err != nil {
>         logger.Error("se", "unable to lookup IdP", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>
>     idp.Login(rw, req, idfabric.WithForceAuthentication())
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: saml
>     type: saml
>     samlMetadataURL: https://idp.example.com/metadata
>     samlConsumerServiceURL: https://orch.example.com/saml/callback
>     samlEntityID: https://orch.example.com
>
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     policies:
>       - location: /
>         authentication:
>           idps:
>             - saml
>           isAuthenticatedSE:
>             funcName: IsAuthenticated
>             file: /etc/maverics/extensions/force-authn.go
>           authenticateSE:
>             funcName: Authenticate
>             file: /etc/maverics/extensions/force-authn.go
> ```

> **Bridge HTTP Basic Auth to LDAP authentication**
>
> When an application relies on HTTP Basic Auth, this extension intercepts
> the credentials and authenticates the user against an LDAP directory over
> TLS. The `IsAuthenticated` function checks the session for a previous
> successful bind, while `Authenticate` extracts the Basic Auth credentials,
> connects to LDAP with StartTLS, and performs a bind to verify them.
>
> **isAuthenticatedSE**
>
> ```go ldap-tls-auth.go theme={null}
> package main
>
> import (
>     "fmt"
>     "net/http"
>
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/session"
> )
>
> // IsAuthenticated checks whether the user has already authenticated
> // against the LDAP directory in a previous request.
> func IsAuthenticated(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) bool {
>     logger := api.Logger()
>
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         logger.Error("se", "unable to retrieve session", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return false
>     }
>
>     metadata := api.Metadata()
>     idpName := metadata["idpName"].(string)
>
>     authenticated, err := sess.GetBool(fmt.Sprintf("%s.authenticated", idpName))
>     if err != nil {
>         logger.Error("se", "unable to check authentication status", "error", err.Error())
>         return false
>     }
>
>     return authenticated
> }
> ```
>
> **authenticateSE**
>
> ```go ldap-tls-auth.go theme={null}
> package main
>
> import (
>     "crypto/tls"
>     "crypto/x509"
>     "fmt"
>     "net/http"
>
>     ldap3 "github.com/go-ldap/ldap/v3"
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/session"
> )
>
> // Authenticate extracts HTTP Basic Auth credentials from the request
> // and verifies them by binding against an LDAP directory over TLS.
> func Authenticate(
>     api orchestrator.Orchestrator,
>     rw http.ResponseWriter,
>     req *http.Request,
> ) {
>     logger := api.Logger()
>
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         logger.Error("se", "unable to retrieve session", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>
>     sp := api.SecretProvider()
>     metadata := api.Metadata()
>     idpName := metadata["idpName"].(string)
>     ldapURL := metadata["ldapURL"].(string)
>     ldapServerName := metadata["ldapServerName"].(string)
>     ldapBaseDN := metadata["ldapBaseDN"].(string)
>
>     username, password, ok := req.BasicAuth()
>     if !ok {
>         logger.Error("se", "unable to read Basic Auth headers")
>         http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
>         return
>     }
>
>     // Load the CA certificate for the LDAP server from the secret provider.
>     caCert, err := sp.GetString("ldapCACert")
>     if err != nil {
>         logger.Error("se", "unable to get LDAP CA cert", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>     certPool, err := x509.SystemCertPool()
>     if err != nil {
>         logger.Error("se", "unable to load system cert pool", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>     certPool.AppendCertsFromPEM([]byte(caCert))
>
>     // Connect to LDAP with TLS.
>     conn, err := ldap3.DialURL(ldapURL, ldap3.DialWithTLSConfig(&tls.Config{
>         ServerName: ldapServerName,
>         RootCAs:    certPool,
>     }))
>     if err != nil {
>         logger.Error("se", "unable to dial LDAP", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>     defer conn.Close()
>
>     // Bind with the user's credentials to verify them.
>     bindDN := fmt.Sprintf("uid=%s,%s", username, ldapBaseDN)
>     err = conn.Bind(bindDN, password)
>     if err != nil {
>         logger.Error("se", "LDAP bind failed", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
>         return
>     }
>
>     // Store authentication status in the session.
>     _ = sess.SetBool(fmt.Sprintf("%s.authenticated", idpName), true)
>     _ = sess.SetString(fmt.Sprintf("%s.cn", idpName), username)
>     if err := sess.Save(); err != nil {
>         logger.Error("se", "unable to save session", "error", err.Error())
>         http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
>         return
>     }
>
>     logger.Info("se", "user authenticated via LDAP", "username", username)
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     policies:
>       - location: /
>         authentication:
>           isAuthenticatedSE:
>             funcName: IsAuthenticated
>             file: /etc/maverics/extensions/ldap-tls-auth.go
>             metadata:
>               idpName: ldap
>           authenticateSE:
>             funcName: Authenticate
>             file: /etc/maverics/extensions/ldap-tls-auth.go
>             metadata:
>               idpName: ldap
>               ldapURL: "ldaps://ldap.example.com"
>               ldapServerName: ldap.example.com
>               ldapBaseDN: "ou=People,dc=example,dc=com"
>         authorization:
>           allowAll: true
> ```

***

### `isAuthorizedSE`

Decide whether an authenticated user is allowed to access the requested resource. Return `true` to allow the request or `false` to deny it. Use this to call an external policy engine, enforce attribute-based access control (ABAC), or apply custom business rules beyond what declarative authorization rules support.

**Signature:**

```go theme={null}
func IsAuthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
```

**Config location:** `apps[].policies[].authorization.isAuthorizedSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer for the current request |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:** `bool` -- `true` if the user is authorized, `false` otherwise.

***

### `handleUnauthorizedSE`

Customize the response when a user fails authorization. Use this to redirect to a custom error page, return a specific error code, or log additional context about the denied request.

**Signature:**

```go theme={null}
func HandleUnauthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request)
```

**Config location:** `apps[].handleUnauthorizedSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer -- use to write a custom error response or redirect |
| `req` | `*http.Request` | The incoming HTTP request that was denied |

***

### `loadAttrsSE`

Enrich the user's session with additional attributes before the request is processed. Use this to pull in user details from external sources -- such as an LDAP directory, a database, or a REST API -- transform attribute values, or merge attributes from multiple identity providers.

**Signature:**

```go theme={null}
func LoadAttrs(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error
```

**Config location:** `apps[].loadAttrsSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer for the current request |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:** `error` -- return `nil` on success, or an error to indicate attribute loading failed.

**Examples:**

> **Load LDAP group memberships into the session**
>
> When authorization decisions depend on group memberships stored in an LDAP
> directory, this extension queries LDAP for the authenticated user's groups
> and stores them in the session. Downstream hooks like `isAuthorizedSE` or
> `createHeaderSE` can then read the groups from the session without
> repeating the LDAP lookup.
>
> **Extension**
>
> ```go load-ldap-groups.go theme={null}
> package main
>
> import (
>     "crypto/tls"
>     "crypto/x509"
>     "errors"
>     "fmt"
>     "net/http"
>     "strings"
>
>     ldap3 "github.com/go-ldap/ldap/v3"
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/secret"
>     "github.com/strata-io/service-extension/session"
> )
>
> func LoadAttrs(
>     api orchestrator.Orchestrator,
>     _ http.ResponseWriter,
>     req *http.Request,
> ) error {
>     logger := api.Logger()
>     logger.Info("se", "loading attributes from LDAP")
>
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         return fmt.Errorf("unable to retrieve session: %w", err)
>     }
>
>     sp := api.SecretProvider()
>     metadata := api.Metadata()
>     ldapServerName := metadata["ldapServerName"].(string)
>     ldapBaseDN := metadata["ldapBaseDN"].(string)
>     ldapFilterFmt := metadata["ldapFilterFmt"].(string)
>     delimiter := metadata["delimiter"].(string)
>
>     // Look up the authenticated user's email to build the LDAP filter.
>     uid, err := sess.GetString("entra.email")
>     if err != nil {
>         return fmt.Errorf("failed to find user email for LDAP query: %w", err)
>     }
>
>     filter := fmt.Sprintf(ldapFilterFmt, uid)
>     groups, err := queryLDAPGroups(ldapServerName, ldapBaseDN, filter, sp)
>     if err != nil {
>         return fmt.Errorf("unable to get groups: %w", err)
>     }
>
>     list := strings.Join(groups, delimiter)
>     logger.Debug("se", "setting groups attribute", "se.groups", list)
>
>     err = sess.SetString("se.groups", list)
>     if err != nil {
>         return fmt.Errorf("unable to set 'se.groups' in session: %w", err)
>     }
>
>     return sess.Save()
> }
>
> // queryLDAPGroups connects to the LDAP server with TLS, binds with a
> // service account, and searches for group CNs matching the filter.
> func queryLDAPGroups(
>     serverName, baseDN, filter string,
>     sp secret.Provider,
> ) ([]string, error) {
>     conn, err := ldap3.DialURL(fmt.Sprintf("ldap://%s", serverName))
>     if err != nil {
>         return nil, fmt.Errorf("unable to dial LDAP: %w", err)
>     }
>     defer conn.Close()
>
>     // Upgrade to TLS using the CA certificate from the secret provider.
>     caCert, err := sp.GetString("ldapCACert")
>     if err != nil {
>         return nil, fmt.Errorf("unable to get LDAP CA cert: %w", err)
>     }
>     certPool, err := x509.SystemCertPool()
>     if err != nil {
>         return nil, fmt.Errorf("unable to get system cert pool: %w", err)
>     }
>     if ok := certPool.AppendCertsFromPEM([]byte(caCert)); !ok {
>         return nil, errors.New("unable to append CA cert to pool")
>     }
>     err = conn.StartTLS(&tls.Config{
>         RootCAs:    certPool,
>         ServerName: serverName,
>     })
>     if err != nil {
>         return nil, fmt.Errorf("unable to start TLS: %w", err)
>     }
>
>     // Bind with the service account credentials.
>     username, err := sp.GetString("serviceAccountUsername")
>     if err != nil {
>         return nil, fmt.Errorf("unable to get service account username: %w", err)
>     }
>     password, err := sp.GetString("serviceAccountPassword")
>     if err != nil {
>         return nil, fmt.Errorf("unable to get service account password: %w", err)
>     }
>     err = conn.Bind(username, password)
>     if err != nil {
>         return nil, fmt.Errorf("unable to bind: %w", err)
>     }
>
>     searchReq := ldap3.NewSearchRequest(
>         baseDN,
>         ldap3.ScopeWholeSubtree, ldap3.NeverDerefAliases, 0, 0, false,
>         filter,
>         []string{"cn"},
>         nil,
>     )
>     result, err := conn.Search(searchReq)
>     if err != nil {
>         return nil, fmt.Errorf("unable to search LDAP: %w", err)
>     }
>
>     groups := make([]string, 0, len(result.Entries))
>     for _, entry := range result.Entries {
>         groups = append(groups, entry.GetAttributeValue("cn"))
>     }
>     return groups, nil
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: entra
>     type: oidc
>     oidcWellKnownURL: https://login.microsoftonline.com/<tenant_id>/v2.0/.well-known/openid-configuration
>     oauthClientID: <entra_client_id>
>     oauthClientSecret: <entra_client_secret>
>     scopes: openid profile email
>
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     # Pass the loaded groups to the upstream app as a header.
>     headers:
>       - name: X-User-Groups
>         value: "{{ se.groups }}"
>     loadAttrsSE:
>       funcName: LoadAttrs
>       file: /etc/maverics/extensions/load-ldap-groups.go
>       metadata:
>         ldapServerName: ldap.example.com
>         ldapBaseDN: dc=example,dc=com
>         ldapFilterFmt: "(&(uniquemember=uid=%s,ou=users,dc=example,dc=com))"
>         delimiter: ","
>     policies:
>       - location: /
>         authentication:
>           idps:
>             - entra
>         authorization:
>           allowAll: true
> ```

***

### `createHeaderSE`

Build custom HTTP headers to send to the upstream application along with the proxied request. Use this when header values need to be computed dynamically -- for example, constructing a header from session attributes, looking up a value from an external service, or encoding user information for the upstream app.

**Signature:**

```go theme={null}
func CreateHeader(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) (http.Header, error)
```

**Config location:** `apps[].headers[].createHeaderSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer for the current request |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:**

* `http.Header` -- a map of header names to values to inject into the upstream request
* `error` -- return `nil` on success, or an error if header creation fails

> [!WARNING]
>
> On [open routes](/reference/orchestrator/applications/proxy#header-security-on-open-routes) (`allowUnauthenticated` + `allowAll`), this extension does not run, and the header names it would emit are not known when configuration loads. As a result, a client-supplied copy of a header this extension normally sets is **not** stripped and is forwarded to the upstream. Do not mark a route open if the upstream trusts an identity header produced by a `createHeaderSE`.

**Examples:**

> **Build custom headers from session attributes**
>
> When an upstream application expects user identity in specific HTTP headers,
> this extension reads attributes from the user's session and constructs the
> required headers. Each `createHeaderSE` entry handles one header, allowing
> you to transform or combine attribute values as needed.
>
> **Extension**
>
> ```go create-headers.go theme={null}
> package main
>
> import (
>     "fmt"
>     "net/http"
>
>     "github.com/strata-io/service-extension/orchestrator"
>     "github.com/strata-io/service-extension/session"
> )
>
> // CreateDisplayNameHeader builds a display name header by combining the
> // user's first and last name from the session.
> func CreateDisplayNameHeader(
>     api orchestrator.Orchestrator,
>     _ http.ResponseWriter,
>     req *http.Request,
> ) (http.Header, error) {
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         return nil, fmt.Errorf("unable to retrieve session: %w", err)
>     }
>
>     firstName, err := sess.GetString("entra.given_name")
>     if err != nil {
>         return nil, fmt.Errorf("unable to retrieve attribute 'entra.given_name': %w", err)
>     }
>     lastName, err := sess.GetString("entra.family_name")
>     if err != nil {
>         return nil, fmt.Errorf("unable to retrieve attribute 'entra.family_name': %w", err)
>     }
>
>     header := make(http.Header)
>     header["X-Display-Name"] = []string{firstName + " " + lastName}
>     return header, nil
> }
>
> // CreateEmailHeader reads the user's email from the session and sets it
> // as a header for the upstream application.
> func CreateEmailHeader(
>     api orchestrator.Orchestrator,
>     _ http.ResponseWriter,
>     req *http.Request,
> ) (http.Header, error) {
>     sess, err := api.Session(session.WithRequest(req))
>     if err != nil {
>         return nil, fmt.Errorf("unable to retrieve session: %w", err)
>     }
>
>     email, err := sess.GetString("entra.email")
>     if err != nil {
>         return nil, fmt.Errorf("unable to retrieve attribute 'entra.email': %w", err)
>     }
>
>     header := make(http.Header)
>     header["X-User-Email"] = []string{email}
>     return header, nil
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> connectors:
>   - name: entra
>     type: oidc
>     oidcWellKnownURL: https://login.microsoftonline.com/<tenant_id>/v2.0/.well-known/openid-configuration
>     oauthClientID: <entra_client_id>
>     oauthClientSecret: <entra_client_secret>
>     scopes: openid profile email
>
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     headers:
>       - createHeaderSE:
>           funcName: CreateDisplayNameHeader
>           file: /etc/maverics/extensions/create-headers.go
>       - createHeaderSE:
>           funcName: CreateEmailHeader
>           file: /etc/maverics/extensions/create-headers.go
> ```

***

### `modifyRequestSE`

Modify the HTTP request before it is forwarded to the upstream application. Use this to add authentication headers, rewrite paths, inject tracing headers, or transform the request body.

**Signature:**

```go theme={null}
func ModifyRequest(api orchestrator.Orchestrator, req *http.Request)
```

**Config location:** `apps[].modifyRequestSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `req` | `*http.Request` | The outbound HTTP request to the upstream -- modify this object directly |

> [!NOTE]
>
> This hook runs on every proxied request. Keep it lightweight to avoid adding latency. Use `api.Cache()` to avoid repeating expensive lookups.

> [!WARNING]
>
> This hook runs on every request, including [open routes](/reference/orchestrator/applications/proxy#header-security-on-open-routes), and it runs *after* the Orchestrator strips managed headers. If your extension reads a client-supplied header and forwards it to the upstream, it can reintroduce attacker-controlled input that stripping would otherwise have removed. Do not pass through client-supplied identity headers on routes that reach a header-trusting upstream.

***

### `modifyResponseSE`

Modify the response from the upstream application before it reaches the user's browser. Use this to add security headers, transform response content, adjust status codes, or inject additional content.

**Signature:**

```go theme={null}
func ModifyResponse(api orchestrator.Orchestrator, resp *http.Response)
```

**Config location:** `apps[].modifyResponseSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `resp` | `*http.Response` | The HTTP response from the upstream -- modify this object directly |

> [!NOTE]
>
> This hook runs on every proxied response. Keep it lightweight to avoid adding latency. Use `api.Cache()` to avoid repeating expensive lookups.

**Examples:**

> **Inject a single logout button into HTML pages**
>
> Some applications lack single logout functionality. When the Orchestrator
> sits in front of such an application, there is no built-in way for users to
> trigger a federated logout because the upstream UI has no logout control
> that integrates with the Orchestrator.
>
> This service extension solves the problem by injecting a fixed-position
> "Single Logout" button into every HTML page returned by the upstream
> application. The button links to the Orchestrator's `/single-logout`
> endpoint, giving users a way to initiate federated logout without any changes
> to the upstream application's source code.
>
> **Extension**
>
> ```go inject-slo-button.go theme={null}
> package main
>
> import (
>     "bytes"
>     "io"
>     "net/http"
>     "strconv"
>     "strings"
>
>     "github.com/strata-io/service-extension/orchestrator"
> )
>
> // ModifyResponse intercepts HTML responses from the upstream
> // application and injects a fixed-position "Single Logout" button
> // before the closing </body> tag.
> func ModifyResponse(api orchestrator.Orchestrator, resp *http.Response) {
>     // Only modify HTML responses. Non-HTML content (e.g., images, JSON
>     // API responses) is passed through unchanged.
>     contentType := resp.Header.Get("Content-Type")
>     if !strings.Contains(contentType, "text/html") {
>         return
>     }
>
>     body, err := io.ReadAll(resp.Body)
>     if err != nil {
>         api.Logger().Error(
>             "se", "modifyResponse",
>             "msg", "failed to read response body",
>             "error", err.Error(),
>         )
>         return
>     }
>     resp.Body.Close()
>
>     // Define the HTML snippet to inject.
>     sloButton := `
> <div style="position: fixed; top: 16px; right: 16px; z-index: 99999;">
>   <a href="/single-logout" style="
>     display: inline-block;
>     padding: 10px 20px;
>     background-color: #6f42c1;
>     color: #fff;
>     border: none;
>     border-radius: 4px;
>     font-family: sans-serif;
>     font-size: 14px;
>     font-weight: bold;
>     text-decoration: none;
>     cursor: pointer;
>     box-shadow: 0 2px 4px rgba(0,0,0,0.2);
>   ">Single Logout</a>
> </div>`
>
>     // Insert the snippet just before the closing </body> tag so it
>     // appears on the rendered page.
>     modified := strings.Replace(string(body), "</body>", sloButton+"</body>", 1)
>
>     // Replace the response body and update the Content-Length header so
>     // downstream consumers receive the correct size.
>     resp.Body = io.NopCloser(bytes.NewReader([]byte(modified)))
>     resp.ContentLength = int64(len(modified))
>     resp.Header.Set("Content-Length", strconv.Itoa(len(modified)))
> }
> ```
>
> **Configuration**
>
> ```yaml maverics.yaml theme={null}
> singleLogout:
>   logoutURL: https://app.example.com/single-logout
>   postLogout:
>     redirectURL: https://app.example.com/
>
> apps:
>   - name: my-app
>     type: proxy
>     upstream: https://app.example.com
>     modifyResponseSE:
>       funcName: ModifyResponse
>       file: /etc/maverics/extensions/inject-slo-button.go
> ```

***

### `isLoggedInSE`

Check whether the user is already logged in to the upstream application. Return `true` if the upstream session is active, or `false` to trigger the login flow. Use this to inspect upstream cookies, session tokens, or other indicators of an active upstream session.

**Signature:**

```go theme={null}
func IsLoggedIn(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
```

**Config location:** `apps[].upstreamLogin.isLoggedInSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer for the current request |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:** `bool` -- `true` if the user is logged in to the upstream application, `false` otherwise.

***

### `loginSE`

Perform the login to the upstream application. Use this to submit credentials to the upstream login page, exchange tokens, set upstream cookies, or perform any steps required to establish an upstream session.

**Signature:**

```go theme={null}
func Login(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error
```

**Config location:** `apps[].upstreamLogin.loginSE`

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `api` | `orchestrator.Orchestrator` | Access to sessions, caches, secrets, logging, and other Orchestrator services |
| `rw` | `http.ResponseWriter` | The HTTP response writer -- use to set cookies or redirect |
| `req` | `*http.Request` | The incoming HTTP request |

**Returns:** `error` -- return `nil` on success, or an error if the upstream login fails.

## Related Pages

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

> **[Proxy Applications](/reference/orchestrator/applications/proxy)**
>
> Proxy application configuration and upstream settings
