Comprendre SAML

SAML en détail

Les messages, les signatures et les vérifications qu'une application doit faire, avec ce que fait ID-Kit à chaque étape.

1. Rôles et vocabulaire

SAML 2.0 (2005) est un standard OASIS de fédération d'identité. Le fournisseur d'identité (IdP) authentifie l'utilisateur et émet des assertions ; le fournisseur de service (SP) les consomme à l'adresse nommée ACS (Assertion Consumer Service). Chaque partie a un identifiant unique, l'Entity ID, souvent une URL qui n'a pas besoin d'être joignable.

2. Les métadonnées

Un document XML décrit chaque partie : son Entity ID, ses points d'accès, ses certificats. Les importer évite la plupart des erreurs de recopie.

<md:EntityDescriptor entityID="https://idp.example/idp">
  <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:KeyDescriptor use="signing">...<ds:X509Certificate>MIIC...</ds:X509Certificate>...</md:KeyDescriptor>
    <md:SingleSignOnService Binding="...HTTP-Redirect" Location="https://idp.example/sso"/>
    <md:SingleSignOnService Binding="...HTTP-POST" Location="https://idp.example/sso"/>
  </md:IDPSSODescriptor>
</md:EntityDescriptor>
  • KeyDescriptor use="signing" : le certificat dont la clé publique permet de vérifier les signatures. use="encryption" sert au chiffrement.
  • SingleSignOnService (IdP) : où envoyer une demande, avec le binding accepté.
  • AssertionConsumerService (SP) : où recevoir la réponse. WantAssertionsSigned indique que le SP exige une assertion signée.
  • Renouvellement de certificat : publiez les deux certificats (ancien et nouveau) pendant la transition. Un certificat expiré, sans remplaçant publié, bloque toutes les connexions d'un coup.

3. Les deux scénarios de connexion

Lancé par l'application (SP-initiated), le plus courant

L'utilisateur ouvre l'application, qui envoie une AuthnRequest. La réponse cite l'identifiant de cette demande dans InResponseTo : l'application peut vérifier qu'elle a bien demandé cette réponse. Le paramètre RelayState permet de revenir à la page demandée au départ.

Lancé par le fournisseur d'identité (IdP-initiated)

L'utilisateur clique sur une tuile dans le portail de son organisation. Aucune demande n'a précédé : la réponse arrive « sans demande » et n'a pas d'InResponseTo. C'est pratique, mais moins sûr (l'application ne peut pas relier la réponse à une demande), et certaines applications le refusent.

4. Les bindings : comment les messages voyagent

Le navigateur transporte les messages. Un binding décrit comment :

BindingPrincipeUsage
HTTP-RedirectLe XML est compressé (deflate), encodé en base64 puis placé dans l'URL (?SAMLRequest=…). Si la demande est signée, la signature est hors du XML, dans les paramètres SigAlg et Signature, et porte sur la chaîne SAMLRequest=…&RelayState=…&SigAlg=… telle qu'envoyée.Demandes (taille limitée par l'URL)
HTTP-POSTLe XML est encodé en base64 dans un formulaire HTML que le navigateur envoie automatiquement. La signature est dans le XML.Réponses (presque toujours) et demandes
HTTP-ArtifactOn n'envoie qu'une référence, le contenu est récupéré directement serveur à serveur.Rare

5. La demande (AuthnRequest)

<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    ID="_a1b2c3"                                   <!-- identifiant unique, repris dans la réponse -->
    IssueInstant="2026-11-01T10:00:00Z"
    Destination="https://idp.example/sso"          <!-- à qui la demande est adressée -->
    AssertionConsumerServiceURL="https://app.example/saml/acs"   <!-- où renvoyer la réponse -->
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Version="2.0">
  <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://app.example/metadata</saml:Issuer>  <!-- l'Entity ID de l'application -->
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
</samlp:AuthnRequest>

Options utiles : ForceAuthn (réauthentifier même si une session existe), IsPassive (ne rien afficher, échouer si l'utilisateur n'est pas déjà connecté), NameIDPolicy (format d'identifiant souhaité), RequestedAuthnContext (niveau d'authentification exigé, par exemple la double authentification).

6. La réponse et l'assertion

<samlp:Response Destination="https://app.example/saml/acs" InResponseTo="_a1b2c3" ...>
  <saml:Issuer>https://idp.example/idp</saml:Issuer>
  <samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
  <saml:Assertion ID="_z9y8" IssueInstant="..." Version="2.0">
    <saml:Issuer>https://idp.example/idp</saml:Issuer>
    <ds:Signature>...</ds:Signature>                        <!-- le sceau -->
    <saml:Subject>
      <saml:NameID Format="...emailAddress">jane@example.com</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData InResponseTo="_a1b2c3" Recipient="https://app.example/saml/acs" NotOnOrAfter="..."/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="..." NotOnOrAfter="...">
      <saml:AudienceRestriction><saml:Audience>https://app.example/metadata</saml:Audience></saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="..." SessionIndex="_z9y8">...</saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="groups"><saml:AttributeValue>admins</saml:AttributeValue></saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>

Deux niveaux : la Response (enveloppe : statut, destination) et l'Assertion (le contenu qui compte). Le Status dit si l'identification a réussi ; en cas d'échec il n'y a pas d'assertion.

7. La signature XML

  • Quoi signer : la Response, l'Assertion, ou les deux. Signer l'assertion est le minimum raisonnable : c'est elle qui contient l'identité.
  • « Enveloped » : la signature est placée à l'intérieur de l'élément qu'elle signe (juste après l'Issuer), et exclue du calcul.
  • Canonicalisation (exc-c14n) : avant de calculer le condensat, le XML est normalisé (ordre des attributs, espaces de noms, échappements) pour que des différences sans importance, comme un retour à la ligne ou un préfixe différent, n'invalident pas la signature.
  • Deux calculs : un condensat (digest) de l'élément, puis la signature RSA du bloc SignedInfo qui contient ce condensat.
  • Algorithmes : RSA-SHA256 est la norme actuelle. SHA-1 est obsolète : à garder uniquement pour d'anciens systèmes.
  • Le certificat dans la signature ne prouve rien : un attaquant peut y mettre le sien. L'application doit comparer avec le certificat de confiance obtenu dans les métadonnées de l'IdP.

