Site ASP.NET MVC : sécuriser l’accès de ses utilisateurs

Brouillon

À venir

0 commentaire

Aujourd’hui encore, la sécurité est un enjeu majeur en informatique et plus spécialement l'identification des utilisateurs. Elle est fondamentale pour gérer les données auxquelles un utilisateur peut et doit avoir accès.

image

Mode d’authentification et hachage du mot de passe

La manière la plus répandue d’accéder à une application reste la combinaison identifiant/mot de passe que l’on renseigne via un formulaire d’authentification. Malheureusement ce système est imparfait car il est possible de casser n’importe quel mot de passe… tout est une question de temps.

Le temps nécessaire pour casser un mot de passe dépend de deux choses : la complexité du mot de passe et le mode de stockage de celui-ci. Donc améliorer la sécurité revient à influer sur ces deux points.

Complexité du mot de passe

La complexité du mot de passe dépend de l'utilisateur, c'est bien souvent lui qui choisit son mot de passe. Depuis plus de 2 ans maintenant, la CNIL (Commission Nationale de l'Informatique et des Libertés) prodigue sur son site différents conseils concernant la complexité d’un mot de passe :

  • 12 caractères et 4 types différents : des minuscules, des majuscules, des chiffres et des caractères spéciaux,
  • Un compte, un mot de passe,
  • Etc…

Si vous voulez avoir l’ensemble des recommandations faites par la CNIL, vous les trouverez dans le lien suivant : https://www.cnil.fr/fr/les-conseils-de-la-cnil-pour-un-bon-mot-de-passe

Néanmoins l’entreprise SplashData fournit une liste des pires mots de passe encore utilisés en 2018 que vous pouvez trouver ici : https://www.teamsid.com/100-worst-passwords/.

Donc il est fortement conseiller d’ajouter un certain nombre de règles contraignant l’utilisateur lors de l’initialisation de son mot de passe.

Mode de stockage du mot de passe

Le mode de stockage dépend de l’algorithme de hachage que vous allez utiliser pour votre mot de passe, pour n’en citer que 3 :

  • L’algorithme PBKDF2 avec une fonction de hachage SHA256,
  • L’algorithme Argon2, gagnant du concours 2015 Password Hashing Competition,
  • L’algorithme Bcrypt qui est spécifiquement conçu comme stockage de mots de passe à long terme.

Par défaut pour le Framework Identity, l’algorithme est la fonction de hachage PBKDF2 avec HMAC-SHA256, un sel 128 bits, une sous-clé 256 bits et 1000 itérations. Le nombre d’itérations permet de rendre plus difficiles les attaques contre les empreintes de mots de passe, malheureusement le nombre d’itérations n’est configurable qu’à partir de la version 3 du Framework Identity (pour ASP.Net Core). Avec un nombre d’itérations aussi faible, il est préférable de changer ou du moins d’initialiser soit même l’algorithme pour le Framework Identity si l’on n’utilise pas ASP.Net Core.

Cet article a pour objectif la mise en place d’un site ASP.Net MVC Identity qui utilise l'authentification OWIN configuré de telle sorte que le stockage soit fait avec l’algorithme Bcrypt et que la complexité respecte la première règle de la CNIL : 12 caractères et 4 types différents (des minuscules, des majuscules, des chiffres et des caractères spéciaux).

Authentification via formulaire

image

Voici comment fonctionne sous ASP.NET l'authentification par formulaire :

1. Un utilisateur veut accéder à une page sécurisée de votre site Asp.Net,

2. Votre site ASP.NET vérifie si le client dispose d'un cookie d'authentification. Si l'utilisateur n'est pas authentifié alors il est redirigé vers la page de login,

3. L'utilisateur saisit son identifiant et son mot de passe,

4. Les informations d'authentification sont vérifiées. Si l'utilisateur n'est pas authentifié alors un message d'accès refusé est affiché,

5. Les informations d'authentification ont été validées, un cookie d'authentification est généré, si l'utilisateur est autorisé par ASP.NET alors il accède à la page demandée.

Création du site ASP.Net MVC

Créez un nouveau projet dans Visual Studio de type Application Web ASP.NET (.NET Framework). Nommez-le comme vous le souhaitez et sur l'écran suivant, sélectionnez le modèle MVC sans authentification.

Une fois le projet ASP.NET MVC terminé, vous devrez ajouter de nouveaux packages NuGet :

Microsoft.AspNet.Identity.EntityFramework

  • Package qui contient l’implémentation d’Entity Framework pour Identity ASP.NET qui conserve les données de Identity ASP.NET et le schéma de SQL Server.

Microsoft.AspNet.Identity.Owin

  • Package qui contient un ensemble de classes d’extension OWIN pour gérer et configurer le middleware OWIN d’authentification pour être consommés par les packages de base d’identité ASP.NET

Microsoft.Owin.Host.SystemWeb

  • Package qui contient un serveur OWIN. Les applications basées sur OWIN s’exécutent sur IIS à l’aide du pipeline de demande ASP.NET.

Configuration de OWIN avec Identity

1. Dans le dossier App_Start de votre projet, créez une nouvelle classe nommée AuthConfig.

clip_image001[4]

2. Dans le fichier Web .config du site, ajoutez une ConnectionString vers la base de données qui stockera les données de Identity, nommée la IdentityConnection.

<connectionStrings>
  <add name="IdentityConnection" connectionString="{A DEFINIR}" />
</connectionStrings>

3. Dans la classe AuthConfig, créez la méthode Configuration

  • Récupération de la ConnectionString de la base Identity
  • Initialisation du contexte OWIN avec Identity
  • Initialisation du UserStore
  • Configuration du UserManager
  • Initialisation SignIn Manager
  • Configuration du cookie de connexion
public void Configuration(IAppBuilder app)
{
    //Récupérationde la connectionString de la base Identity
    string connectionString = ConfigurationManager.ConnectionStrings["IdentityConnection"].ConnectionString;

    #region Owin Configuration
    //Initialisation du context OWIN avec Identity
    app.CreatePerOwinContext(() => new IdentityDbContext(connectionString));
    //Initialisation UserManager
    app.CreatePerOwinContext<UserStore<IdentityUser>>((opt, cont) => new UserStore<IdentityUser>(cont.Get<IdentityDbContext>()));
    //Configuration du UserManager
    app.CreatePerOwinContext<UserManager<IdentityUser>>(
        (opt, cont) =>
        {
            var usermanager = new UserManager<IdentityUser>(cont.Get<UserStore<IdentityUser>>()){ };    
           
           return usermanager;
        });
    //Initialisation SignIn Manager  
    app.CreatePerOwinContext<SignInManager<IdentityUser, string>>(
        (opt, cont) =>
            new SignInManager<IdentityUser, string>(cont.Get<UserManager<IdentityUser>>(), cont.Authentication));
    #endregion Owin Configuration

    #region Cookie Configuration
    // Configuration du cookie de connexion
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        CookieName = "MyApplication.ApplicationCookie",
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Authentification/Login"),
        Provider = new CookieAuthenticationProvider
        {
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<UserManager<IdentityUser>, IdentityUser>(
                validateInterval: TimeSpan.FromSeconds(3),
                regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie))
        }
    });
    #endregion Cookie Configuration

}

