Authentification LDAP et cookie partagé entre deux applications WebHost / SelfHost

Cet article est très ancien. Les choses ont bien changées depuis 2015. Le sujet abordé est conservé pour référence mais n'est certainement plus applicable en l'état.

Cet article est susceptible de référencer des images manquantes ou de contenir des erreurs de formatage sur son contenu. Il s'agit d'un import provenant d'un ancien blog.

Les problématiques abordées dans cet article sont :

  • Intégration d’une authentification LDAP avec Identity 2
  • Partage du cookie d’authentification entre deux applications :
    • un front-end (site WebHost)
    • et un back-end (service SelfHost)

Après MembershipProvider, SimpleMembershipProvider et Universal Providers, ASP.NET Identity est la nouvelle API de Microsoft pour la gestion de l’authentification et des autorisations dans une application ASP.NET. Identity 1 est apparue en 2013. La version 2 est arrivée en 2014, avec récemment la sortie de la version 2.2 (février 2015).

Ces dates sont importantes car l’API Identity a eu des changements significatifs d’une version à l’autre et il peut être difficile de s’y retrouver dans la documentation trouvée sur Internet.

Intégration d’une authentification LDAP

Je n’ai trouvé aucune documentation concernant l’intégration d’une authentification LDAP dans Identity, autrement que par AD FS. Ce n’est tout simplement pas prévu dans l’implémentation par défaut de l’API Identity qui distingue deux types d’authentification :

  • Locale (géré par l’application)
  • Sociale (géré par un service web externe)

L’authentification « locale » suppose que l’application stocke les mots de passe. L’authentification externe, « sociale », est basée sur OAuth. Un client LDAP n’entre dans aucune de ces catégories: c’est plus proche d’une authentification locale mais le mot de passe de l’utilisateur n’est pas géré par l’application qui se contente de la transmettre au client LDAP afin de valider l’identité soumise.

La solution la plus simple consiste à modifier l’implémentation par défaut des classes UserManager et SignInManager, en les dérivant, afin de surcharger leur méthode responsable de l’authentification par mot de passe :

La méthode principale est celle de UserManager qui utilise un client LDAP pour valider l’identité de l’utilisateur :

        public override async Task<bool> CheckPasswordAsync(UserIdentity user, string password)
        {
            bool authResult = await _ldapAuth.ValidateUserAsync(user.UserName, password);

            if (!authResult)
                return false;

            UserIdentity existingUser = await Store.FindByNameAsync(user.UserName);
            if (existingUser == null)
                await _store.CreateAsync(user);

            return true;
        }

Il faut également surcharger SignInManager.PasswordSignInAsync :

        public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
        {
            if (UserManager == null)
                return SignInStatus.Failure;
            var user = new UserIdentity(userName);
          
            bool isAuth = await UserManager.CheckPasswordAsync(user, password);
            if (!isAuth)
                return SignInStatus.Failure;

            user = await UserManager.FindByNameAsync(userName); 
            await base.SignInAsync(user, isPersistent, false);
            return SignInStatus.Success;
        }

Le code complet de ces deux classes est disponible sur GitHub.

Partage du cookie d’authentification entre WebHost et SelfHost

Comme pour l’intégration de LDAP, j’ai trouvé peu d’exemples de partage de cookie d’authentification entre plusieurs applications. L’argument est qu’il est déconseillé d’utiliser un cookie d’authentification avec WebApi car cette méthode est sensible aux attaques CSRF. La méthode conseillée est d’utiliser un token. Pourtant, la méthode du cookie est également supportée par WebApi. Il n’y a donc pas de raison de ne pas pouvoir partager le cookie entre un site hébergé dans IIS (WebHost), et un service Windows exposant des WebApi (SelfHost).

La difficulté vient de la stratégie par défaut pour la protection du cookie, qui diffère selon le type d’hôte :

Ce choix est surprenant alors que l’un des principaux arguments de l’intégration d’OWIN à Identity 2 est justement de rendre l’authentification indépendante du framework (Mvc, WebApi, SignalR…) et du type d’hôte (WebHost, SelfHost).

Il faut donc expliciter la stratégie à utiliser dans la configuration du middleware d’authentification dans Owin :

	app.UseCookieAuthentication(new CookieAuthenticationOptions
	{
	    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
	    TicketDataFormat = new TicketDataFormat(
	        new MachineKeyDataProtector(
	            "Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware", 
	            DefaultAuthenticationTypes.ApplicationCookie, 
	            "v1"))
	});

MachineKeyDataProtector est une implémentation très simple de IDataProtector :

    using System.Web.Security;
    using Microsoft.Owin.Security.DataProtection;
    
    public class MachineKeyDataProtector : IDataProtector
    {
        private readonly string[] _purposes;

        public MachineKeyDataProtector(params string[] purposes)
        {
            if (purposes == null)
                throw new ArgumentNullException("purposes");
            _purposes = purposes;
        }

        public byte[] Protect(byte[] userData)
        {
            return MachineKey.Protect(userData, _purposes);
        }

        public byte[] Unprotect(byte[] protectedData)
        {
            return MachineKey.Unprotect(protectedData, _purposes);
        }
    }

Les paramètres fournis dans le constructeur sont à ce jour les mêmes que ceux utilisés par l’implémentation par défaut dans la classe CookieAuthenticationMiddleware d’Owin (open source). Par conséquent, il est théoriquement possible de configurer uniquement le SelfHost et de conserver la configuration par défaut du WebHost. Je le déconseille cependant car en cas d’évolution dans la classe CookieAuthenticationMiddleware, le cookie ne sera plus compatible entre les deux hôtes. Il est donc important de configurer explicitement chaque hôte pour utiliser exactement la même stratégie.

Une solution d’exemple est disponible sur GitHub

La solution proposée sur GitHub contient trois projets :

  • WebHost: site ASP.NET MVC permettant de s’identifier afin de recevoir le cookie.
  • SelfHost: programme console avec WebApi hébergé par un hôte OWIN.
  • Shared : implémentation de IDataProtector basée sur MachineKey, utilisé par les deux hôtes pour la protection du cookie d’authentification (Application Cookie).

Le WebHost et le SelfHost, tels que configurés, ne peuvent pas fonctionner en même temps. Il faut donc d’abord s’identifier sur le WebHost, puis arrêter le serveur web de Visual Studio afin de pouvoir lancer le SelfHost sans erreur. Ce dernier expose un contrôleur WebApi protégé sur l’URL http://localhost:9000/externalApi/protectedValues.

Afin de faciliter la démonstration, l’implémentation du client LDAP est vide (l’authentification réussie toujours), ainsi que l’implémentation de IUserStore (Identity). Si vous utilisez l’implémentation par défaut d’Identity, le store est basé sur Entity Framework. Il n’y a rien de particulier à considérer quant au sujet de cet article, sur cette partie.

J’ai inclus en exemple la classe AdAuthClient (pas configurée dans la solution proposée sur GitHub). Cette implémentation LDAP a été testée sur Active Directory uniquement.

Autres ressources utiles

Self Host Web API avec OWIN (Katana)
Katana sur CodePlex (implémentation open source d’Owin)
Identity sur CodePlex (open source)
Introduction à ASP.NET Identity sur MSDN
Understanding OWIN Forms authentication in MVC 5 (MSDN Magazine, 07/2013)