Applications SAML

L’orchestrateur d’identité Maverics peut être utilisé en tant que fournisseur d’identité pour protéger les applications SAML.

ℹ️
Pour définir les applications de type SAML, la valeur samlProvider doit être définie.

Options de configuration

Name

name est un identifiant unique pour l’application.

Type

type représente le type d’application. Lors de la définition des applications SAML, le type doit être saml.

Public

audience correspond à un identifiant unique de l’application. La valeur audience est généralement une URL, et elle doit correspondre au champ « Issuer » (Émetteur) fourni par le fournisseur de services SAML.

Consumer Service URL

consumerServiceURL correspond à l’URL vers laquelle les réponses d’authentification SAML sont envoyées.

Logout Service URL

logoutServiceURL est un champ facultatif qui définit l’URL vers laquelle les réponses de déconnexion SAML
sont envoyées. Ce champ doit être défini si la fonctionnalité de déconnexion est requise.

IDP Initiated Login

La clé idpInitiatedLogin est une configuration facultative qui, lorsqu’elle est spécifiée, permet au fournisseur d’authentification d’effectuer la connexion initiée par le fournisseur d’identité au client.

Login URL

loginURL correspond au terminal consulté par l’utilisateur à partir de son navigateur pour initier le flux de connexion au fournisseur d’identité. Il doit s’agir de l’unique terminal sur l’orchestrateur.

Relay State URL

relayStateURL correspond au terminal transmis au fournisseur de services qui servira de page d’accueil à l’utilisateur à l’issue du flux d’authentification.

Extension de services Build Relay State

buildRelayStateSE peut être utilisé pour construire le paramètre RelayState dans un flux de connexion initié par le fournisseur d’identité. Cette extension peut être utilisée lorsque l’état du relais est dynamique et ne peut donc pas être défini avec un relayStateURL statique.

ℹ️
Veuillez noter que les fournisseurs de services SAML doivent se protéger contre le vecteur d’attaque Open Redirect.

Durée

duration correspond à la durée en secondes pendant laquelle les assertions SAML sont valides. Si aucune valeur n’est définie, une durée de cinq minutes (300 secondes) sera appliquée.

ID du nom

nameID est un champ facultatif utilisé pour définir les propriétés de l’élément NameID dans une réponse SAML.

Format

format est un champ facultatif utilisé pour définir le format NameID qui sera utilisé dans l’assertion SAML. Lorsqu’elle n’est pas définie, une valeur de 'urn:oasis:names:tc:SAML:1.0:nameid-format:unspecified' sera appliquée. Si une règle NameIDPolicy est définie dans la requête d’authentification SAML, elle doit correspondre au format NameID défini sur le client. Pour plus de détails sur le format NameID, veuillez consulter les sections 2.2, 3.4.1 et 8.3 du document SAML spec.

Mappage des attributs

attrMapping définit la valeur NameID dans la réponse SAML. Si la valeur n’est pas définie, c’est le sujet de l’utilisateur qui sera utilisé. Pour charger un attribut du cache de session, utilisez la syntaxe connectorName.attributeValue.

Requête de vérification

requestVerification définit la clé publique utilisée pour valider la signature des requêtes entrantes. Il est également possible de l’utiliser pour désactiver la vérification de la signature des requêtes.

Certificat

certificate correspond au certificat RSA x509, qui sera utilisé pour vérifier les signatures des requêtes entrantes. Cette valeur peut être définie en ligne ou à l’aide d’un fournisseur de secrets. Elle doit être compatible avec le système RSA. Actuellement, ce fournisseur d’authentification ne prend en charge que l’algorithme SHA-256 pour la signature et le prétraitement des requêtes.

Ignorer la vérification

skipVerification est une valeur booléenne utilisée lorsque le client ne souhaite pas valider les signatures des requêtes entrantes.

Authentification

authentication définit la manière dont les utilisateurs sont authentifiés pour cette application.

IDPs

idps répertorie les fournisseurs d’identité utilisés pour authentifier l’utilisateur.

Extension de services IsAuthenticated

isAuthenticatedSE est une extension de services optionnelle qui peut être utilisée pour modifier le comportement par défaut qui détermine si un utilisateur est déjà authentifié. Cette extension doit être utilisée avec authenticateSE.

Extension de services Authenticate

authenticateSE est une extension de services optionnelle utilisée pour contrôler la manière dont l’authentification est effectuée. Cette extension doit être utilisée avec isAuthenticatedSE.

Authorization

authorization est une configuration optionnelle qui définit les règles de contrôle d’accès qui sont nécessaires pour accéder à l’application.

