Tout d'abord il me faut présenter l'architecture du système nécessitant cette authentification : un site Web ASP.NET appelle un Web Service (toujours ASP.NET) pour récupérer les données d'une base SQL Server ou des éléments traduits à partir de fichiers XML. Il faut donc que l'accès à ce Web Service soit restreint à certains utilisateurs (sinon n'importe qui pourrait appeler les méthodes) et que ces utilisateurs aient des rôles définissant les méthodes qu'ils peuvent appeler.

Avantages :

  • L'authentification est faite au chargement de la page et toutes les méthodes de la classe peuvent utiliser le Web Service sans avoir à s'authentifier à nouveau
  • Les mots de passe ne circulent jamais en clair sur le réseau
  • Ce système est simple et relativement peu coûteux en CPU

Inconvénients :

  • C'est un système d'authentification "non standard". Il serait préférable d'utiliser les Web Services Enhancements 2.0 de Microsoft, mais cela implique l'installation d'un SDK supplémentaire. De plus, l'utilisation des WSE est beaucoup plus lourde et complexe (car récente et peu documentée).
  • L'utilisation d'un timestamp comme jeton n'est pas optimale car pas forcément unique. Il serait préférable de passer l'ID de session au client.

L'authentification se déroule de cette manière : le client demande un jeton unique au Web Service puis envoie ses credentials, avec un mot de passe hashé, accompagnés de ce jeton. Le WS compare alors le mot de passe hashé avec celui contenu dans une base de données (ou un fichier XML mais c'est moins secure), pour le nom d'utilisateur envoyé avec les credentials. Si les deux hashages sont identiques, l'utilisateur est alors authentifié, ce qui se traduit par la création d'une variable de session du côté du WS. Pour maintenir l'état de cette session, le client doit alors fournir un CookieContainer, qui contiendra l'ID de session. Voilà pour la logique du système. On est maintenant prêt à attaquer le code :)

Première étape : le Web Service
using System.Security.Cryptography;
using System.Text;
...

[WebService(Namespace="http://mon_domaine/")]
public class MonWebService : System.Web.Services.WebService
{
	public MonWebService ()
	{
		// Trucs à mettre dans le constructeur
	}

	[WebMethod]
	public string ObtenirJeton()
	{
		string aHacher, jeton;
		DateTime dt = DateTime.Now;
		aHacher=dt.ToString("yyyyMMdd")+"|"+dt.ToString("HHmm");
		jeton= Hash(aHacher);
		return jeton;
	}

	[WebMethod(EnableSession=true)]
	public bool Authentifier(string Key)
	{
		string [] hashArray;
		// On renvoie un booléen mais ce n'est pas vraiment utile ...
		bool resultat = false;
		try
		{
			hashArray=Key.Split('|');
			if (TestHash(hashArray[0], hashArray[1], 0))
			{
				Context.Session.Add("authUser","True");
				resultat = true;
			}
		}
		catch(Exception exc)
		{
			// Traitement de l'exception
		}
		return resultat;
	}
		
	private string Hash(string aHacher)
	{
		// On convertit d'abord la chaîne en octets,
		// avec un encodeur de texte.
		Encoder enc = System.Text.Encoding.ASCII.GetEncoder();

		// On crée un buffer suffisamment grand pour contenir la chaîne
		byte[] data = new byte[aHacher.Length];
		enc.GetBytes(aHacher.ToCharArray(), 0, aHacher.Length, data, 0, true);

		// On utilise la classe MD5 pour hasher data.
		MD5 md5 = new MD5CryptoServiceProvider();
		byte[] resultat = md5.ComputeHash(data);

		return BitConverter.ToString(resultat).Replace("-", "").ToLower();
	}

	private bool TestHash (string HashStr, string UserName, int minutes)
	{
		string Pwd, aHacher;
		string sResult, sResultT, sResultToken;
		try
		{
			// Juste pour la démo : le mot de passe
			// est codé en dur puis hashé ...
			// Par sécurité, il vaudrait mieux qu'il
			// ne soit pas stocké en clair
			Pwd="toto123";

			DateTime dt = DateTime.Now;
			System.TimeSpan minute = 
				new System.TimeSpan(0,0,minutes,0,0);
			dt = dt-minute;
			// Avant le hashage on a :
			// USERNAME|PassWord|YYYYMMDD|HHMM
			aHacher=UserName.ToUpper()+
				"|"+Pwd+
				"|"+dt.ToString("yyyyMMdd")+
				"|"+dt.ToString("HHmm");
			sResult = Hash(aHacher);
			ToHash=dt.ToString("yyyyMMdd")+
				"|"+dt.ToString("HHmm");
			sResultToken = Hash(aHacher);
			//USERNAME|PassWord|TokenWeGotBefore
			aHacher=UserName.ToUpper()+
				"|"+Pwd+"|"+sResultToken;
			sResultT = Hash(aHacher);
    
			if ((sResult==HashStr) || (sResultT==HashStr)) 
				return true;
			else if (minutes==0) 
				// On permet un écart d'une seconde entre l'obtention
				// du jeton et la transmission des crédentials
				return TestHash (HashStr, UserName, 1);
			else
				return false;
		}
		catch
		{
			return false;
		}
	}

	[WebMethod(EnableSession=true)]
	public string HelloWorld()
	{
		string retour = String.Empty;
		try
		{
			if(!Context.Session.IsNewSession)
			{
				if(Context.Session["authUser"].ToString()=="True")
				{
					retour = "Vous êtes authentifié !";
				}
			}
		}
		catch(Exception ex)
		{
			// Traitement de l'exception
		}
		return retour;
	}
}

Deuxième étape : le client

Là c'est beaucoup plus simple : il faut ajouter la référence Web au WS qu'on vient de créer puis créer une page Web selon le modèle ci-dessous. On déclare un CookieContainer en private que l'on présente au Web Service pour stocker les informations de Session, puis on demande un jeton, on créé une clé et enfin on l'envoie au WS pour être authentifié. Si l'authentification échoue, une exception est levée du côté du WS, à vous de voir ce que vous voulez ;)
using MonNameSpace.WebService;
...

public class toto : System.Web.UI.UserControl
{
	private WebService.MonWebService ws;
	private System.Net.CookieContainer cookies;

	private void Page_Load(object sender, System.EventArgs e)
	{
		string login, password, jeton, cle, aHacher;
		bool auth;
		try
		{
			login="toto";
			password="toto123";
			ws = new MonWebService();
			if(this.cookies == null) 
				this.cookies = new System.Net.CookieContainer();
			ws.CookieContainer = this.cookies;

			jeton=ws.ObtenirJeton();
			aHacher=login.ToUpper()+"|"+password+"|"+jeton;
			// Ici on utilise une classe qui contient la même méthode 
			// de hachage que dans le WS
			// Ca évite de répéter ce code dans toutes les pages
			Hasher hash = new Hasher();
			cle=hash.Hash(aHacher)+"|"+login;
			auth = ws.Authentifier(cle);

			// On peut alors appeler n'importe quelle méthode demandant
			// une authentification. HelloWorld devrait donc renvoyer
			// "Vous êtes authentifié" :)
			string toto = ws.HelloWorld();
		}
		catch(Exception ex)
		{
			// Traitement de l'exception
		}
	}
}