Proxy applications

The Maverics Identity Orchestrator can be used as an HTTP proxy to protect apps. This can be useful for apps that consume identity via HTTP headers, but can also be useful for apps that natively authenticate users via other mechanisms.

Configuration options

Name

name is a unique identifier for the app.

Type

type represents the application type. When defining proxy apps, the type should be proxy.

Route Patterns

routePatterns are the list of patterns that will be used to map a request to the appropriate app. The Orchestrator will match a given request to the most specific pattern. Pattern matching is case-insensitive.

A pattern can be a rooted path like /index.html or a rooted subtree like /dashboard/ (note the trailing slash). Since a pattern ending in a slash names a rooted subtree, the pattern / matches all paths not matched by other registered patterns, not just the URL with a path of /.

Patterns may optionally begin with a host name, restricting matches to URLs on that host only. Host-specific patterns take precedence over general patterns. For example, a pattern of app.example.com would match all requests that contain the specified host such as https://app.example.com/dashboard.

A pattern can be further specified by including both a host name and a path. For instance, a pattern of example.com/app/ would only match requests that contain both the specified host and specified path such as https://example.com/app/reports.

ℹ️
If you are seeing an HTTP 404 (not found) response that did not come from an upstream app, it is likely the request did not match any of the registered patterns.

Upstream

upstream is the URL of the application that is being proxied to.

Preserve Host

preserveHost is a flag used to determine if the ‘Host’ header should be preserved on outbound requests. By default, the Orchestrator will set the host header to match the upstream’s host.

Transport Layer Security

tls defines an option TLS configuration for outbound requests to the upstream app. The value must reference a TLS object defined in the top-level TLS configuration. This field is typically used when the upstream target uses certificates signed by an untrusted certificate authority.

Unauthorized Page

unauthorizedPage is the URL a user will be redirected to when a policy evaluation denies access to the app.

Attribute Providers

attrProviders defines a list of identity systems or data stores from which attributes required for evaluating policies and building headers are loaded from.

Attribute providers only need to be defined when attributes should to be loaded post-authentication. Many IDPs will return a complete set of attributes as part of the authentication process via the OIDC ID token or SAML response.

name

name references the name of the provider that will be used to load attributes.

Username Mapping

usernameMapping defines the attribute to use as a lookup key in order to load attributes from the attribute provider. The value usually comes from the IDP that was used for primary authentication, for example, {{ azure.mail }}. In order to load an attribute from the session cache, use the {{ namespace.value }} syntax.

Headers

headers are the list of HTTP headers that will be added to requests that are made to upstream applications. Headers will only be added to the request if the location is protected.

Name

name is the name of the HTTP header.

Value

value is the value of the HTTP header. The value can either be a string or a dynamic attribute loaded from the session. In order to load an attribute from the session cache, use the {{ namespace.value }} syntax.

CreateHeader service extension

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.

Policies

policies defines a list of conditions that determine whether a given request should be allowed, ultimately defining which users are allowed application access. If no policies are defined, all requests will be denied.

Location

location is a URL path used to map application resources to a policy. A request will be matched to the most specific location with regular expressions (regex) locations taking priority. The ordering in configuration of non-regex policy locations does not matter, but locations that use a regex maintain the order that they are defined.

Simple policy location matching is case-insensitive. If you define a location of /EXAMPLE it will match requests with a path of /example or /Example. If you want the policy match to be case-sensitive, use a regex.

To apply regex matching to a location, add ~ before the pattern (note the trailing space). For example:

- location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$

You can use tools such as regex101 (choose Golang) to test your regex against URL paths you would like to match.

⚠️
Poorly constructed regexes can impact orchestrator performance. Use anchors (^ or $) where possible, and counters or ranges (.{0,15}) instead of “greedy” wildcards (.*).

Use Query Params Matching

useQueryParamsMatching is an optional configuration used to match requests with query parameters to the correct policy. Query parameter matching only works with locations that use regex matching (i.e. preceded with a ~).

In order to match URLs that include query parameters such as the URL /dashboard?sort=asc, you would define a location similar to location: ~ ^/dashboard\?sort=asc.

Authentication

authentication defines the authentication policy for the location.

