Licence CC BY

Sécurité et gestion des utilisateurs

Dernière mise à jour :

Jusqu’à maintenant, nous avons géré notre site sans nous soucier d’un problème majeur : l’utilisateur doit-il être enregistré pour envoyer des postes ou des commentaires ?

C’est là qu’intervient cette partie. Nous vous guiderons pas à pas pour mettre en place une gestion complète des autorisations liées à chaque utilisateur. Les deux mots clef de cette partie sont authentification et autorisation.

Authentifier un utilisateur, le login et le mot de passe

Lorsque vous naviguez sur un site web, il est fort probable que vous tombiez sur un formulaire qui vous demande votre nom d’utilisateur (le login, qu’on peut traduire par identifiant en français) et votre mot de passe1.

Cette fonctionnalité est très simple à mettre en place : par défaut ASP.NET vous fournit toutes les bases pour créer les modèles associés à la fonctionnalité d’authentification par mot de passe.

Le saviez vous? Le mot "authentification" se distingue du mot "identification" par le fait qu’une authentification assure que la personne est bien celle qu’elle prétend être. À l’opposé, une simple identification permet à la personne de déclarer qui elle est. C’est pour ça que vous avez un "identifiant" et un "mot de passe". Le premier vous permet de déclarer votre identité, le second, comme il n’est connu que de vous (même pas de la plateforme) permet de vous authentifier.

Lorsque vous avez sélectionné le type de projet (…) Visual Studio a immédiatement créé un modèle, un contrôleur et quelques vues.

Je vous laisserez décortiquer les vues mais il est important de comprendre comment fonctionne le contrôleur et le modèle de données.

# Le contrôleur

Le modèle de code Visual Studio a généré un ensemble de méthodes qui sont là pour implémenter de manière automatique toutes les fonctionnalités habituelles d’une authentification par login et mot de passe.

  • l’inscription, qui se dit register en anglais,
  • l’authentification en elle-même,
  • la possibilité de récupérer son mot de passe en cas de perte,
  • la désinscription,
  • et nous le verrons en fin de chapitre : l’utilisation de fournisseurs externes tels que google, twitter… pour se connecter.

Le contrôleur se trouve dans Controllers\AccountController.cs. Il contient un certains nombre de méthodes simples (return View();) qu’il vous appartiendra de modifier si cela vous semble nécessaire : elles ne font qu’afficher la page qui contient le formulaire.

Les autres sont un peu plus complexes pour deux raisons :

  • elles utilisent à volonté le principe des méthodes asynchrones ;
  • elles se basent sur le composant Microsoft.AspNet.Identity.Owin, qui est le composant maître de ce chapitre.
using System;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using AuthDemo.Models;

namespace AuthDemo.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        private ApplicationSignInManager _signInManager;
        private ApplicationUserManager _userManager;

        public AccountController()
        {
        }

        public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager )
        {
            UserManager = userManager;
            SignInManager = signInManager;
        }

        public ApplicationSignInManager SignInManager
        {
            get
            {
                return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
            }
            private set 
            { 
                _signInManager = value; 
            }
        }

        public ApplicationUserManager UserManager
        {
            get
            {
                return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
            }
            private set
            {
                _userManager = value;
            }
        }
        //
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // Ceci ne comptabilise pas les échecs de connexion pour le verrouillage du compte
            // Pour que les échecs de mot de passe déclenchent le verrouillage du compte, utilisez shouldLockout: true
            var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
            switch (result)
            {
                case SignInStatus.Success:
                    return RedirectToLocal(returnUrl);
                case SignInStatus.LockedOut:
                    return View("Lockout");
                case SignInStatus.RequiresVerification:
                    return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
                case SignInStatus.Failure:
                default:
                    ModelState.AddModelError("", "Tentative de connexion non valide.");
                    return View(model);
            }
        }
/// ...
    }
}
Le code de login est asynchrone et utilise le composant Microsoft.AspNet.Identity.Owin comme _loginManager

Les versions les plus récentes d’ASP.NET ont introduit le composant "Owin" qui a pour but de fournir des identités et de les authentifier lorsque cela est nécessaire.

