Tutoriel : Contrôle de l’accès au service de données OGDI avec Windows Azure AppFabric – 3ième partie

La précédente partie de ce tutoriel nous a permis de créer et de configurer le contrôle d’accès de Windows Azure AppFabric de façon à ce que ce service soit prêt à délivrer des jetons SWT (sous réserve de s’authentifier avec le bon login et le bon mot de passe). Nous allons maintenant passer à l’étape suivante, c’est-à-dire l’établissement de la relation de confiance entre notre service de contrôle d’accès Windows Azure AppFabric ACS et notre service de données OGDI.

Pour cela, commencez par ouvrir votre solution Ogdi.sln dans Visual Studio 2010, puis ouvrez le projet Services_WebRole dans le dossier DataService.

image

Ouvrez ensuite le fichier Global.asax, qui contient le code permettant de répondre aux évènements applicatifs. Dans le cas présent, nous nous servirons de la méthode permettant de répondre aux évènements « réception de requête ». Commencez par rajouter les propriétés suivantes à la classe Global :

string acsHostName = "accesscontrol.windows.net";

 

// Nom de votre service ACS

string serviceNamespace = "ogdiaccesscontrol";

 

// Clée de signature du jeton

// (champ Token signing key lors de la procédure de création d'une relation de confiance

// avec une application)

string trustedTokenPolicyKey = "a8UfhHlHY56q99hR/gtmYWLasv6oC2eYkwQh00mDksQ=";

 

//Audience de confiance

//(champ Realm lors de la céation d'une relation de confiance avec une application)

string trustedAudience = "https://ogdidataservice2908.cloudapp.net/";

 

//Type de revendication attendue

string requiredClaimType = "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/authorizationdecision";

 

//Valeur de la revendication attendue

string requiredClaimValue = "yes";

Nous allons maintenant avoir besoin d’une classe permettant de valider le jeton SWT reçu avec chaque requête envoyée par le client. Pour cela, nous allons devoir à chaque requête vérifier qu’un jeton est bien présent :

  • Dans le cas où le jeton ne serait pas présent, il faut renvoyer un code d’erreur 401 (accès non-autorisé) au client ;
  • Dans le cas où un jeton serait présent, il faut vérifier sa validité (pour cela, nous allons créer une classe dédiée, étant donné la relative complexité algorithmique du processus de validation) et, le cas échéant, autoriser ou refuser l’accès au service.

Commençons tout d’abord par rajouter cette méthode à la classe Global :

private void ReturnUnauthorized()

{

    Response.StatusCode = (int)HttpStatusCode.Unauthorized;

    Response.End();

}

Pour passer chaque requête reçue par le service de données dans la « moulinette » permettant de valider l’accès aux données, il faut écrire notre code dans la méthode Application_BeginRequestqui est actuellement vide :

protected void Application_BeginRequest(object sender, EventArgs e)

{

    string headerValue = Request.Headers.Get("Authorization");

 

    // vérifier que l'en-tête est bien présente

    if (string.IsNullOrEmpty(headerValue))

    {

        this.ReturnUnauthorized();

        return;

    }

 

    // vérififer que l'entête commence par 'OAuth2'

    if (!headerValue.StartsWith("OAuth2 "))

    {

        this.ReturnUnauthorized();

        return;

    }

 

    string[] nameValuePair = headerValue.Substring("OAuth2 ".Length).Split(new char[] { '=' }, 2);

 

    if (nameValuePair.Length != 2 ||

        nameValuePair[0] != "access_token" ||

        !nameValuePair[1].StartsWith("\"") ||

        !nameValuePair[1].EndsWith("\""))

    {

        this.ReturnUnauthorized();

        return;

    }

 

    // sanitisation du jeton

    string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);

 

    // créer un validateur de jeton

    TokenValidator validator = new TokenValidator(

        this.acsHostName,

        this.serviceNamespace,

        this.trustedAudience,

        this.trustedTokenPolicyKey);

 

    // valider le jeton

    if (!validator.Validate(token))

    {

        this.ReturnUnauthorized();

        return;

    }

 

    // vérifier que la revendication attendue est bien présente

    Dictionary<string, string> claims = validator.GetNameValues(token);

 

    string actionClaimValue;

    if (!claims.TryGetValue(this.requiredClaimType, out actionClaimValue))

    {

        this.ReturnUnauthorized();

        return;

    }

 

    // vérifier la valeur de la revendication

    if (!actionClaimValue.Equals(this.requiredClaimValue))

    {

        this.ReturnUnauthorized();

        return;

    }

}

Finalement, il ne nous reste plus qu’à rajouter à notre projet la classe TokenValidator :

public class TokenValidator

{

    private string issuerLabel = "Issuer";

    private string expiresLabel = "ExpiresOn";