Allow Unauthenticated

allowUnauthenticated can be used to allow open access to a resource. If set to true, the resource for this policy does not require any authentication. This operator is typically used for resources like error or login pages.

IDPs

idps take a list of one or more IDP connector names to use for authentication.

IsAuthenticated service extension

isAuthenticatedSE is an optional service extension that can be used to override the default behavior that determines if a user is already authenticated. This extension must be used with authenticateSE.

Authenticate service extension

authenticateSE is an optional service extension used to take control of how authentication will be done. This extension must be used with isAuthenticatedSE.

Authorization

authorization defines the access control rules that are required to access a set of locations.

Allow All

allowAll enables open access to a given location. This option is usually used when fine-grained authorization is not being enforced.

Rules

rules define a list of access control conditions. All rules must evaluate to true in order user to get access to a given location.

And

and defines a list of conditions that must all be true.

Or

or defines a list of conditions where at least one condition must be true.

Operators

The operators defined below can be listed under an or or and rule. All operators expect exactly two values (operands).

In order to load an attribute from the session cache that will be leveraged in policy, use the {{ connectorName.attributeName }} syntax. For example, {{ ldap.memberOf }} could be used to write a policy that was predicated on LDAP group membership.

Additionally, an HTTP request method can be considered during policy evaluation by using the {{ http.request.method }} syntax. The HTTP request method will be returned as an uppercase string, for example, POST. Please note that only the HTTP request method attribute can be used in policy, but support for other HTTP attributes will be added over time.

equals evaluates to true if the two values are equivalent.

notEquals evaluates to true if the two values are not equivalent.

contains evaluates to true if the left-hand operand (the complete string) contains the right-hand operand (a substring).

notContains evaluates to true if the left-hand operand (the complete string) does not contain the right-hand operand (a substring).

IsAuthorized Service Extension

isAuthorizedSE is an optional service extension that can be used to override the default behavior that determines if a user is authorized.

Headers

headers define the HTTP headers for a given policy location. The headers defined at the policy-level will override headers of the same name that are defined at the app-level.

Logout

logout defines the optional logout settings for the application. When an application logout occurs, a user will be logged out of all the IDPs defined in policy for the app that the user is authenticated with. Additionally, all session data that is cached related to the app will be deleted.

The app-specific logout endpoint has some important differences from the single logout (SLO) endpoint. Specifically, the SLO endpoint logs the user out of all authenticated IDPs whereas the app-specific logout will only log a user out of IDPs defined in the policies for the given app.

If an IDP is shared across multiple apps, the user will be logged out of the shared IDP. However, Maverics will still respect the session for the apps that weren’t logged out of. That is, the user would not have to reauthenticate against apps that weren’t logged out of explicitly.

Logout URL

logoutURL is the endpoint that will be exposed to facilitate a logout for application users.

Post Logout Redirect URL

postLogoutRedirectURL is the address where a user will be redirected after a successful logout.

LoadAttributes Service Extension

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.

ModifyRequest Service Extension

modifyRequestSE is an optional service extension that can be used to modify every request that passes through the app.

ModifyResponse Service Extension

modifyResponseSE is an optional service extension that can be used to modify every response that passes through the app.

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.

IsLoggedIn Service Extension

isLoggedInSE is used to determine if an upstream application has been logged into.

Login Service Extension

loginSE is used to log in to an upstream application.

Handle Unauthorized Service Extension

handleUnauthorizedSE is an optional service extension that can be used to override the default behavior when a policy evaluation denies access to the app.

Example

