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

## 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:**

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

***

### `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:**

<AccordionGroup>
  <Accordion title="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.

    <Tabs>
      <Tab title="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
        }
        ```
      </Tab>

      <Tab title="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>`
        ```
      </Tab>

      <Tab title="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
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="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.

    <Tabs>
      <Tab title="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
        }
        ```
      </Tab>

      <Tab title="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)
        }
        ```
      </Tab>

      <Tab title="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
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

***

### `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:**

<AccordionGroup>
  <Accordion title="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.

    <Tabs>
      <Tab title="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
        }
        ```
      </Tab>

      <Tab title="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
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

***

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

**Examples:**

<AccordionGroup>
  <Accordion title="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.

    <Tabs>
      <Tab title="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
        }
        ```
      </Tab>

      <Tab title="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
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

***

### `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.
</Note>

***

### `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.
</Note>

**Examples:**

<AccordionGroup>
  <Accordion title="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.

    <Tabs>
      <Tab title="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)))
        }
        ```
      </Tab>

      <Tab title="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
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

***

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

<CardGroup cols={2}>
  <Card title="Service Extensions Overview" icon="code" href="/reference/orchestrator/service-extensions">
    Configuration, SDK reference, and best practices
  </Card>

  <Card title="Proxy Applications" icon="server" href="/reference/orchestrator/applications/proxy">
    Proxy application configuration and upstream settings
  </Card>
</CardGroup>