ID-Kit peut émettre des signatures sur l'assertion, la réponse ou les deux, avec SHA-1/256/384/512, et simuler les cas défectueux pour vérifier que votre application les refuse.

8. Les vérifications côté application

Dans l'ordre, une application sérieuse contrôle :

  1. Le statut est Success.
  2. La signature est valide, faite avec un certificat de confiance, et couvre bien l'élément qu'on va lire (protection contre le « signature wrapping »).
  3. Destination : l'URL ACS de l'application.
  4. InResponseTo : correspond à une demande envoyée et non encore consommée (sauf IdP-initiated autorisé).
  5. Issuer : l'IdP attendu.
  6. Audience : l'Entity ID de l'application.
  7. NotBefore / NotOnOrAfter : valable maintenant, avec une petite tolérance d'horloge (quelques minutes).
  8. SubjectConfirmation : méthode bearer, Recipient = URL ACS, pas expirée.
  9. Anti-rejeu : l'identifiant de l'assertion n'a pas déjà été utilisé.
  10. Seulement ensuite : lire le NameID et les attributs.

Le SP de test d'ID-Kit effectue tous ces contrôles et vous les affiche un par un, avec la correction à apporter.

9. NameID et attributs

  • Formats de NameID : emailAddress (lisible, mais l'adresse peut changer), persistent (identifiant stable propre à la paire IdP/SP, à préférer comme clé de compte), transient (change à chaque connexion), unspecified.
  • Attributs : paires nom/valeurs, parfois à valeurs multiples (groupes). Le nom exact compte (email, groups, ou un long nom de type URI chez Microsoft). C'est l'IdP qui décide quels attributs partent, et l'application qui doit les mapper.

10. Chiffrement et déconnexion

  • EncryptedAssertion : l'IdP peut chiffrer l'assertion avec le certificat de chiffrement du SP. Utile quand la réponse transite par un intermédiaire non maîtrisé. Au sein d'une connexion HTTPS de bout en bout, la signature suffit généralement. ID-Kit ne déchiffre pas encore les assertions.
  • Déconnexion unique (SLO) : LogoutRequest/LogoutResponse entre SP et IdP, repérés par le SessionIndex. En pratique, c'est la partie la moins bien et la moins uniformément implémentée du standard.

11. Sécurité : les pièges classiques

  • XML Signature Wrapping : l'attaquant glisse une fausse assertion à côté de la vraie signée. Parade : vérifier que la signature référence exactement l'élément lu, et que les identifiants sont uniques.
  • XXE : un parseur XML qui suit les entités externes peut lire des fichiers. Parade : interdire DOCTYPE et ENTITY (ID-Kit le fait).
  • Rejeu : une assertion volée est réutilisée. Parade : durée courte, InResponseTo, mémoire des identifiants consommés.
  • Signature absente acceptée : une application qui « vérifie si une signature existe » mais n'exige pas qu'elle soit présente est contournable.
  • Audience et Recipient ignorés : une assertion émise pour une autre application serait acceptée.
  • RelayState non contrôlé : redirection ouverte. Parade : n'accepter que des adresses internes.

Pour tester concrètement ces comportements, utilisez les simulations d'erreur de l'IdP ID-Kit : chacune doit être refusée par votre application.

Envie de voir ça en vrai ? ID-Kit joue le rôle qui vous manque et vous montre chaque étape.