SAML applications

The Maverics Identity Orchestrator can be used as an IDP to protect SAML apps.

ℹ️
To define SAML type apps the 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 responses will be sent.

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.

ℹ️
Please note that SAML service providers should protect against the Open Redirect attack vector.

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.

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.

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.

ℹ️
Only a subset of data types can be used as attribute values. The following builtin data types are supported as attribute values: 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
    idpInitiatedLogin:
      loginURL: https://idp.enterprise.com/sso/example-app
      relayStateURL: https://app.enterprise.com/index.html
    duration: 300
    requestVerification:
      certificate: <appSigningCertificate>
    nameID:
      format: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
      attrMapping: azure.email
    authentication:
      idps:
        - azure
    attrProviders:
      - connector: ldap
        usernameMapping: azure.email
    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 With Service Extension

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
    authentication:
      isAuthenticatedSE:
        funcName: IsAuthenticated
        file: /etc/maverics/extensions/auth.go
      authenticateSE:
        funcName: Authenticate
        file: /etc/maverics/extensions/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 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)
}