    private string audienceLabel = "Audience";

    private string hmacSHA256Label = "HMACSHA256";

 

    private string acsHostName;

 

    private string trustedSigningKey;

    private string trustedTokenIssuer;

    private string trustedAudienceValue;

 

    public TokenValidator(string acsHostName, string serviceNamespace,

               string trustedAudienceValue, string trustedSigningKey)

    {

        this.trustedSigningKey = trustedSigningKey;

        this.trustedTokenIssuer = String.Format("https://{0}.{1}/",

            serviceNamespace.ToLowerInvariant(),

            acsHostName.ToLowerInvariant());

 

        this.trustedAudienceValue = trustedAudienceValue;

    }

 

    public bool Validate(string token)

    {

        if (!this.IsHMACValid(token, Convert.FromBase64String(this.trustedSigningKey)))

        {

            return false;

        }

 

        if (this.IsExpired(token))

        {

            return false;

        }

 

        if (!this.IsIssuerTrusted(token))

        {

            return false;

        }

 

        if (!this.IsAudienceTrusted(token))

        {

            return false;

        }

 

        return true;

    }

 

    //Transforme le jeton de sécurité SWT en un dictionnaire clé/valeur

    public Dictionary<string, string> GetNameValues(string token)

    {

        if (string.IsNullOrEmpty(token))

        {

            throw new ArgumentException();

        }

 

        return

            token

            .Split('&')

            .Aggregate(

            new Dictionary<string, string>(),

            (dict, rawNameValue) =>

            {

                if (rawNameValue == string.Empty)

                {

           return dict;

                }

 

                string[] nameValue = rawNameValue.Split('=');

 

                if (nameValue.Length != 2)

                {

                    throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character");

                }

 

                if (dict.ContainsKey(nameValue[0]) == true)

                {

                    throw new ArgumentException("Repeated name/value pair in form");

                }

 

                dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1]));

                return dict;

            });

    }

 

    private static ulong GenerateTimeStamp()

    {

        TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);

        return Convert.ToUInt64(ts.TotalSeconds);

    }

 

    //Vérification de l'audience du jeton délivré

    private bool IsAudienceTrusted(string token)

    {

        Dictionary<string, string> tokenValues = this.GetNameValues(token);

 

        string audienceValue;

 

        tokenValues.TryGetValue(this.audienceLabel, out audienceValue);

 

        if (!string.IsNullOrEmpty(audienceValue))

        {

            if (audienceValue.Equals(this.trustedAudienceValue, StringComparison.Ordinal))

            {

                return true;

            }

        }

 

        return false;

    }

 

    //Vérification de l'origine du jeton

    private bool IsIssuerTrusted(string token)

    {

        Dictionary<string, string> tokenValues = this.GetNameValues(token);

 

        string issuerName;

 

        tokenValues.TryGetValue(this.issuerLabel, out issuerName);

 

        if (!string.IsNullOrEmpty(issuerName))

        {

            if (issuerName.Equals(this.trustedTokenIssuer))

            {

                return true;

            }

        }

 

        return false;

    }

 

    //Vérification de la signature du jeton

    private bool IsHMACValid(string swt, byte[] sha256HMACKey)

    {

        string[] swtWithSignature = swt.Split(new string[] { "&" + this.hmacSHA256Label + "=" }, StringSplitOptions.None);

 

        if ((swtWithSignature == null) || (swtWithSignature.Length != 2))

        {

            return false;

        }

 

        HMACSHA256 hmac = new HMACSHA256(sha256HMACKey);

 

        byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));

 

        string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));

 

        return locallyGeneratedSignature == swtWithSignature[1];

    }

 

    //Vérification de l'expiration du jeton

    private bool IsExpired(string swt)

    {

        try

        {

            Dictionary<string, string> nameValues = this.GetNameValues(swt);

            string expiresOnValue = nameValues[this.expiresLabel];

            ulong expiresOn = Convert.ToUInt64(expiresOnValue);

            ulong currentTime = Convert.ToUInt64(GenerateTimeStamp());

 

            if (currentTime > expiresOn)

            {

                return true;

            }

 

            return false;

        }

        catch (KeyNotFoundException)

        {

            throw new ArgumentException();

        }

    }

}

A ce stade, si vous compilez et déployez votre paquet dans Windows Azure, chacune de vos requêtes devrait recevoir une réponse avec un code d’erreur 401. Ce qui est le résultat attendu, vu que vous ne vous êtes toujours pas authentifié auprès du service de contrôle d’accès ACS.

Nous allons donc créer un rapide client de test pour mettre en œuvre un cas d’utilisation de bout en bout. C’est l’objet de la quatrième partie de ce tutoriel.

Tutoriel Contrôle de l'accès au service de données OGDI - Partie 3.docx