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
.
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.
^
or $
) where possible, and counters or ranges (.{0,15}
) instead of “greedy”
wildcards (.*
).location
can be loaded from a secret provider.
See the example.
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
.
Decision
decision
is an optional configuration that defines when authorization policies are
re-evaluated. This configuration should only be paired with the IsAuthorized
service extension.
Lifetime
lifetime
is an optional field that defines the duration
of decision from the policy evaluation.
By default, the decision duration is 0, which defaults to the max session lifetime. If just an integer is provided, it is interpreted as seconds. For example, 30
is 30 seconds.
- If
lifetime
is unset, the decision will be cached for the lifetime of the session. SeemaxLifetimeSeconds
in the session configuration for more information. - If
lifetime
is negative, the decision will not be cached, every request will be evaluated. - If
lifetime
is positive, the decision will be cached for the specified duration or until the session ends.
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.
Load Attributes 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: /
decision:
lifetime: 1m
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
- location: <my-secret-location>
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: /
decision:
lifetime: -1s
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,
)
}