Configure service extensions

Configure service extensions

Maverics can be extended using service extensions, which are custom, Golang code that support arbitrary functionality such as retrieving and constructing complex attributes or defining and evaluating policies with logic not pre-built into an Orchestrator. Service extensions give administrators the ability to customize the behavior of the Maverics Orchestrator to suit the particular needs of their integration.

Service extension selections are based on app type as follows:

SAML service extensions

  • Authentication: isAuthenticatedSE and authenticateSE service extensions are available to determine if a user is already authenticated and to control the authentication behavior.
  • Custom Claims: buildClaimsSE can customize which attributes will be added to the SAML 2.0 AttributeStatement.
  • Relay State: buildRelayStateSE can optionally be used to build the RelayState parameter in an IDP-initiated login flow. This extension can be used when the relay state is dynamic and therefore cannot be defined with a static relayStateURL.

OIDC service extensions

  • Authentication: isAuthenticatedSE and authenticateSE service extensions are available to determine if a user is already authenticated and to control the authentication behavior.
  • Access Token: accessToken defines the configuration for the OAuth access token.
  • ID Token: The buildIDTokenClaimsSE is an optional service extension that can customize how claims in the ID token are built. buildAccessTokenClaimsSE can customize how claims in the access token are built.

Proxy app service extensions

  • Authentication: isAuthenticatedSE and authenticateSE service extensions are available to determine if a user is already authenticated and to control the authentication behavior.
  • Upstream Login: upstreamLogin is an optional configuration used to determine if a request to an upstream application is authenticated and to be able to log in to an upstream app. Such situations are common when an app manages its own sessions or directly authenticates against a backing data store like LDAP or a relational database.
  • Load Attributes: loadAttrsSE is an optional service extension used to customize how the loading of attributes is done. This extension is often used to load attributes from proprietary data sources such as enterprise APIs.
  • Header Creation: createHeaderSE is an optional service extension used to create a custom HTTP header. This extension is often used when an attribute needs to be enriched or concatenated with additional data.
  • Authorization: isAuthorizedSE is an optional service extension that can be used to override the default behavior that determines if a user is authorized.
  • Handle Unauthorized: handleUnauthorizedSE is an optional service extension that can be used to override the default behavior when a policy evaluation denies access to the app.
  • Modify Request: modifyRequestSE is an optional service extension that can be used to modify every request that passes through the app.
  • Modify Response: modifyResponseSE is an optional service extension that can be used to modify every response that passes through the app.

Additional settings and configuration

After you’ve created a service extension, you can use the following fields to make additional selections on the service extension:

Service extension settings

  • Assets: allows you to upload additional files to reference in your service extension. Maverics also supports allowed protected packages.
  • Providers: allows you to select one or more providers to use in the service extension. These provider configurations will be used in the user flow when the service extension is invoked.
  • Claims: allows you to include one or more claims for use in the headers section. (Available for the Authentication and Load Attributes service extensions only).
  • Metadata: Metadata is an arbitrary set of key-value pairs that can be made available to a given extension. The values can be referenced from within the Go code, making service extensions more flexible and the configuration more obvious.

Use the service extension code editor to configure a custom service extension. Our code editor provides errors when attempting to compile malformed code.

For more information on configuring a custom service extension and code examples, see the service extension repository. Additionally, the on-premises Orchestrator documentation on service extensions can serve as a helpful reference.

Using service extensions in a user flow

Service extensions can be referenced in user flows in various ways. For instance, the Authentication service extension can be selected anywhere you are setting an authentication policy in a user flow or needing claims from a session for attributes, headers, or authorization policies.

Using a service extension

Using a service extension

Each user flow may have multiple places to reference different types of service extensions depending on the context.

Using a service extension

Deploying service extensions

When a service extension is updated, all user flows that reference the service extension will display an Uncommitted Changes status on the user flows page. To deploy your user flows with the latest updates, open an affected user flow, click Commit, then click Deploy.

The Deploy preview step displays a diff view on service extension and non-binary asset (txt, html, .js, etc) changes between deployments.

Deployment preview

If you have multiple user flows deployed that reference the same service extension to the same environment, all of the user flows will use the updated service extension.

If you deploy a previous version of a user flow with a service extension, it will roll back to a snapshot of the service extension from the time of the previous deploy.

Examples

For more information on configuring a custom service extension and code examples, see the service extension repository. Additionally, the on-premises Orchestrator documentation on service extensions can serve as a helpful reference.

Reference Assets within Service Extension

There are two ways to reference assets files uploaded with the service extension.

  • ReadFile returns the contents of the file as a []byte. The path of the file is relative to how it was uploaded. Currently, the Maverics UI only allows for files to be uploaded without specifying sub-directories.
  • FS returns a filesystem object that contains all the asset files uploaded with the related service extension. That is, it will not contain all the assets for all service extensions deployed to the orchestrator.

Read a single file from the uploaded assets

In the example below we are reading the contents of a single file that was uploaded as a service extension asset.

func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
    logger := api.Logger()
    logger.Debug("se", "authenticating user")

    if req.Method == http.MethodGet {
        // ServiceExtensionAssets exposes any assets that may have been bundled with the
        // service extension.
        assets := api.ServiceExtensionAssets()
        // ReadFile returns the file contents as a []byte.
        idpForm, err := assets.ReadFile("idpForm.html")
        if err != nil {
            logger.Error("se", "failed to read service extension asset file", "error", err.Error())
            http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        logger.Info("se", "received GET request, rendering IDP selector form")
        _, _ = rw.Write(idpForm)
        return
    }

    if req.Method != http.MethodPost {
        logger.Error("se", fmt.Sprintf("received unexpected request method '%s', expected POST", req.Method))
        http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
        return
    }

    logger.Debug("se", "parsing form from request")
    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' IDP 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)
}

Read all the uploaded assets as a filesystem

In the example below we are reading all the uploaded service extension assets as a filesystem.

func ServeWebAssets(api orchestrator.Orchestrator) error {
    router := api.Router()
    router.HandleFunc("/assets/idpform.html", func(rw http.ResponseWriter, req *http.Request) {
        logger := api.Logger()
        logger.Info("se", "serving idp picker form")

        session, 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
        }
        username, _ := session.GetString("idp.username")

        seAssets := api.ServiceExtensionAssets()
        // FS returns a filesystem of type (fs.FS) containing the assets associated
        // with the service extension.
        sefs, err := seAssets.FS()
        if err != nil {
            logger.Error("se", "failed to get service extension filesystem", "error", err.Error())
            http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        }
        tmpl, err := template.ParseFS(sefs, "idp-form.html")
        if err != nil {
            logger.Error("se", "unable to parse template", "error", err.Error())
            http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }

        data := struct {
            Title string
        }{
            Title: fmt.Sprintf("Pick your IDP %s", username),
        }
        err = tmpl.Execute(rw, data)
        if err != nil {
            logger.Error("se", "unable to execute template", "error", err.Error())
            http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
            return
        }
    })
    return nil
}