SAML applications
The Maverics Identity Orchestrator can be used as an IDP to protect SAML apps.
samlProvider
must be defined.Configuration options
Name
name
is a unique identifier for the app.
Type
type
represents the application type. When defining SAML apps, the type
should be saml
.
Audience
audience
is a unique identifier of the application. The audience
value is typically a URL, and it should match the Issuer field provided by the SAML service provider.
Consumer Service URL
consumerServiceURL
is the URL where SAML authentication responses will be sent.
Logout Service URL
logoutServiceURL
is an optional field that defines the URL where SAML logout
responses will be sent. This field must be defined if requiring logout functionality.
IDP Initiated Login
The idpInitiatedLogin
key is an optional configuration which when specified will
enable the auth provider to perform IDP initiated login to the client.
Login URL
loginURL
is the endpoint that the user will visit from their browser to initiate
the IDP login flow. This endpoint needs to be unique on the Orchestrator.
Relay State URL
relayStateURL
is the endpoint that gets passed to the service provider and is
intended to be the landing page for the user after the authentication flow is complete.
Build Relay State Service Extension
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
.
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.
Duration
duration
is the time in seconds for which the SAML assertions are valid. If a value
is not defined, a duration of five minutes (300 seconds) will be used.
Name ID
nameID
is an optional field used to define the properties of the NameID
element
in a SAML response.
Format
format
is an optional field used to define the NameID Format that will be used
in the SAML assertion. When not defined, a value of
'urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified'
will be used. If a
NameIDPolicy is defined on the SAML Authentication request, it must match the
NameID Format defined on the client. For more details on NameID Format, please see
section 2.2, 3.4.1, and 8.3 of the SAML spec.
Attribute Mapping
attrMapping
sets the NameID
value in the SAML response. When not defined, the user’s
subject will be used. In order to load an attribute from the session cache, use
the connectorName.attributeValue
syntax.
Request Verification
requestVerification
defines the public key used to validate the signature of incoming
requests. Alternatively, it can be used to disable request signing verification.
Certificate
certificate
is the RSA x509 certificate, and will be used to verify the signatures of incoming requests. It may be defined inline or with a secret provider. It must be RSA compatible. Currently this auth provider only supports SHA-256 for request signing and digest algorithm.
Skip Verification
skipVerification
is a boolean value and is used when the client does not want to validate the signatures of incoming requests.
Authentication
authentication
defines how users are authenticated for this app.
IDPs
idps
lists the IDPs which will be used to authenticate the user.
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
is an optional config which defines access control rules that
are required to access the app.
authorization
is omitted, it defaults to
allowAll: true
. All authenticated users will be authorized to access the app.Allow All
allowAll
enables open access to a given app. 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 app.
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.
Attribute Providers
The attrProviders
is an optional configuration for an identity system or
data store from which the SAMLProvider may retrieve additional attributes used
in claimsMapping
.
Connector
Connector is a reference to the name of the defined connector which will be used as an attribute provider.
usernameMapping
The usernameMapping
configuration makes sure the Attribute Provider (i.e. LDAP Connector) has the correct attribute it needs to successfully query for the user’s attributes.
It specifies the attribute used to look up user attributes from the defined attribute provider.
Signature
signature
defines the certificate and key used when signing SAML responses. By default,
both the SAML response element and SAML assertion element will be signed.
This field is optional when defined on a SAML app. When the signature
properties are not
defined on the app, the certificates attached to the SAML Provider
will be used. It may be advantageous to define unique signing certificates per SAML app
to help manage the testing overhead of certificate rotations.
To retrieve app-specific metadata, add the appID
query param when making a metadata
request. For example,
https://idp.enterprise.com/idp/saml/metadata.xml?appID=0cb87d2a-76af-4450-8d7c-455b45eec312
where
0cb87d2a-76af-4450-8d7c-455b45eec312
represents the app’s ID (name).
Certificate
certificate
the x509 certificate used by SAML service providers to validate the
signature of SAML response and assertions.
Private Key
privateKey
is the RSA256 private key used to sign SAML assertions.
Disable Signed Response
disableSignedResponse
a boolean value to disable the signing of the SAML response
element.
Disable Signed Assertion
disableSignedAssertion
a boolean value to disable the signing of the SAML assertion
element.
Encryption
encryption
is an optional configuration used for encrypting the SAML assertion.
Key Encrypt Method
keyEncryptMethod
configures the encryption method for encrypting the symmetric key
which is used to encrypt the assertion. Currently, we support two values here which
are RSA_OAEP
and RSA-1_5.
RSA-1_5
is not recommended according to the
XML encryption spec due to
security risks associated with the algorithm.
Data Encrypt Method
dataEncryptMethod
configures the encryption method for encrypting the actual data
of the assertion. Valid values are AES128CBC
, AES192CBC
, and AES256CBC
.
Digest Method
digestMethod
is the message digest algorithm use to compute a message digest as
part of the encryption process. Valid values are SHA256
, and SHA512
.
Certificate
certificate
is the PEM encoded string that can be defined inline or via a
secret provider. This certificate is typically retrieved from the Service Provider.
Claims Mapping
claimsMapping
defines how to map attributes from the session to a SAML assertion.
Build Claims Service Extensions
buildClaimsSE
is an optional Service Extension that can customize which attributes
will be added to the SAML 2.0 AttributeStatement.
bool
, int
, int8
, int32
, int64
, float32
, float64
, and string
.Examples
Basic SAML App Config Example
apps:
- name: exampleSAMLApp
type: saml
audience: https://app.enterprise.com
consumerServiceURL: https://app.enterprise.com/acs
logoutServiceURL: https://app.enterprise.com/logout
idpInitiatedLogin:
loginURL: https://idp.enterprise.com/sso/example-app
relayStateURL: https://app.enterprise.com/index.html
duration: 300
requestVerification:
certificate: <exampleSPRequestVerificationCert>
nameID:
format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
attrMapping: azure.email
authentication:
idps:
- azure
attrProviders:
- connector: ldap
usernameMapping: azure.email
signature:
certificate: <exampleSPSigningCert>
privateKey: <exampleSPSigningKey>
encryption:
keyEncryptMethod: RSA_OAEP
dataEncryptMethod: AES256CBC
digestMethod: SHA256
certificate: <encryptionCert>
claimsMapping:
id: azure.name
email: azure.email
lastName: azure.surname
groups: ldap.groupMembership
SAML App Config Example With Authorization Rules
apps:
- name: exampleSAMLApp
type: saml
audience: https://app.enterprise.com
consumerServiceURL: https://app.enterprise.com/acs
requestVerification:
certificate: <appSigningCertificate>
authentication:
idps:
- azure
attrProviders:
- connector: ldap
usernameMapping: azure.email
authorization:
rules:
- and:
- contains: [ "{{ ldap.groups }}", "admin" ]
- contains: ["{{ azure.emailaddress }}", "@example.com"]
SAML App With Service Extensions
apps:
- name: exampleSAMLApp
type: saml
audience: https://app.enterprise.com
consumerServiceURL: https://app.enterprise.com/acs
duration: 300
requestVerification:
certificate: <appSigningCertificate>
idpInitiatedLogin:
loginURL: https://idp.enterprise.com/sso/example-app
buildRelayStateSE:
funcName: BuildRelayState
file: /etc/maverics/extensions/auth.go
loadAttrsSE:
funcName: LoadAttributes
file: /etc/maverics/extensions/auth.go
authentication:
isAuthenticatedSE:
funcName: IsAuthenticated
file: /etc/maverics/extensions/auth.go
authenticateSE:
funcName: Authenticate
file: /etc/maverics/extensions/auth.go
authorization:
isAuthorizedSE:
funcName: IsAuthorized
file: /etc/maverics/auth.go
attrProviders:
- connector: ldap
usernameMapping: azure.email
buildClaimsSE:
funcName: BuildClaims
file: /etc/maverics/extensions/auth.go
/etc/maverics/extensions/auth.go
package main
import (
"fmt"
"net/http"
"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 attribute 'azure.authenticated'", "error", err)
return false
}
if isAzureAuth == "true" {
return true
}
return false
}
func Authenticate(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) {
logger := api.Logger()
logger.Info("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 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
}
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
}
func BuildClaims(api orchestrator.Orchestrator, _ http.ResponseWriter, _ *http.Request) (map[string]any, error) {
logger := api.Logger()
logger.Info("se", "building custom claims")
session, err := api.Session()
if err != nil {
logger.Error("se", "unable to retrieve session", "error", err.Error())
return nil, err
}
name, _ := session.GetString("azure.email")
firstName, _ := session.GetString("azure.givenname")
lastName, _ := session.GetString("azure.surname")
return map[string]any{
"name": name,
"firstName": firstName,
"lastName": lastName,
}, nil
}
func BuildRelayState(api orchestrator.Orchestrator, rw http.ResponseWriter, req *http.Request) string {
const fallbackRelayState = "https://app.enterprise.com/index.html"
logger := api.Logger()
logger.Info("se", "building custom relay state")
session, err := api.Session()
if err != nil {
logger.Error(
"se", "failed to build RelayState: failed to retrieve session",
"error", err.Error(),
)
return fallbackRelayState
}
userID, err := session.GetString("azure.objectidentifier")
if err != nil {
logger.Error(
"se", "failed to build RelayState: failed to retrieve value from session",
"error", err.Error(),
)
return fallbackRelayState
}
if userID == "" {
logger.Error(
"se", "failed to build RelayState: user ID value not found session",
)
return fallbackRelayState
}
return fmt.Sprintf("https://app.enterprise.com/user/%s", userID)
}