Standard Configuration

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
      - www.example.com/app/
    upstream: https://app-internal.example.com
    tls: exampleAppTLS
    unauthorizedPage: https://app.example.com/unauthorized
    attrProviders:
      - name: ldap
        usernameMapping: "{{ azure.emailaddress }}"
    headers:
      - name: SM_USER
        value: "{{ azure.emailaddress }}"
      - name: groups
        value: "{{ ldap.memberOf }}"
      - name: Organization
        value: Example Inc.
    policies:
      - location: /
        authentication:
          allowUnauthenticated: false
          idps:
            - azure
        authorization:
          allowAll: false
          rules:
            - and:
                - equals: ["{{ ldap.department }}", "Engineering"]
                - notEquals: ["{{ ldap.title }}", "Junior Software Engineer"]
                - contains: ["{{ ldap.memberOf }}", "cn=admins,ou=groups,ou=example,ou=com"]
            - or:
                - contains: ["{{ azure.emailaddress }}", "@example.com"]
                - contains: ["{{ azure.emailaddress }}", "@example.io"]
            - or:
                - equals: ["{{ http.request.method }}", "GET"]
                - equals: ["{{ http.request.method }}", "OPTIONS"]
      - location: /index.html
        authentication:
          allowUnauthenticated: false
          idps:
            - azure
        authorization:
          allowAll: true
        headers:
          - name: firstName
            value: "{{ ldap.givenname }}"
          - name: lastName
            value: "{{ ldap.sn }}"
      - location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true
    logout:
      logoutURL: app.example.com/logout
      postLogoutRedirectURL: /login

Auth Service Extensions

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
    upstream: https://app-internal.example.com
    policies:
      - location: /
        authentication:
          isAuthenticatedSE:
            funcName: IsAuthenticated
            file: /etc/maverics/auth.go
          authenticateSE:
            funcName: Authenticate
            file: /etc/maverics/auth.go
        authorization:
          isAuthorizedSE:
            funcName: IsAuthorized
            file: /etc/maverics/auth.go

/etc/maverics/auth.go

package main

import (
	"net/http"
	"strings"

	"github.com/strata-io/service-extension/orchestrator"
)

func IsAuthenticated(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user is authenticated")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return false
	}

	isAzureAuth, err := session.GetString("azure.authenticated")
	if err != nil {
		logger.Error("se", "unable to retrieve session value 'azure.authenticated'", "error", err.Error())
		return false
	}
	if isAzureAuth == "true" {
		return true
	}

	return false
}

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

	azureIDP, err := api.IdentityProvider("azure")
	if err != nil {
		logger.Error(
			"se", "failed to retrieve Azure IDP",
			"error", err.Error(),
		)
		http.Error(
			rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError,
		)
		return
	}
	azureIDP.Login(rw, req)
}

func IsAuthorized(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user is authorized")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return false
	}

	rawGroups, _ := session.GetString("azure.groups")
	groups := strings.Split(rawGroups, ",")
	for _, group := range groups {
		if group == "executives" {
			return true
        }
    }

	return false
}

Header Creation Service Extension

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      - app.example.com
    upstream: https://app-internal.example.com
    headers:
      - name: firstName
        value: "{{ azure.givenname }}"
      - name: lastName
        value: "{{ azure.surname }}"
      - createHeaderSE:
          funcName: CreateHeader
          file: /etc/maverics/header.go
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true

/etc/maverics/header.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func CreateHeader(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) (http.Header, error) {
	logger := api.Logger()
	logger.Debug("se", "building custom header")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return nil, err
	}

	firstName, err := session.GetString("azure.givenname")
	if err != nil {
        return nil, fmt.Errorf("unable to retrieve attribute 'azure.givenname': %w", err)
	}
	surname, err := session.GetString("azure.surname")
	if err != nil {
		return nil, fmt.Errorf("unable to retrieve attribute 'azure.givenname': %w", err)
	}
	preferredName := fmt.Sprintf(`%s 'The Great' %s`, firstName, surname)
	
	return http.Header{"preferredName": []string{preferredName}}, nil
}

Attribute Loading Service Extension

This extension is used to load information about the authenticated user. Typically, the attribute information will be stored on the session, allowing it to be used for making policy decisions or for it to be used in headers passed to the protected resource.

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    loadAttrsSE:
      funcName: LoadAttributes
      file: /etc/maverics/auth.go
    headers:
      - name: SM_USER
        value: "{{ azure.mail }}"
      - name: firstName
        value: "{{ ldap.givenname }}"
      - name: lastName
        value: "{{ ldap.sn }}"
      - name: mobile
        value: "{{ ldap.mobile }}"
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true