La dernière étape de configuration consiste à indiquer dans le fichier WebConfig dans quelle classe OWIN doit trouver sa configuration lors du démarrage du site.

<add key="owin:appStartup" value="{VOTRE NAMESPACE}.AuthConfig" />

Voilà il ne reste plus qu’à implémenter une page d’authentification, dont on a défini le chemin lors de la configuration du cookie de connexion, et une page d’inscription

Implémentation de la page d’authentification

1. Dans le dossier Controllers de votre projet, créez un nouveau Controller nommé AuthentificationController

2. Dans la classe AuthentificationController, ajoutez les membres UserManager et SignInManager de OWIN

 protected UserManager<IdentityUser> UserManager => HttpContext.GetOwinContext().Get<UserManager<IdentityUser>>();

 protected SignInManager<IdentityUser, string> SignInManager => HttpContext.GetOwinContext().Get<SignInManager<IdentityUser, string>>();

3. Dans la classe AuthentificationController, créez la méthode Login pour l’appel du formulaire d’authentification

[HttpGet]
[AllowAnonymous]
public ActionResult Login()
{
      return View();
}

4. Dans le dossier Models de votre projet, créez une nouvelle classe nommée LoginViewModel

public class LoginViewModel
{
        [Required]
        [Display(Name = "E-mail")]
        public string Login { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Mot de passe")]
        public string Password { get; set; }
}

5. Dans la classe AuthentificationController, créez la méthode Login pour le retour du formulaire d’authentification

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        //Si l'utilisateur n'existe pas
        var user = await UserManager.FindByNameAsync(model.Login);
        if (user == null)
        {
            ModelState.AddModelError(string.Empty, "Le nom d'utilisateur ou le mot de passe est incorrect.");
            return View(model);
        }
        //Si le mot de passe est incorrect
        if (await UserManager.CheckPasswordAsync(user, model.Password))
        {
            if (await UserManager.IsLockedOutAsync(user.Id))
            {
                ModelState.AddModelError(string.Empty, "Compte bloqué");
                return View(model);
            }

            //Vérification du login pwd
            var signInStatus = await SignInManager.PasswordSignInAsync(model.Login, model.Password, false, true);

            switch (signInStatus)
            {
                case SignInStatus.Success:
                    return RedirectToAction("Index","Home");

                //Si l'utilisateur n'est pas reconnu
                default:
                    ModelState.AddModelError(string.Empty, "Le nom d'utilisateur ou le mot de passe est incorrect.");
                    return View(model);
            }
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Le nom d'utilisateur ou le mot de passe est incorrect.");
            return View(model);
        }
    }
    return View();
}