L’idée derrière ce composant est de fournir une abstraction assez élevée dans la manière de gérer l’authentification afin que les migrations soient facilitées:

  • en v1 on ne propose qu’un login/mot de passe simplifié,
  • en v2 on propose l’authentification par google, twitter et facebook,
  • en v3 on utilise un service de SSO pour que tous nos sites soient unifiés.

Sans ce composant, c’est le contrôleur complet qui doit être redéveloppé à chaque fois. Avec Owin, vous n’avez qu’à développer (si .NET ne le possède pas déjà) un connecteur (en anglais vous trouverez le mot Adapter voire parfois Bridge même si il y a des nuances entre les deux) entre votre méthode d’authentification et Owin et le tour est joué. Mieux, vous pouvez même, par soucis de compatibilité proposer deux systèmes en parallèle.

Nous ne personnaliserons pas ce contrôleur dans ce tutoriel, je terminerai simplement cette partie en portant à votre attention la présence du constructeur public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager ). Ce dernier n’est pas utilisé par ASP.NET il n’est présent que pour vous faciliter la tâche lorsque vous écrirez des tests unitaires.

En effet à partir de ce moment là, il vous suffira d’écrire un mock d’objet ApplicationSignInManager et de l’envoyer dans le contrôleur pour avoir pouvoir tester votre contrôleur comme il se doit. Nous reviendront plus tard —et en vidéo, je vous le promet— sur les tests qu’on peut faire sur une application web.

Le modèle de données

Lorsque vous essayez de trouver le modèle de données lié aux utilisateurs, vous pouvez être un peu rassuré : en effet nous retombons sur ce que nous avons eu l’habitude de voir dans notre cours :

  • un fichier IdentityModels.cs qui va être l’objet de ce paragraphe avec les représentations C# de la base de données ;
  • un fichier AccountViewModels.cs qui contient tous les ViewModel qui permettent d’enregistrer un utilisateur ;
  • un fichier ManageViewModel.cs qui sera dirigé vers les tâche d’administration ou simplement les modifications de son compte par l’utilisateur.

Si nous ne détaillerons pas les fichier de ViewModel, il est important de garder en mémoire que chaque modification des IdentityModels entraînera une modification des ViewModels et de leur template associé.

Le fichier IdentityModels.cs contient par défaut deux éléments :

  • une classe ApplicationUser qui hérite de IdentityUser ;
  • un contexte de base de données.

Le contexte est un peu spécial car il ne se contente pas d’hériter de DbContext mais de IdentityDbContext. Ce qui lui ajoute des méthodes supplémentaires pour interroger la base de données à propos des utilisateurs, à savoir :

public bool RequireUniqueEmail { get; set; }
public virtual IDbSet<TRole> Roles { get; set; }
public virtual IDbSet<TUser> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder);
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items);
les ajouts de IdentityDbContext

Pour ce qui est de notre utilisateur, il s’agit d’une classe simple qui hérite d’un objet déjà défini part ASP.NET et qui est lui-même personnalisable :

Diagramme de classe des rôles par défaut
Diagramme de classe des rôles par défaut

Les rôles c’est simplement l’ensemble des niveaux d’autorisation qu’il y a dans votre application.

Si on fait le lien avec zeste de savoir vous avez "trois niveaux" :

  • le membre de la communauté,
  • le validateur/modérateur,
  • l’administrateur technique de la plateforme.

Il suffirait alors de créer trois Role dans notre base de données qui seraient Community, Staff, Technical admin et on pourrait dès lors simplement promouvoir un membre vers staff en faisant _userManager.AddToRole(user.UserName, "Staff"); dans notre contrôleur et le tour serait joué !

Par défaut, notre utilisateur ne possède que des informations qui permettent de l’identifier et de l’authentifier. C’est donc cet objet qui devra être lié aux objets tel que les articles de blog. Un auteur d’article est un ApplicationUser.

Néanmoins, ce genre d’objet n’est pas vraiment suffisant. Par exemple, où stocke-t-on l’avatar du membre ? Sa date de naissance (si on en a besoin, n’oubliez pas que c’est une donnée personnelle) ?

Deux philosophies existent alors :

  • soit vous augmentez votre ApplicationUser de toutes les informations nécessaires,
  • soit vous créez un second objet qui pourrait être UserProfile par exemple où toutes les informations seraient ajoutées et qui serait créé en même temps que l’objet ApplicationUser.

