Skip to main content
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. 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.
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:
func IsAuthenticated(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
Config location: apps[].policies[].authentication.isAuthenticatedSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer for the current request
req*http.RequestThe incoming HTTP request
Returns: booltrue if the user is authenticated, false otherwise. Examples:
See the full example under authenticateSE for a combined implementation that uses both isAuthenticatedSE and authenticateSE to let users choose which identity provider to authenticate with.
See the full example under 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:
func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request)
Config location: apps[].policies[].authentication.authenticateSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer — use to redirect or return an error response
req*http.RequestThe incoming HTTP request
Examples:
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.
idp-selector.go
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
}
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.
ldap-tls-auth.go
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
}

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:
func IsAuthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
Config location: apps[].policies[].authorization.isAuthorizedSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer for the current request
req*http.RequestThe incoming HTTP request
Returns: booltrue 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:
func HandleUnauthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request)
Config location: apps[].handleUnauthorizedSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer — use to write a custom error response or redirect
req*http.RequestThe 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:
func LoadAttrs(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error
Config location: apps[].loadAttrsSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer for the current request
req*http.RequestThe incoming HTTP request
Returns: error — return nil on success, or an error to indicate attribute loading failed. Examples:
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.
load-ldap-groups.go
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
}

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:
func CreateHeader(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) (http.Header, error)
Config location: apps[].headers[].createHeaderSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer for the current request
req*http.RequestThe 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:
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.
create-headers.go
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
}

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:
func ModifyRequest(api orchestrator.Orchestrator, req *http.Request)
Config location: apps[].modifyRequestSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
req*http.RequestThe outbound HTTP request to the upstream — modify this object directly
This hook runs on every proxied request. Keep it lightweight to avoid adding latency. Use api.Cache() to avoid repeating expensive lookups.

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:
func ModifyResponse(api orchestrator.Orchestrator, resp *http.Response)
Config location: apps[].modifyResponseSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
resp*http.ResponseThe HTTP response from the upstream — modify this object directly
This hook runs on every proxied response. Keep it lightweight to avoid adding latency. Use api.Cache() to avoid repeating expensive lookups.
Examples:
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.
inject-slo-button.go
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)))
}

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:
func IsLoggedIn(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) bool
Config location: apps[].upstreamLogin.isLoggedInSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer for the current request
req*http.RequestThe incoming HTTP request
Returns: booltrue 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:
func Login(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error
Config location: apps[].upstreamLogin.loginSE Parameters:
ParameterTypeDescription
apiorchestrator.OrchestratorAccess to sessions, caches, secrets, logging, and other Orchestrator services
rwhttp.ResponseWriterThe HTTP response writer — use to set cookies or redirect
req*http.RequestThe incoming HTTP request
Returns: error — return nil on success, or an error if the upstream login fails.