Il ne reste plus qu’à créer la vue associée à la méthode Login

@model MonNamespace.Models.LoginViewModel

@{
    ViewBag.Title = "Login";
}

<h2>Authentification</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Connectez vous!!</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Login, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Login, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Login, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Se Connecter" class="btn btn-default" />
            </div>
        </div>
    </div>
}
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Avant de pouvoir tester cette page, il convient de rendre les autres pages du site non consultables sans authentification, pour ce faire il suffit d’ajouter l’attribut [Authorize] sur les autres Controllers.

Implémentation de la page d’inscription

Votre site est maintenant sécurisé, il faut un login et un mot passe à vos utilisateurs pour accéder aux pages de votre site, il ne reste plus qu’à faire la page d’inscription afin de permettre aux gens de s’inscrire.

1. Dans la classe AuthentificationController, créez la méthode Register pour l’appel du formulaire d’inscription

[HttpGet]
[AllowAnonymous]
public ActionResult Register ()
{
    return View();
}

2. Dans le dossier Models de votre projet, créez une nouvelle classe nommée RegisterViewModel

public class RegisterViewModel
{
    [Required]
    [Display(Name = "E-mail")]
    [RegularExpression(@"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", ErrorMessage = "adresse email non valide")]
    public string UserName { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Mot de passe")] 
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirmer le mot de passe")]
    [Compare("Password", ErrorMessage = "Le mot de passe et le mot de passe de confirmation ne correspondent pas.")]
    public string ConfirmPassword { get; set; }
}

3. Dans la classe AuthentificationController, créez la méthode Register pour le retour du formulaire d’inscription

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        //Si l'utilisateur n'existe pas
        var user = await UserManager.FindByNameAsync(model.UserName);
        if (user != null)
        {
            ModelState.AddModelError(string.Empty, "Utilisateur déja existant");
            return View(model);
        }
        user = new IdentityUser
        {
            Email = model.UserName,
            UserName = model.UserName,
        };
        var identityResult = await UserManager.CreateAsync(user, model.Password);
    }
    return View();
}

Il ne reste plus qu’à créer la vue associée à la méthode Register

@model MonNamespace.Models.RegisterViewModel

@{
    ViewBag.Title = "Register";
}

