Authentification SAML2
Cette page reprend la configuration SAML2 d'une application Lagoon et les réglages ADFS associés. La configuration se fait côté serveur, dans Main.cs, pendant l'enregistrement des services.
Informations à renseigner
| Élément | Description | Exemple |
|---|---|---|
Nom d'affichage (deuxième paramètre d'AddSaml) | explicit affiche sur la page de connexion un bouton qui porte le nom du schéma. Toute autre valeur est une expression régulière comparée à l'identifiant saisi : un identifiant qui la vérifie est envoyé vers ce schéma. | explicit |
NameIdPolicy | Type d'information utilisé pour identifier l'utilisateur. Par défaut, l'adresse e-mail est utilisée. | NameIdFormat.EmailAddress |
IdpMetadataLocation | Emplacement du fichier XML du fournisseur d'identité. | https://login.microsoftonline.com/12aaf12a-12c0-1d12-1234-12345cdcf12f/federationmetadata/2007-06/federationmetadata.xml |
EntityId | Identifiant de l'application connu par le fournisseur d'identité. | api://bf123456-f123-123c-b203-9d9a123f1234 |
RequireHttpsMetadata | Exige le téléchargement des métadonnées du fournisseur d'identité en HTTPS (true par défaut). Un fichier local est toujours accepté. | true |
IdpMetadataFallbackDuration | Durée pendant laquelle les dernières métadonnées téléchargées restent utilisées quand un nouveau téléchargement échoue (1 jour par défaut). | TimeSpan.FromDays(1) |
CallbackPath | Chemin, relatif à l'URL publique de l'application, sur lequel le fournisseur d'identité poste ses réponses. Un GET sur ce chemin renvoie les métadonnées de l'application. /saml par défaut ; chaque schéma SAML2 doit avoir le sien. | /saml |
AllowedNameIdDomains | Domaines des utilisateurs que le fournisseur d'identité du schéma peut connecter. Liste vide par défaut : tous les domaines sont acceptés. | { "partner.example" } |
Si le serveur n'a pas accès à internet, utilisez un fichier local pour IdpMetadataLocation plutôt qu'une URL distante.
Enregistrer le fournisseur SAML2
protected override void OnConfigureServices(IServiceCollection services)
{
Services.Registry.RegisterServices(services);
services.AddAuthentication().AddSaml("Enterprise identity", "explicit", options =>
{
// Identifie l'utilisateur avec son adresse e-mail.
options.NameIdPolicy = NameIdFormat.EmailAddress;
options.EntityId = Configuration["Saml:SpEntityId"];
options.IdpMetadataLocation = Configuration["Saml:IdpMetadataUrl"];
});
}
{
"Saml": {
"SpEntityId": "api://bf123456-f123-123c-b203-9d9a123f1234",
"IdpMetadataUrl": "https://login.microsoftonline.com/12aaf12a-12c0-1d12-1234-12345cdcf12f/federationmetadata/2007-06/federationmetadata.xml"
}
}
- Si
EntityIdn'est pas défini, l'URL racine de l'application est utilisée. IdpMetadataLocationpeut référencer un chemin local.
options.IdpMetadataLocation = @"C:\...\IdpMetadata.xml";
Les métadonnées téléchargées sont conservées 6 heures par emplacement. Si un nouveau téléchargement échoue, la version précédente reste utilisée et le téléchargement est retenté toutes les 5 minutes, tant qu'elle date de moins de IdpMetadataFallbackDuration (1 jour par défaut). Au-delà, l'authentification échoue jusqu'au prochain téléchargement réussi : un attaquant qui ferait échouer le téléchargement ne peut pas maintenir en service un certificat retiré par le fournisseur d'identité. Un téléchargement est abandonné au bout de 30 secondes.
Les métadonnées contiennent les certificats qui signent les réponses : téléchargées en HTTP, elles peuvent être remplacées par un attaquant sur le réseau. Une URL http:// est donc refusée. Ne passez RequireHttpsMetadata à false que pour un fournisseur d'identité de développement.
Plusieurs fournisseurs d'identité
Une application peut enregistrer plusieurs schémas SAML2, chacun avec son fournisseur d'identité, par exemple l'annuaire de l'entreprise et celui d'un partenaire :
services.AddAuthentication()
.AddSaml("Corporate", "explicit", options =>
{
options.IdpMetadataLocation = "https://adfs.corp.example/FederationMetadata/2007-06/FederationMetadata.xml";
})
.AddSaml("Partner", @"@partner\.example$", options =>
{
options.CallbackPath = "/saml-partner";
options.EntityId = "https://app.example/partner";
options.IdpMetadataLocation = "https://idp.partner.example/metadata";
// Le fournisseur d'identité du partenaire ne connecte que ses propres utilisateurs.
options.AllowedNameIdDomains.Add("partner.example");
});
- Un chemin par schéma : chaque schéma reçoit les réponses postées sur son
CallbackPath, comparé à la requête en tenant compte de la casse, comme le chemin du cookie de corrélation. L'application ne démarre pas si deux schémas partagent un chemin, même avec une casse différente ; le message d'erreur propose/saml-<schéma>. Un chemin vide ou/est refusé au démarrage. Le chemin est traité avant le routage : ne lui donnez pas le chemin d'une page ni celui d'un autre fournisseur externe (/signin-oidc). - Un
EntityIdpar schéma pour un même fournisseur d'identité : ADFS et Entra ID imposent un identifiant unique à chaque relying party. Deux schémas qui enregistrent le mêmeEntityIdauprès des mêmes métadonnées produisent un avertissement dans les journaux au démarrage : cette configuration ne fonctionne qu'avec un fournisseur d'identité qui renvoie chaque réponse à l'URL de retour indiquée par la requête. - Métadonnées de l'application par schéma : elles sont servies en GET sur l'URL publique de l'application suivie du
CallbackPathdu schéma (https://app.example/saml-partner), dans un fichierSpMetadata-<schéma>.xml. Les routes SAML ne sont pas disponibles hors connexion : dans un navigateur où l'application est installée (service worker), ouvrir cette URL affiche la page de l'application. Ouvrez-la en navigation privée, ou laissez le fournisseur d'identité la télécharger. - Options modifiées à chaud : les options sont contrôlées au démarrage. Si elles sont liées à une configuration rechargée et deviennent invalides, seul le schéma concerné cesse de fonctionner (erreur dans les journaux, une fois) ; le reste de l'application continue de répondre. Si le changement donne le même chemin à deux schémas, il ne peut plus être refusé : une erreur nomme les deux schémas dans les journaux, et le premier enregistré reçoit les réponses des deux jusqu'à la correction de la configuration.
- Page de connexion : un schéma
explicitaffiche un bouton au nom du schéma. Une expression régulière envoie vers le schéma les identifiants qui la vérifient ; elle tient compte de la casse, et le premier schéma enregistré qui correspond l'emporte.AddSaml(schéma, options), sans nom d'affichage, utilise l'expressionSAML: deux schémas enregistrés ainsi sont ambigus. Avec plusieurs SSO, la redirection automatique vers le seul SSO configuré disparaît.
Confiance accordée à chaque fournisseur d'identité
L'utilisateur d'un SSO est retrouvé par son identifiant de connexion (le NameID, ou le claim renvoyé par GetNameIdentifierClaimType pour le schéma), quel que soit le schéma qui l'a authentifié. Sans restriction, le fournisseur d'identité d'un partenaire qui émet l'identifiant pdg@corp.example connecte l'utilisateur au compte du dirigeant de l'entreprise.
Renseignez AllowedNameIdDomains sur chaque schéma dont le fournisseur d'identité ne gère pas tous les utilisateurs de l'application. Le contrôle porte sur l'identifiant de connexion utilisé pour retrouver le compte : le NameID, ou le claim renvoyé par GetNameIdentifierClaimType si l'application le surcharge pour ce schéma. Le domaine est la partie de cet identifiant qui suit son dernier @, comparée sans tenir compte de la casse :
x@partner.exampleest accepté pourpartner.example,x@PARTNER.exampleaussi ;- un sous-domaine (
x@evil.partner.example), un autre domaine (x@partner.example.evil) et un identifiant sans@sont refusés ; - un domaine écrit avec son
@(@partner.example), vide, entouré d'espaces ou terminé par un point est refusé au démarrage.
La réponse refusée ne connecte personne, et l'erreur nomme le schéma et le domaine refusé.
Pour une règle plus fine, les méthodes de LgAuthApplication reçoivent le nom du schéma dans context.IdentitySource.SourceName : GetIdentityUserAsync (recherche du compte), OnUnknownUserAsync (création d'un compte inconnu) et OnSignInAsync.
AllowedNameIdDomains contrôle l'identifiant de connexion (context.IdentitySource.UserLogin). Une surcharge de GetIdentityUserAsync qui cherche le compte par une autre valeur, par exemple un claim e-mail, n'est pas protégée : appliquez la même règle à cette valeur avec IsAllowedLogin des options du schéma, ou désignez ce claim avec GetNameIdentifierClaimType pour que le contrôle porte dessus.
protected override Task<ApplicationUser> GetIdentityUserAsync(AuthenticationContext context)
{
// Seul le schéma "Partner" retrouve les comptes par le claim e-mail : les autres connexions (formulaire,
// autres SSO) gardent la recherche par défaut
if (context.IdentitySource.SourceName != "Partner")
{
return base.GetIdentityUserAsync(context);
}
string email = context.IdentitySource.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
SamlOptions options = context.ServiceProvider.GetRequiredService<IOptionsMonitor<SamlOptions>>().Get("Partner");
// La valeur réellement utilisée pour retrouver le compte doit exister et appartenir aux domaines du schéma.
// Renvoyer null ne suffit pas : OnUnknownUserAsync pourrait alors créer le compte.
if (string.IsNullOrEmpty(email) || !options.IsAllowedLogin(email))
{
throw new AuthenticationException("Accès refusé.");
}
return context.UserManager.FindByEmailAsync(email);
}
Par exemple, pour que seul l'annuaire de l'entreprise crée des comptes :
protected override Task<ApplicationUser> OnUnknownUserAsync(AuthenticationContext context)
{
if (context.IdentitySource.SourceName != "Corporate")
{
// Un utilisateur inconnu d'un autre fournisseur d'identité n'est pas créé
return Task.FromResult<ApplicationUser>(null);
}
// ...
}
Contrôles de sécurité
Contrôles appliqués à chaque réponse
Ces contrôles sont toujours appliqués et ne peuvent pas être désactivés :
- le statut de la réponse est un succès ; un refus d'accès du fournisseur d'identité lève une
UnauthorizedException; - la réponse contient une seule assertion, en clair (les assertions chiffrées ne sont pas supportées) ;
- la signature porte sur le nœud qui la contient, avec un identifiant unique dans le document (protection contre le « signature wrapping ») ;
- la signature utilise RSA-SHA256 ou plus fort, et uniquement les transformations prévues par SAML ;
- l'assertion porte une confirmation
bearerdont la date limite de livraison (NotOnOrAfter) n'est pas dépassée ; - l'assertion est destinée à l'application (
Audience) et n'a pas expiré ; - le document ne contient pas de DTD ; sa taille (3 Mo) et sa profondeur sont bornées.
Seules les valeurs signées font foi : quand seule l'assertion est signée, la destination (Destination) et la requête (InResponseTo) de l'enveloppe de la réponse sont contrôlées si elles sont présentes, mais ne suffisent pas. Ce sont le Recipient et l'InResponseTo de la confirmation bearer qui sont exigés.
Contrôles optionnels
Une assertion déjà utilisée est refusée tant qu'elle n'a pas expiré (EnableReplayCache, activé par défaut). Elle est identifiée par son émetteur et son identifiant, et mémorisée dans le cache mémoire de l'application : sur plusieurs instances, la protection est donc partielle.
La propriété Validation des options règle les autres contrôles, tous actifs par défaut :
| Propriété | Description | Défaut |
|---|---|---|
RequireDestination | La confirmation bearer doit désigner l'URL de l'application (Recipient). | true |
RequireInResponseTo | La réponse doit répondre à la requête d'authentification envoyée par l'application. | true |
RequireIssuerMatch | L'émetteur de l'assertion doit être le fournisseur d'identité attendu. | true |
RequireNotBefore | Une assertion dont la validité n'a pas commencé est refusée. | true |
ClockSkew | Tolérance sur l'écart d'horloge avec le fournisseur d'identité. | 2 minutes |
AllowSha1 | Accepte une signature SHA-1. | false |
RequireDestination peut révéler une URL de retour déclarée dans les métadonnées qui diffère de l'URL réelle de l'application, typiquement derrière un reverse proxy : corrigez l'URL publique de l'application plutôt que de désactiver le contrôle.
Le flux initié par le fournisseur d'identité (réponse non sollicitée) n'est pas supporté, quelle que soit la valeur de RequireInResponseTo : une réponse doit répondre à une requête envoyée par l'application.
Pour lire vous-même une réponse avec SamlResponse, fournissez les valeurs attendues avec ForRequest(identifiantDeLaRequête, urlDeRetour). Un contrôle actif dont la valeur attendue est inconnue fait échouer la lecture, plutôt que d'accepter la réponse sans la contrôler. Désactivez explicitement le contrôle (RequireDestination ou RequireInResponseTo à false) s'il ne s'applique pas.
SamlResponseValidation validation = options.Validation.ForRequest(requestId, "https://mon-application/saml");
SamlResponse response = SamlResponse.FromHttpRequest(idpMetadata, options.EntityId, Request, logger, validation);
Fournisseur d'identité signant en SHA-1
Les signatures SHA-1 sont refusées par défaut. Pour un fournisseur d'identité ancien qui ne sait pas signer en SHA-256, autorisez-les explicitement :
services.AddAuthentication().AddSaml("Enterprise identity", "explicit", options =>
{
// ...
// Fournisseur d'identité ancien : à réserver au temps de sa migration vers SHA-256.
options.Validation.AllowSha1 = true;
});
Sur AD FS, préférez passer l'algorithme de signature du Relying Party Trust à SHA-256 (onglet Advanced).
Cookie de corrélation
La requête d'authentification pose un cookie de corrélation, valable 15 minutes, sur le chemin de l'URL de retour du schéma (le CallbackPath, /saml par défaut, ajouté à l'URL publique de l'application). La réponse n'est acceptée que par le navigateur qui porte ce cookie : un attaquant ne peut pas faire connecter la victime sous son propre compte (« login CSRF »).
- En HTTPS, le cookie
__Secure-Lagoon.Saml.Correlation.<identifiant>estSameSite=None; Secure: il accompagne directement le POST du fournisseur d'identité. Le préfixe__Secure-empêche un attaquant sur le réseau de le poser depuis une réponse HTTP. - En HTTP, un navigateur refuse un cookie
SameSite=NonesansSecure. Le cookie.Lagoon.Saml.Correlation.<identifiant>est doncSameSite=Lax, et il n'est pas envoyé avec le POST du fournisseur d'identité, qui vient d'un autre site. L'application répond alors par une page qui poste de nouveau la réponse vers elle-même, automatiquement : ce second POST porte le cookie. La connexion fonctionne, avec un aller-retour de plus.
Le schéma, le chemin et le caractère sécurisé du cookie suivent l'URL publique de l'application, et non la requête reçue : ils restent corrects derrière un reverse proxy qui termine TLS ou retire un préfixe de chemin.
Configuration SAML2 pour ADFS
Configuration de l'application
Configurez l'application pour utiliser le format Transient pour le NameIdPolicy : urn:oasis:names:tc:SAML:2.0:nameid-format:transient.
Certaines versions d'ADFS semblent avoir du mal à retourner le format e-mail pour le NameId. Dans ce cas, l'e-mail est retourné dans le format transient.
Créer un Relying Party Trust dans AD FS
Pour enregistrer l'application Lagoon dans ADFS, récupérez le fichier de métadonnées de l'application en ajoutant le CallbackPath du schéma (saml par défaut) à l'URL racine de l'application.
Si le serveur ADFS accède au serveur de l'application en HTTPS, ajoutez le Relying Party Trust à partir de l'URL. Sinon, téléchargez le fichier de métadonnées puis importez-le dans ADFS.
Modifier les Claim Rules
Dans la fenêtre Edit Claim Issuance Policy du Relying Party Trust, créez les règles qui détaillent les attributs requis par l'application et fournis par AD FS à chaque ouverture de session.
Règle 1 : envoyer les claims
Cette règle demande à AD FS d'envoyer les trois attributs requis : e-mail, prénom et nom.
- Cliquez sur
Add rule. - Sélectionnez
Send LDAP Attributes as Claims. - Pour
Claim rule name, saisissezSend attributes. - Pour
Attribute store, sélectionnezActive Directory. - Définissez la table de mappage suivante, puis validez.
| LDAP Attribute Outgoing | Claim type |
|---|---|
E-Mail-Addresses | E-Mail Address |
Given-Name | Given Name |
Surname | Surname |
Règle 2 : passer l'adresse e-mail en NameId
Passez l'adresse e-mail en tant que Transient Identifier pour qu'elle puisse être utilisée par l'application Lagoon.
Groupes de l'utilisateur
Ajoutez une règle pour envoyer en claims la liste des groupes auxquels appartient l'utilisateur.
Il y aura autant de claims du type http://schemas.xmlsoap.org/claims/Group que l'utilisateur possède de groupes.