Tôt ou tard, un grand client pose la question : notre personnel peut-il se connecter à votre application avec son compte d'entreprise ? Il ne s'agit pas d'un nom d'utilisateur et d'un mot de passe de plus. Il s'agit de la connexion Microsoft Entra ID, Okta ou AD FS qu'il utilise déjà pour tout le reste, avec sa propre politique de mots de passe, sa propre double authentification et un seul endroit pour désactiver un compte le jour où quelqu'un quitte l'entreprise.
La réponse attendue par leur équipe identité est SAML 2.0. Dans la présentation des nouveaux composants de connexion, SAML n'avait droit qu'à un seul paragraphe. Cet article couvre le flux complet : ce que fait TsgcSAMLServiceProvider, le code d'une page de connexion et d'un Assertion Consumer Service, comment enregistrer votre application auprès des fournisseurs d'identité courants, et comment tester tout cela dès aujourd'hui sans compte nulle part.
Comment fonctionne la connexion SAML
Trois parties interviennent. Votre application est le service provider (SP). L'annuaire du client est le identity provider (IdP). Le navigateur transporte les messages entre les deux, de sorte que votre serveur et l'IdP ne communiquent jamais directement.
- L'utilisateur ouvre votre URL de connexion. Votre application construit un AuthnRequest et redirige le navigateur vers l'IdP.
- L'IdP connecte l'utilisateur, avec les règles de mot de passe, de MFA ou d'accès conditionnel que l'entreprise a définies.
- L'IdP répond avec un SAMLResponse signé, et le navigateur le poste vers l'URL de votre Assertion Consumer Service (ACS).
- Votre application valide la réponse et crée sa propre session pour l'utilisateur qu'elle désigne.
C'est à l'étape quatre que les implémentations SAML se trompent, et c'est exactement la partie que le composant fait pour vous.
Le service provider, étape par étape
TsgcSAMLServiceProvider n'est pas un serveur HTTP. Il construit et vérifie les messages SAML, et vous l'appelez depuis le gestionnaire de requêtes du serveur que votre application possède déjà, par exemple un TsgcWebSocketHTTPServer ou un TsgcHTTPServer.
- Décrivez votre application. Définissez
EntityID, le nom unique de votre application (généralement son URL de métadonnées), etAssertionConsumerServiceURL, l'URL https où arrive la réponse. - Décrivez le fournisseur d'identité. Appelez
LoadIdPMetadataavec le document de métadonnées de l'IdP. Il lit l'entity ID de l'IdP, son URL de connexion et son binding, ainsi que tous les certificats de signature. Sans métadonnées, définissez à la mainIdPEntityID,IdPSSOURLetIdPCertificates. - Envoyez la requête.
GetAuthnRequestRedirectURLrenvoie l'URL vers laquelle rediriger le navigateur. Pour un IdP qui n'offre que le binding HTTP-POST,GetAuthnRequestPostFormrenvoie à la place une page qui poste la requête. - Conservez l'id de la requête. Les deux méthodes renvoient l'id du nouvel AuthnRequest. Conservez-le sur le serveur, indexé par un RelayState aléatoire ou par le cookie de session, et supprimez-le à l'arrivée de la réponse, afin que chaque requête ne puisse recevoir qu'une seule réponse.
- Traitez la réponse. À l'URL de l'ACS, appelez
ProcessResponseavec le SAMLResponse posté, le RelayState et l'id de requête conservé. Lorsqu'elle renvoieTrue, unTsgcSAMLResultcontient leNameID, leSessionIndexet tous les attributs envoyés par l'IdP. Lorsqu'elle renvoieFalse,ErrorMessageindique la raison etOnSAMLErrorse déclenche.
Avec EntityID, AssertionConsumerServiceURL et LoadIdPMetadata configurés une fois au démarrage, la page de connexion et l'ACS tiennent dans un seul gestionnaire de requêtes :
uses
sgcAuth_SAML_SP;
procedure TMyApp.OnCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
vRelayState, vRequestID: string;
oResult: TsgcSAMLResult;
begin
if ARequestInfo.Document = '/saml/login' then
begin
// 1. send the browser to the identity provider
vRelayState := NewRelayState;
AResponseInfo.Redirect(FSAML.GetAuthnRequestRedirectURL(vRelayState,
vRequestID));
// 2. keep the request id, the response must answer it
AddPendingRequest(vRelayState, vRequestID);
end
else if (ARequestInfo.Document = '/saml/acs') and
SameText(ARequestInfo.Command, 'POST') then
begin
// 3. the browser posts SAMLResponse and RelayState back
vRelayState := ARequestInfo.Params.Values['RelayState'];
vRequestID := TakePendingRequest(vRelayState);
oResult := TsgcSAMLResult.Create;
try
if FSAML.ProcessResponse(ARequestInfo.Params.Values['SAMLResponse'],
vRelayState, vRequestID, oResult) then
begin
// 4. signed in: create your own session for this user
CreateUserSession(AResponseInfo, oResult.NameID, oResult.SessionIndex);
AResponseInfo.Redirect('/');
end
else
AResponseInfo.ResponseNo := 403; // log oResult.ErrorMessage
finally
oResult.Free;
end;
end;
end;
NewRelayState, AddPendingRequest, TakePendingRequest et CreateUserSession représentent votre propre code : un GUID, une liste thread safe indexée par RelayState qui délivre chaque id de requête une seule fois, et le cookie de session de votre application. Les attributs arrivent sous forme de lignes Name=Value, donc oResult.Attributes.Values['email'] en lit un par son nom. Entra ID les nomme avec des claim URI comme http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.
Le RelayState n'est pas couvert par la signature de l'IdP. Utilisez-le comme clé pour retrouver votre propre requête en attente, jamais comme une URL vers laquelle vous redirigez sans la vérifier.
Enregistrer votre application auprès du fournisseur d'identité
GetMetadata renvoie les métadonnées du service provider : votre entity ID et votre URL d'ACS avec le binding HTTP-POST. Servez-les à une URL telle que /saml/metadata, ou enregistrez-les dans un fichier, et transmettez-les à l'IdP. Chaque fournisseur d'identité demande les deux mêmes valeurs, l'entity ID du SP et l'URL de l'ACS, donc les notes ci-dessous portent surtout sur l'endroit où chaque console les conserve. Dans tous les cas, laissez le chiffrement des assertions désactivé.
- Microsoft Entra ID. Enterprise applications, New application, Create your own application (non-gallery). Dans Single sign-on choisissez SAML, puis téléversez les métadonnées du SP ou remplissez Identifier (Entity ID) et Reply URL. Attribuez des utilisateurs ou des groupes, et chargez l'App Federation Metadata Url affiché dans SAML Certificates.
- Okta. Applications, Create App Integration, SAML 2.0. Single sign-on URL est votre URL d'ACS, avec “Use this for Recipient URL and Destination URL” coché, et Audience URI est votre entity ID. Ajoutez des attribute statements tels que email, firstName et lastName, attribuez des personnes ou des groupes, et chargez le Metadata URL depuis l'onglet Sign On.
- AD FS. Ajoutez un claims aware Relying Party Trust et importez les métadonnées du SP. AD FS n'accepte que des endpoints https. Ajoutez des claim rules qui envoient un Name ID, par exemple E-Mail-Addresses envoyé comme E-Mail Address, puis E-Mail Address transformé en Name ID. Les métadonnées de l'IdP se trouvent à
https://<adfs-host>/FederationMetadata/2007-06/FederationMetadata.xml. - Google Workspace. Admin console, Apps, Web and mobile apps, Add custom SAML app. Téléchargez les métadonnées de l'IdP, saisissez votre URL d'ACS et votre entity ID, choisissez le Name ID (par exemple l'adresse e-mail principale) et activez l'application pour vos utilisateurs.
- Keycloak. Créez un SAML client dont le Client ID est votre entity ID, ou importez les métadonnées du SP. Keycloak signe tout le document par défaut, activez donc également Sign assertions. Si Client signature required est activé, définissez
SignAuthnRequests,SPCertificateetSPPrivateKey. Les métadonnées de l'IdP se trouvent àhttps://<host>/realms/<realm>/protocol/saml/descriptor.
Quel que soit l'IdP, la dernière étape est la même : transmettez ses métadonnées à LoadIdPMetadata. Lorsque le document décrit plusieurs entités, le second paramètre sélectionne la vôtre.
Ce que ProcessResponse vérifie
Une réponse SAML est un document XML signé, et la plupart des vulnérabilités SAML connues sont des façons de faire lire à un service provider autre chose que ce qui a été signé. Une réponse n'est acceptée que si chacun de ces contrôles est validé :
- La signature, contre le certificat de l'IdP. La réponse n'est vérifiée qu'avec les certificats présents dans
IdPCertificates. Un certificat intégré au message n'est jamais approuvé, car un attaquant peut lui aussi en intégrer un. AvecWantAssertionsSigned, la valeur par défaut, l'assertion doit porter sa propre signature. - Défense contre le signature wrapping. La signature doit référencer un élément dont l'ID est unique dans le document, et après vérification seul l'élément signé est lu. Une assertion non signée glissée à côté de celle qui est signée n'est jamais examinée.
- Une seule assertion. La réponse doit contenir exactement une assertion, directement sous la response.
- Audience et recipient. L'audience doit être votre
EntityIDet le recipient votreAssertionConsumerServiceURL, de sorte qu'une assertion émise pour une autre application soit refusée. - La fenêtre temporelle. NotBefore et NotOnOrAfter sont vérifiés par rapport à l'UTC avec une tolérance de
ClockSkewsecondes, deux minutes par défaut.MaxAssertionAgepeut aussi limiter l'ancienneté maximale d'une assertion. - InResponseTo. La réponse doit répondre à l'id de requête que vous avez conservé. Les réponses non sollicitées, initiées par l'IdP, sont refusées sauf si vous définissez
AllowIdPInitiated. - Cache anti-rejeu. L'ID de chaque assertion acceptée est conservé jusqu'à son expiration, de sorte que la même réponse postée deux fois soit refusée. Le cache est thread safe et réside en mémoire. Lorsque plusieurs serveurs partagent la connexion, surchargez
DoAddToReplayCachepour conserver les ID dans un stockage partagé. - SHA-1 désactivé par défaut. Les signatures RSA-SHA1 et les empreintes SHA-1 sont refusées sauf si vous définissez
AllowSHA1pour un IdP qui en a encore besoin.
L'analyseur refuse également les déclarations DOCTYPE, il n'y a donc pas d'entités externes, et il limite la taille et la profondeur d'imbrication du document. L'issuer doit être l'IdP que vous avez configuré. Le premier contrôle qui échoue arrête la validation et sa raison se trouve dans ErrorMessage : consignez-la, et affichez à l'utilisateur une simple page “échec de la connexion”.
Testez-le sans compte
Vous n'avez pas besoin d'un tenant Entra ID pour voir SAML fonctionner. Mock SAML est un fournisseur d'identité de test gratuit sur mocksaml.com. Il accepte n'importe quel service provider et reprend l'audience et l'URL d'ACS depuis l'AuthnRequest, il n'y a donc rien à enregistrer.
La démo Demos\26.Authentication\03.SAML_ServiceProvider est un service provider complet sur un TsgcWebSocketHTTPServer, avec les endpoints /login, /acs et /metadata sur http://localhost:8090 :
- Compilez la démo et conservez libcrypto-3.dll et libssl-3.dll à côté de l'exécutable. Elles se trouvent dans le dossier de la démo, et OpenSSL vérifie les signatures RSA.
- Cliquez sur Load IdP metadata. La source par défaut est l'URL de métadonnées de mocksaml.com.
- Cliquez sur Start, puis sur Open Browser, et suivez le lien de connexion.
- Sur mocksaml.com, saisissez n'importe quel nom d'utilisateur du domaine example.com et n'importe quel mot de passe.
- Le navigateur revient sur l'ACS, et la page affiche le NameID, le SessionIndex et les attributs id, email, firstName et lastName.
Une fois que cela fonctionne, ouvrez http://localhost:8090/metadata, enregistrez-le auprès de votre véritable IdP, chargez les métadonnées de l'IdP dans la démo et connectez-vous à nouveau. Pour AD FS, exécutez d'abord la démo avec SSL, car AD FS n'accepte que https.
Limites actuelles
- Pas d'assertions chiffrées. Une réponse contenant un EncryptedAssertion ou un NameID chiffré est refusée. Laissez le chiffrement des assertions désactivé dans l'IdP. L'assertion reste tout de même signée et circule en https.
- Pas de Single Logout. Le SLO n'est pas implémenté.
SessionIndexest renvoyé afin que votre application puisse terminer sa propre session et construire sa propre déconnexion.
Documentation
Où l'obtenir
TsgcSAMLServiceProvider est inclus dans les éditions Enterprise et All-Access de sgcWebSockets, pour Delphi et C++ Builder, et le même composant fait partie de sgcWebSockets .NET. Si vous n'avez besoin que de l'authentification, le pack sgcAuth le propose avec les autres composants de connexion. L'unit est sgcAuth_SAML_SP, et rien ne change dans une application existante tant que vous n'avez pas déposé le composant sur une form.
À lire aussi
- Connexion Delphi avec Passkeys, SSO SAML, LDAP et TOTP 2FA
- Delphi PKCE OAuth2
- Autorisation via les PassKeys
Voir la vidéo
Une courte vidéo, “SAML single sign-on in Delphi with Entra ID, Okta and AD FS”, est disponible sur la chaîne eSeGeCe. Elle montre le code dans l'IDE et une connexion en direct avec la démo face à mocksaml.com.
Des questions, des retours, ou besoin d'aide pour connecter votre fournisseur d'identité ? Contactez-nous. Vous recevrez une réponse des personnes qui ont écrit le code.