La méthode conseillée par ASP.NET est la première, car en plus de convenir à 99% des cas, elle est plus simple à maintenir.

Le choix de la méthode est très important. Si vous pensez que votre application sort des 99% évoqués au dessus, n’hésitez pas à venir poster la question sur le forum avec le tag [asp.net], nous serons heureux de vous répondre.

Pour ce qui est des rôles, Je vous propose de laisser les choses telles que ASP vous le propose mais sachez qu’il est tout à fait possible de les modifier. Imaginons que nous voulons ajouter un badge à un rôle (comme le badge staff) :

class RoleWithAvatar: IdentityRole:
{
public RoleWithAvatar(): super(){
    AvatarUrl = "DefaultAvatarUrl";
};
public RoleWithAvatar(string name): super(name){
   AvatarURL = "DefaultAvatarUrl";
}
public RoleWithAvatar(string name, string url): super(name){
   AvatarURL = url;
}
    public string AvatarURL {get; set;}
}
Notre nouveau groupe

Il suffira ensuite de changer légèrement notre ApplicationDbContext pour lui indiquer que nous allons changer le format des rôles :

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, RoleWithAvatar, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {

        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }
Un petit changement dans les rôles.

Si dans un de vos contrôleurs vous avez besoin d’accéder aux rôles, il suffit d’utiliser la ligne suivante RoleManager roleManager = new RoleManager<IdentityRole>(new RoleStore<RoleWithAvatar>(context));

[Mini TP] Restreindre les accès

Peut être vous souvenez-vous de ce conseil : "Il est fortement conseillé de mettre [Authorize]sur toutes les classes de contrôleur puis de spécifier les méthodes qui sont accessibles publiquement à l'aide de[AllowAnonymous]`". C’est maintenant que nous allons pleinement le mettre en œuvre.

Comme nous sommes arriver assez loin dans ce tutoriel, vous devriez, avec un minimum d’information réussir le mini tp qui arrive.

Les attributs Authorize et AllowAnonymous sont des filtres d’autorisation. Nous reverrons plus tard le fonctionnement des filtres en général, mais l’idée c’est que ces attributs permettent d’appliquer un certains nombres d’actions avant que le code métier ne soit exécuté. Ils vérifient avec le connecteur Owin si l’utilisateur qui est en train de visité la page a bien ouvert une session authentifiée.

Notre but sera de proposer notre propre attribut d’autorisation qui fera deux vérifications :

  • premièrement le visiteur est bien authentifié (comme AuthorizeAttribute)
  • deuxièmement le visiteur appartient à un groupe qu’on aura défini (par exemple Staff)

L’idée serait qu’à la fin on ait une action du style :

        // POST: /Manage/BanUser
        [HttpPost]
        [ValidateAntiForgeryToken]
        [AuthorizedFor("Staff")]
        public async Task<ActionResult> BanUser(BanUserViewModel model)
        {
            //etc.
        }

En ce qui concerne les IAuthorizeAttribute, ils nous propose de surcharger une méthode appelée OnAuthentication qui ne renvoie rien. Pourtant, elle peut modifier son paramètre filterContext pour y spécifier l’attribut Result lorsque l’autorisation a échouée. Il suffira de lui dire que ce résultat est un HttpUnauthorizedResult.

Si vous n’y arrivez pas n’hésitez pas à venir sur le forum avant de regarder la correction.

public AuthorizedForAttribute:AuthorizeAttribute
{
    public AuthorizedForAttribute(string group) super(){
        Role = group;
    }
    public string Role{ get; private set;}
    public override void OnAuthorization(AuthorizationContext filterContext){
         super.OnAuthorization(filterContext);
         if(filterContext.Result != null && filterContext.Result instanceof HttpUnauthorizedResult){
            // on sait que ce n'est pas autorisé
            return;
          }
         if(!filterContext.HttpContext.User.IsInRole(Role)){
              filterContext.Result = new HttpUnauthorizedResult();
         }
    }
}
Notre attribut, ce n’est qu’une solution possible.

  1. Et n’oubliez pas de bien définir votre mot de passe.


Vous voilà mieux armé pour gérer les utilisateurs de votre site. Dans une prochaine version de ce tutoriel vous aurez droit à une vidéo de démonstration vous permettant d’ajouter une authentification via les compte facebook, twitter ou encore google.