Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Gérer l’authentification et les authorisations avec les claims

Poster un commentaire

Il est important de comprendre ce que sont les claims (revendications) pour une raison simple: les outils d’authentification modernes se basent dessus.

En tant que développeur nous y serons inévitablement confrontés.

Je vais essayer de synthétiser tout ce que j’ai pu apprendre à ce sujet. Après tout les claims sont déjà dans nos applications, alors apprenons à manipuler le code qui va avec.

Ils sont déjà là, mais comment font t’ils?

La question des claims a été brièvement abordée dans un article sur les jetons JWT et nous avons constaté dans un autre article sur WS-Federation qu’ils sont effectivement déjà dans notre code sans que l’on s’en rend forcément compte.

Je vais justement repartir de là et en particulier avec cette copie d’écran:

2014-11-06_22-32-39

Il s’agissait de montrer à quoi ressemble un principal dans le contexte d’une authentification fédérée. Rappelons que dans le monde de l’authentification, un principal représente le contexte de sécurité d’un utilisateur. Il s’agit d’une classe qui implémente IPrincipal.

 

On voit apparaître des classes que d’habitude on ne voit pas à cet endroit qui montre que Microsoft a modifié la modèle d’héritage depuis .Net 4.5:

2014-11-09_16-02-17

Les différentes instances de IPrincipal fournies par .Net héritent non plus directement de IPrincipal, mais de la classe ClaimsPrincipal. Et s’il vous arrive de créer des principaux personnalisés il faudra faire pareil pour que ça fonctionne comme prévu.

De la même façon pour IIdentity un petit nouveau apparaît dans la famille:

2014-11-09_16-18-58

C’est de cette façon que les claims peuvent s’intégrer de façon transparente dans notre vie de développeur.

Si cette intégration est aussi simple, c’est parce que sur le fond les claims sont nativement compatibles avec les précédents modèles de sécurités. Un login/mot de passe, c’est juste deux claims. Le modèle des claims présente l’avantage de proposer un format unifié, c’est à dire indépendant de l’origine des claims.

WIF propose une panoplie complète de SecurityTokenService qui sont entre autre fonction chargés de transformer les différents protocoles d’authentification en claims.

La classe Claim et ses amis

Les claims sont une classe avec une structure type/valeur. Mais attention, cette structure n’est pas assimilable à un dictionnaire car un même type peut apparaître plusieurs fois dans la liste des claims.

Type est le type du claim, par exemple le nom de l’utilisateur, son email, la pointure de ses chaussures… Value est la valeur de ce type.

On peut créer un claim de façon très simple avec la classe Claim:

Claim claim = new Claim("Name", "Amethyste");

Le premier paramètre est le type du claim, le deuxième la valeur du type. Il existe des types par défaut exposés via ClaimTypes. On aurait pu écrire ceci:

Claim claim = new Claim(ClaimTypes.Name, "Amethyste");

Si vous faites F12 pour voir le source:

2014-11-09_17-11-32

Vous observez que le nom est préfixé par un espace de nom afin de le rendre unique. Il n’est pas impossible en effet que Name soit un claim dans un sens entièrement différent dans un autre contexte. D’une façon générale, ajouter un espace de nom au nom des claims est une bonne pratique.

On peut également instancier un ClaimsIdentity et un ClaimsPrincipal:

IList<Claim> claimCollection = new List<Claim>
{
    new Claim(ClaimTypes.Name, "Amethyste")
  , new Claim(ClaimTypes.Country, "France")
  , new Claim(ClaimTypes.Country, "Belgique")
  , new Claim(ClaimTypes.Role, "ITSuper héros")
};

ClaimsIdentity claimsIdentity = new ClaimsIdentity(claimCollection);
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);

Remarquez que le claim Country a plusieurs valeurs.

Normalement IIdentity.IsAuthenticated est mis à true par défaut. Depuis .Net 4.5, false est maintenant la valeur par défaut. On doit maintenant ajouter:

IList<Claim> claimCollection = new List<Claim>
{
new Claim(ClaimTypes.Name, "Amethyste")
    , new Claim(ClaimTypes.Country, "France")
    , new Claim(ClaimTypes.Gender, "M")
    , new Claim(ClaimTypes.Role, "Super héros")
};

ClaimsIdentity claimsIdentity = new ClaimsIdentity(claimCollection, "chef j'ai glissé");
ClaimsPrincipal principal = new ClaimsPrincipal(claimsIdentity);

Console.WriteLine(claimsIdentity.AuthenticationType); // chef j'ai glissé
Console.WriteLine(claimsIdentity.IsAuthenticated); // true

La valeur de AuthenticationType est juste une chaîne.

Vous avez deux solutions pour obtenir le ClaimsPrincipal en cours:

ClaimsPrincipal currentClaimsPrincipal = Thread.CurrentPrincipal as ClaimsPrincipal;
ClaimsPrincipal currentClaimsPrincipal = ClaimsPrincipal.Current;

D’autres fonctions utilitaires sont à notre disposition:

if (principal.HasClaim(ClaimTypes.Country, "France"))
{
    Claim claim = principal.FindFirst(ClaimTypes.Country);
}
    IEnumerable<Claim> claims1 = principal.FindAll(ClaimTypes.Country);
    IEnumerable<Claim> claims2 = principal.FindAll(c =>
               {
                   return c.Type == ClaimTypes.Country;
               });

Claims1 et claims2 contiennent la même chose.

 

Transformation des claims

Le pipeline de traitement des claims est rappelé ici:

2014-11-09_20-08-05

Ce schéma vient de ce site:

http://stack247.wordpress.com/2013/04/18/net-4-5-claims-and-tokens-as-standard-model/

Commentons un peu les deux premiers blocs:

  1. L’application reçoit le jeton de sécurité dans un format quelconque comme XML…
  2. Le jeton est pris en charge par une instance de SecurityTokenService qui le désérialise
  3. Le STS lance également sa méthode ValidateToken pour le valider. La méthode renvoie un ClaimsPrincipal
  4. Le ClaimsPrincipal est ensuite transformé… ou pas c’est selon

Pourquoi voudrait t’on transformer un claims tout frais reçu?

Voici à quoi ressemble typiquement un claim dans un contexte d’authentification Windows:

2014-11-09_20-27-51

Dans la majorité des scénarios ils est improbable que l’on ai besoin de tout ceci et un nettoyage s’imposera. On peut aussi imaginer que l’application elle-même souhaite ajouter des claims comme l’adresse de l’utilisateur.

Un dernier scénario intéressant est lorsque l’on souhaite utiliser un principal personnalisé.

 

On ajoute System.IdentityModel et System.IdentityModel.Services dans les références.

Les transformations s’installent au sein d’une instance de ClaimsAuthenticationManager, La méthode que l’on surcharge est Authenticate. Cette méthode accepte un ClaimsPrincipal et retourne un autre ClaimsPrincipal , le claim transformé.

public sealed class CustomClaimsTransformer : ClaimsAuthenticationManager
{
    public override ClaimsPrincipal Authenticate(string resourceName, ClaimsPrincipal incomingPrincipal)
    {
        string nameClaimValue = incomingPrincipal.Identity.Name;

        if (string.IsNullOrEmpty(nameClaimValue))
        {
            throw new SecurityException("Il est où son nom?");
        }
        return CreatePrincipal(nameClaimValue);
    }

    private ClaimsPrincipal CreatePrincipal(string userName)
    {
        List<Claim> claimsCollection = new List<Claim>();

        Claim claim = new Claim(ClaimTypes.Name, userName);
        claimsCollection.Add(claim);

        claim = new Claim(ClaimTypes.Email, "amethyste16@hotmail.com");
        claimsCollection.Add(claim);

        claim = new Claim(ClaimTypes.DateOfBirth, "22/02/1964", ClaimValueTypes.Date);
        claimsCollection.Add(claim);

        claim = new Claim(ClaimTypes.Country, "France");
        claimsCollection.Add(claim);

        ClaimsIdentity identity = new ClaimsIdentity(claimsCollection, "on sait qui il est, c'est bon");
        return new ClaimsPrincipal(identity);
    }
}