⚠️
Si la configuration authorization est omise, la valeur par défaut est allowAll: true. Tous les utilisateurs authentifiés seront autorisés à accéder à l’application.

Allow All

allowAll permet un accès libre à une application donnée. Cette option est généralement utilisée lorsque l’autorisation à granularité fine n’est pas appliquée.

Règles

rules définit une liste de conditions de contrôle d’accès. Toutes les règles doivent être évaluées avec une valeur « true » pour permettre à l’utilisateur d’accéder à une application donnée.

Extension de services IsAuthorized

isAuthorizedSE est une extension de services optionnelle qui peut être utilisée pour remplacer le comportement par défaut qui détermine si un utilisateur est autorisé.

And

and définit une liste de conditions qui doivent toutes être remplies.

Or

or définit une liste de conditions dont au moins une doit être remplie.

Opérateurs

Les opérateurs définis ci-dessous peuvent être répertoriés sous une règle or ou and. Tous les opérateurs attendent exactement deux valeurs (opérandes).

Pour charger un attribut du cache de session qui sera exploité dans la politque, utilisez la syntaxe {{ connectorName.attributeName }}. Par exemple, {{ ldap.memberOf }} pourrait être utilisé pour rédiger une politique basée sur l’appartenance à un groupe LDAP.

En outre, une méthode de requête HTTP peut être prise en compte lors de l’évaluation de la politique en utilisant la syntaxe {{ http.request.method }}. La méthode de requête HTTP sera renvoyée sous la forme d’une chaîne en majuscules, par exemple, POST. Veuillez noter que seul l’attribut de méthode de requête HTTP peut être utilisé dans la politique, la prise en charge d’autres attributs HTTP sera introduite progressivement.

equals s’évalue avec une valeur « true » si les deux valeurs sont équivalentes.

notEquals s’évalue avec une valeur « true » si les deux valeurs ne sont pas équivalentes.

contains s’évalue avec une valeur « true » si l’opérande de gauche (la chaîne complète) contient l’opérande de droite (une sous-chaîne).

notContains s’évalue avec une valeur « true » si l’opérande de gauche (la chaîne complète) ne contient pas l’opérande de droite (une sous-chaîne).

Attribute Providers

attrProviders constituent une configuration facultative pour un système d’identité ou un magasin de données à partir duquel le SAMLProvider peut récupérer des attributs supplémentaires utilisés dans claimsMapping.

Connecteur

Connector est une référence au nom du connecteur défini qui sera utilisé en tant que fournisseur d’attributs.

usernameMapping

La configuration usernameMapping permet de s’assurer que le fournisseur d’attributs (c’est-à-dire le connecteur LDAP) dispose de l’attribut correct nécessaire à l’interrogation des attributs de l’utilisateur. Elle spécifie l’attribut utilisé pour rechercher les attributs utilisateur à partir du fournisseur d’attributs défini.

Chiffrement

encryption est une configuration facultative utilisée pour crypter l’assertion SAML.

Méthode de chiffrement de clé

keyEncryptMethod permet de configurer la méthode de chiffrement de la clé symétrique utilisée pour chiffrer l’assertion. Deux valeurs sont actuellement prises en charge : RSA_OAEP et RSA-1_5.

Conformément aux spécifications de cryptage XML, la valeur RSA-1_5 n’est pas recommandée en raison des risques de sécurité associés à l’algorithme.

Méthode de chiffrement des données

dataEncryptMethod permet de configurer la méthode de chiffrement des données réelles de l’assertion. Les valeurs valides sont AES128CBC , AES192CBC et AES256CBC.

Digest Method

digestMethod correspond à l’algorithme de prétraitement des messages utilisé pour le calcul dudit prétraitement dans le cadre du processus de cryptage. Les valeurs valides sont SHA256et SHA512.

Certificat

certificate correspond à la chaîne encodée au format PEM qui peut être définie en ligne ou par l’intermédiaire d’un fournisseur de secrets. Ce certificat est généralement récupéré auprès du fournisseur de services.

Mappage des réclamations

claimsMapping définit comment mapper les attributs de la session à une assertion SAML.

Extension de services Build Claims

buildClaimsSE est une extension de services optionnelle qui permet de personnaliser les attributs qui seront ajoutés à l’élément AttributeStatement de SAML 2.0.

ℹ️
Seul un sous-ensemble de types de données peut être utilisé en tant que valeurs d’attribut. Les types de données intégrés suivants sont pris en charge en tant que valeurs d’attribut : bool, int, int8, int32, int64, float32, float64 et string.

Exemples

Exemple de configuration de base de l’application SAML

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: <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

Exemple de configuration de l’application SAML avec règles d’autorisation

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"]

Application SAML avec extensions de services

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
    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 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)
}