Aller au contenu principal

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émentDescriptionExemple
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
NameIdPolicyType d'information utilisé pour identifier l'utilisateur. Par défaut, l'adresse e-mail est utilisée.NameIdFormat.EmailAddress
IdpMetadataLocationEmplacement du fichier XML du fournisseur d'identité.https://login.microsoftonline.com/12aaf12a-12c0-1d12-1234-12345cdcf12f/federationmetadata/2007-06/federationmetadata.xml
EntityIdIdentifiant de l'application connu par le fournisseur d'identité.api://bf123456-f123-123c-b203-9d9a123f1234
RequireHttpsMetadataExige 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
IdpMetadataFallbackDurationDuré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)
CallbackPathChemin, 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
AllowedNameIdDomainsDomaines 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" }
Attention

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​

Server\Main.cs
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"];
});
}
appsettings.json
{
"Saml": {
"SpEntityId": "api://bf123456-f123-123c-b203-9d9a123f1234",
"IdpMetadataUrl": "https://login.microsoftonline.com/12aaf12a-12c0-1d12-1234-12345cdcf12f/federationmetadata/2007-06/federationmetadata.xml"
}
}
Remarques
  • Si EntityId n'est pas défini, l'URL racine de l'application est utilisée.
  • IdpMetadataLocation peut référencer un chemin local.
Server\Main.cs
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.

Attention

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 :

Server\Main.cs
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 EntityId par 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ême EntityId auprè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 CallbackPath du schéma (https://app.example/saml-partner), dans un fichier SpMetadata-<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 explicit affiche 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'expression SAML : 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é​

Important

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.example est accepté pour partner.example, x@PARTNER.example aussi ;
  • 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.

Attention

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.

Server\Main.cs
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 :

Server\Main.cs
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 bearer dont 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éDescriptionDéfaut
RequireDestinationLa confirmation bearer doit désigner l'URL de l'application (Recipient).true
RequireInResponseToLa réponse doit répondre à la requête d'authentification envoyée par l'application.true
RequireIssuerMatchL'émetteur de l'assertion doit être le fournisseur d'identité attendu.true
RequireNotBeforeUne assertion dont la validité n'a pas commencé est refusée.true
ClockSkewTolérance sur l'écart d'horloge avec le fournisseur d'identité.2 minutes
AllowSha1Accepte 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 :

Server\Main.cs
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).

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> est SameSite=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=None sans Secure. Le cookie .Lagoon.Saml.Correlation.<identifiant> est donc SameSite=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.
astuce

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.

Attention

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.

  1. Cliquez sur Add rule.
  2. Sélectionnez Send LDAP Attributes as Claims.
  3. Pour Claim rule name, saisissez Send attributes.
  4. Pour Attribute store, sélectionnez Active Directory.
  5. Définissez la table de mappage suivante, puis validez.
LDAP Attribute OutgoingClaim type
E-Mail-AddressesE-Mail Address
Given-NameGiven Name
SurnameSurname

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.

astuce

Il y aura autant de claims du type http://schemas.xmlsoap.org/claims/Group que l'utilisateur possède de groupes.