On aurait également pu en profiter pour instancier un ClaimsPrincipal personnalisé.

Notez au passage la possibilité de fournir un type pour la valeur à l’aide de ClaimValueTypes.

 

Une bonne pratique consiste à instancier un nouveau claim plutôt que de faire le ménage dans celui reçu. La propriété resourceName indique juste un contexte. C’est à vous de définir le contenu.

On peut ensuite placer ce code de tests dans la méthode Main:

WindowsPrincipal incomingPrincipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());

ClaimsAuthenticationManager authenticationManager = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;
Thread.CurrentPrincipal = authenticationManager.Authenticate("none", incomingPrincipal);

La première ligne obtient un ClaimsPrincipal avec tous les claims que l’on a vu précédemment. On obtient ensuite l’instance en cours du gestionnaire d’authentification et on s’en sert pour obtenir le claim transformé.

Comment le code sait t’il qu’il doit appeler CustomClaimsTransformer? Il faut ajouter cette déclaration dans le fichier de configuration (à adapter selon votre contexte):

<configSections>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0,  Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
</configSections>

<system.identityModel>
    <identityConfiguration>
        <claimsAuthenticationManager type="ConsoleApplication1.CustomClaimsTransformer,ConsoleApplication1"/>
    </identityConfiguration>
</system.identityModel>

Et puis tester:

2014-11-10_23-13-52

On peut également déclarer le gestionnaire personnalisé par code:

FederatedAuthentication.FederationConfigurationCreated += FederatedAuthentication_FederationConfigurationCreated;

Puis le gestionnaire d’événement:

void FederatedAuthentication_FederationConfigurationCreated(object sender, System.IdentityModel.Services.Configuration.FederationConfigurationCreatedEventArgs e)
{
    e.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager = new CustomClaimsTransformer();
}

 

Dans une application Web on pourrait ajouter ce code dans un module ou bien dans Global.asax lors de l’événement PostAuthenticationRequest qui est levé juste après l’authentification:

protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
    var context = ((HttpApplication)sender).Context;

    if (FederatedAuthentication.SessionAuthenticationModule != null &&
        FederatedAuthentication.SessionAuthenticationModule.ContainsSessionTokenCookie(context.Request.Cookies))
    {
        // ce n'est pas une authentification fédérée (par claims)
        return;
    }

    var authenticationManager = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthenticationManager;

    if (authenticationManager != null)
    {
        var transformedPrincipal = authenticationManager.Authenticate("none", context.User as ClaimsPrincipal);

        context.User = transformedPrincipal;
        Thread.CurrentPrincipal = transformedPrincipal;
    }
}

On commence par vérifier que l’on a un cookie d’authentification fédérée. Si c’est le cas on continue en recherchant le gestionnaire d’autorisation. Celui-ci lance l’authentification puis génère notre nouveau ClaimsPrincipal qui prend la place du principal d’origine.

Contrôle de l’accès au code via les claims

Avant les claims on contrôlait les accès au code en interrogeant IsInRole ou en posant des attributs comme:

[PrincipalPermission(SecurityAction.Demand, Role = "Client")]

Si on n’a moins de raisons d’utiliser IsInrole (encore que l’on dispose d’un type Role) on a encore dans notre palette des déclarations:

[ClaimsPrincipalPermission(SecurityAction.Demand, Operation="Add", Resource="Basket")]

Qui pourrait décorer une méthode d’ajout d’un élément dans un panier.

Cet exemple est intéressant car il est caractéristique de ce qui fait la différence entre les rôles et les claims. Les claims permettent une granulométrie très fine des autorisations. On est pas par exemple obligé de définir un rôle particulier pour chaque action que l’on peut faire dans un panier. On ajoute juste ou non un claim.

 