<h2>Authentification</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Créez un compte!</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(model => model.UserName, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.UserName, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.UserName, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.Password, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.Password, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.ConfirmPassword, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(model => model.ConfirmPassword, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(model => model.Password, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Se Connecter" class="btn btn-default" />
        </div>
    </div>
</div>
}
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Vous avez maintenant un site ASP.Net MVC Identity qui utilise l'authentification OWIN pour la connexion et la déconnexion des utilisateurs. Néanmoins votre site n’est pas sécurisé pour autant car aucune configuration n’a été faite concernant la complexité du mot de passe et son mode de stockage.

Stockage du mot de passe personnalisé

Le Framework Identity est conçu pour être facilement configurable, la plupart des éléments clés de l’infrastructure sont exposés en tant qu’interfaces, avec des implémentations enregistrées par défaut. Comme dit précédemment, par défaut pour le Framework Identity, l’algorithme est la fonction de hachage PBKDF2 mais comme le nombre d’itérations de cette fonction n’est pas suffisant, il est préférable de la changer à l’aide de l’interface IPasswordHasher.

Interface IPasswordHasher

L’interface IPasswordHasher est là pour intervenir durant les deux cas suivants : l’inscription et la connexion. Lors de l’inscription, elle utilise la méthode HashPassword pour créer le hachage de mot de passe qui sera stocké dans la base de données. Lors de la connexion, elle utilise la méthode VerifyHashedPassword pour hacher le mot de passe fourni et le comparer au hachage stocké.

En résumé l’interface IPasswordHasher est utilisé dans les deux scénarios décrits ci-dessus et expose une méthode pour chacun

public interface IPasswordHasher
{
    string HashPassword(string password);

    PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword);
}

Implémentation d’un PasswordHasher

On implémentera la méthode BCryptPasswordHasher utilisant l’algorithme Bcrypt.

Algorithme Bcrypt

L’algorithme Bcrypt utilise une variante de l'algorithme de chiffrement Blowfish et introduit un facteur de travail, qui permet de déterminer le coût de la fonction de hachage. Ceci offre une certaine protection contre la loi de Moore. A mesure que les ordinateurs deviennent plus rapides, cet algorithme peut être ajusté pour nécessiter plus de puissance du processeur.

Installation du packages NuGet

De nombreux packages implémentent déjà l’algorithme. Afin de pouvoir configurer un nouveau PasswordHasher, vous devrez ajouter de nouveaux packages NuGet :

BCrypt.Net-Next

Création de la classe BCryptPasswordHasher

1. Dans le dossier App_Start, créer une nouvelle classe nommée BCryptPasswordHasher.

2. Faire hériter la classe BCryptPasswordHasher de l’interface IPasswordHasher.

3. Implémenter la méthode HashPassword.

4. Implémenter la méthode VerifyHashedPassword.

public class BcryptPasswordHasher : IPasswordHasher
{
    public string HashPassword(string password)
    {
        return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 15);
    }

    public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
    {
        if (BCrypt.Net.BCrypt.Verify(providedPassword, hashedPassword))
        {
            return PasswordVerificationResult.Success;
        }
        else
        {
            return PasswordVerificationResult.Failed;
        }
    }
}

Configuration du UserManager

L’ajout du PasswordHasher se fait dans le fichier AuthConfig.cs, lors de la configuration du UserManager il faut initialiser l’attribut PasswordHasher avec notre nouveau BCryptPasswordHasher

//Configuration du UserManager
app.CreatePerOwinContext<UserManager<IdentityUser>>(
    (opt, cont) =>
    {
       var usermanager = new UserManager<IdentityUser>(cont.Get<UserStore<IdentityUser>>()) { };
       usermanager.PasswordHasher = new BcryptPasswordHasher();
       return usermanager;
    });

Règles de complexité du mot de passe

Le dernier point concerne la complexité du mot de passe, les règles à suivre sont les suivantes :

  • 1 lettre minuscule minimum,
  • 1 lettre majuscule minimum,
  • 1 chiffre minimum,
  • 1 caractère spécial minimum,
  • 12 caractères minimum.

Cette action se fait encore dans le fichier AuthConfig.cs, lors de la configuration du UserManager il faut initialiser, cette fois ci, l’attribut PasswordValidator avec notre nouveau PasswordValidator

usermanager.PasswordValidator = new PasswordValidator
{
    RequireDigit = true,
    RequireLowercase = true,
    RequireNonLetterOrDigit = true,
    RequireUppercase = true,
    RequiredLength = 12
};

Conclusion

L’objectif est maintenant atteint, il s’agit d’un site ASP.Net MVC Identity qui utilise l'authentification OWIN configuré de telle sorte que le stockage soit fait avec l’algorithme Bcrypt et que la complexité respecte les règles : 12 caractères et 4 types différents (des minuscules, des majuscules, des chiffres et des caractères spéciaux).

Nabil Sidhoum

Titulaire d'un diplôme d’ingénieur télécommunications et réseaux à l'école d’ingénieur Sup-Galilée , Nabil a travaillé pendant deux ans en tant qu'ingénieur logiciel C#/.Net dans le centre de service Microsoft Dynamics CRM de Cap Gemini avant d'intégrer SoftFluent.

Profil de l'auteur