/etc/maverics/auth.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func LoadAttributes(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) error {
	logger := api.Logger()
	logger.Debug("se", "loading custom attributes from LDAP")
	
	session, err := api.Session()
	if err != nil {
		logger.Error("se", "unable to retrieve session", "error", err.Error())
		return err
	}

	mail, err := session.GetString("azure.email")
	if err != nil {
		return fmt.Errorf("failed to find user email required for LDAP query: %w", err)
	}

	ldap, err := api.AttributeProvider("ldap")
	if err != nil {
		return fmt.Errorf("failed to find LDAP attribute provider")
	}

	attrs, err := ldap.Query(mail, []string{"givenname", "sn", "mobile"})
	if err != nil {
		return fmt.Errorf("failed to query LDAP: %w", err)
	}

	for k, v := range attrs {
		logger.Debug(
			"se", "setting LDAP attribute on session",
			"attribute", k,
			"value", v,
		)
		_ = session.SetString(k, v)
	}

	err = session.Save()
	if err != nil {
		return fmt.Errorf("unable to save session state: %w", err)
	}
	return nil
}

Request and Response Modification Service Extensions

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    modifyRequestSE:
      funcName: ModifyRequest
      file: /etc/maverics/proxy.go
    modifyResponseSE:
      funcName: ModifyResponse
      file: /etc/maverics/proxy.go
    policies:
      - location: /
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true

/etc/maverics/proxy.go

package main

import (
	"net/http"
	"time"

	"github.com/strata-io/service-extension/orchestrator"
)

func ModifyRequest(api orchestrator.Orchestrator, req *http.Request) {
	api.Logger().Debug("se", "setting header on request")
	req.Header.Set("X-Req-Time", time.Now().String())
}

func ModifyResponse(api orchestrator.Orchestrator, resp *http.Response) {
	if resp.Request.URL.Path != "/special/path/" {
		return
	}

	api.Logger().Debug("se", "setting status code for '/special/path/' resource")
	resp.StatusCode = http.StatusOK
}

Upstream Application Login Service Extensions

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    upstreamLogin:
      isLoggedInSE:
        funcName: IsLoggedIn
        file: /etc/maverics/login.go
      loginSE:
        funcName: Login
        file: /etc/maverics/login.go
    policies:
      - location: /
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true
      - location: ~ \.(jpg|png|ico|svg|ttf|js|css|gif)$
        authentication:
          allowUnauthenticated: true
        authorization:
          allowAll: true

/etc/maverics/login.go

package main

import (
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func IsLoggedIn(api orchestrator.Orchestrator, _ http.ResponseWriter, req *http.Request) bool {
	logger := api.Logger()
	logger.Debug("se", "determining if user has session with upstream app")

	if req.URL.Path == "/login" {
		return true
	}
	
	_, err := req.Cookie(".ASPXAUTH")
	if err != nil {
		logger.Debug("se", "user is not authenticated with upstream app")
		return false
	}

	logger.Debug("se", "user is authenticated with upstream app")
	return true
}

func Login(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) error {
	api.Logger().Info("se", "redirecting user to upstream app's login page")
	
	http.Redirect(rw, req, "/login", http.StatusFound)
	return nil
}

Handle Unauthorized Service Extension

apps:
  - name: exampleProxyApp
    type: proxy
    routePatterns:
      -  /
    upstream: https://example.com
    handleUnauthorizedSE:
      funcName: HandleUnauthorized
      file: /etc/maverics/unauthorized.go
    policies:
      - location: /
        authentication:
          idps:
            - azure
        authorization:
          allowAll: true
      - location: /reports
        authentication:
          idps:
            - azure
        authorization:
          rules:
            - or:
                - equals: ["{{ azure.title }}", "CEO"]
                - equals: ["{{ azure.title }}", "CFO"]
                - equals: ["{{ azure.title }}", "COO"]

/etc/maverics/unauthorized.go

package main

import (
	"fmt"
	"net/http"

	"github.com/strata-io/service-extension/orchestrator"
)

func HandleUnauthorized(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
    api.Logger().Debug("se", "handling unauthorized request")

    http.Error(
        rw,
        fmt.Sprintf(
            "Access denied to %s. Please contact [email protected] for help.",
            req.URL.Path,
        ),
        http.StatusUnauthorized,
    )
}