Tout comme on avait un gestionnaire d’authentification, on a un gestionnaire d’autorisation: ClaimsAuthorizationManager. Cette classe permet de séparer complètement la logique de gestion des autorisations de la déclaration elle-même comme on le faisait avec IsInRole.

Voyons un exemple pour clarifier les choses qui fait suite au code déjà écrit précédemment pour la démo avec l’application MVC.

public sealed class CustomAuthorisationManager : ClaimsAuthorizationManager
{
    public override bool CheckAccess(AuthorizationContext context)
    {
        string resource = context.Resource.First().Value;
        string action = context.Action.First().Value;

        if (resource == "Basket")
        {
            if (action == "Add" && context.Principal.HasClaim(ClaimTypes.Country, "France"))
            {
                 // seuls les clients français peuvent acheter sur le site
                 return true;
            }
        }

         return false;
    }
}

On complète le fichier web.config avec:

2014-11-11_17-12-06

Note: ne surtout pas oublier cette déclaration. Le gestionnaire par défaut renvoie toujours true ce qui risque d’être dangereux en cas d’oubli.

 

On ajoute ensuite une action de test dans le contrôleur de Home.

[ClaimsPrincipalPermission(SecurityAction.Demand, Operation = "Add", Resource = "Basket")]
public ActionResult AddBasket(int? idItem)
{
    return View();
}

[ClaimsPrincipalPermission(SecurityAction.Demand, Operation = "Delete", Resource = "Basket")]
public ActionResult DeleteBasketItem(int? idItem)
{
    return View();
}

Placez un point d’arrêt dans le gestionnaire et lancez l’action AddBasket. Vous devriez constater que l’on passe bien par le gestionnaire. Essayez maintenant avec DeleteBasketItem:

2014-11-11_17-20-05

Il est possible d’avoir des scénarios d’autorisation complexe ou bien de préférer la programmation impérative. On peut obtenir le gestionnaire courant facilement:

ClaimsAuthorizationManager authManager = FederatedAuthentication.FederationConfiguration.IdentityConfiguration.ClaimsAuthorizationManager;

On peut alors poursuivre en écrivant par exemple du code spécifique comme:

Collection<Claim> resourceClaims = new Collection<Claim>();
resourceClaims.Add(new Claim("resource1", "value1"));
resourceClaims.Add(new Claim("resource2", "value2"));

Collection<Claim> actionClaims = new Collection<Claim>();
actionClaims.Add(new Claim("action1", "insert"));
actionClaims.Add(new Claim("action2", "update"));

AuthorizationContext authContext = new AuthorizationContext(ClaimsPrincipal.Current, resourceClaims, actionClaims);
bool allowed = authManager.CheckAccess(authContext);

 

En MVC on est plutôt habitué à utiliser l’attribut Authorize. A ce jour il ne gère pas les ressources/action, uniquement les rôles:

[Authorize(Roles="IT")]

Il n’est bien sûr pas bien compliqué d’écrire un attribut personnalisé qui hérite de AuthorizeAttribute, mais le travail a déjà été fait par Thinktecture sous la forme d’un package Nuget.

http://thinktecture.github.io/

On dispose alors de ClaimsAuthorizeAttribute qui répond à nos besoins. Je recommande vraiment cette approche qui dans un contexte MVC est plus adaptée que ClaimsPrincipalPermission il est en particulier mieux armé pour être testé unitairement et surtout ne lève pas une exception au lieu de retourner un code Http.

Vous pouvez avoir plus d’information ici:

http://leastprivilege.com/2012/10/26/using-claims-based-authorization-in-mvc-and-web-api/

 

Bibliographie

L’idée de cet article me vient d’un article d’Andras Nemes qui doit être le développeur le plus organisé que je connaisse. Figurez vous qu’il a un planning pour tous ses articles à venir!!!

A titre informatif, si j’ai publié 100 articles, j’en ai 40 encore en draft et n’ai aucune idée de quand je les terminerai!

 

 

 

 

 

 

 

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s