• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 23/11/2017

Gérer l’authentification

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Bon, et si nous gérions enfin l’authentification sur notre site ?
Parce que je ne sais pas si vous êtes comme moi, mais la bidouille du dernier TP pour tenter de reconnaître un utilisateur via le nom du navigateur… ce n’était pas terrible. En tout cas, bien indigne de tous les espoirs que nous mettons dans ce tutoriel. Nous allons donc remédier à tout cela et voir comment nous pouvons proposer un système d’authentification sur notre site, de manière à identifier une personne de manière unique mais surtout à empêcher une personne non authentifiée de pouvoir accéder à certaines parties de notre site.

Authentification maison

La plupart des ressources sur Internet se basent sur le mécanisme de membership d’ASP.NET pour gérer l’authentification. C’est une solution intéressante, quoiqu'elle commence un peu à dater, mais je trouve qu’elle masque un peu la mécanique qu’il y a derrière une authentification.
J’ai choisi dans ce tutoriel de vous présenter comment réaliser une authentification maison afin de vous montrer autre chose et aussi que vous puissiez constater qu’il n’y a pas que cette solution.

Nous avons déjà plusieurs méthodes dans la DAL pour gérer la création d’un utilisateur et son authentification. Je vous les rappelle :

public interface IDal : IDisposable
{
    […]
    int AjouterUtilisateur(string nom, string motDePasse);
    Utilisateur Authentifier(string nom, string motDePasse);
    Utilisateur ObtenirUtilisateur(int id);
    Utilisateur ObtenirUtilisateur(string idStr);
}

Ces méthodes fonctionnent vu que nous avons écrit les tests automatisés permettant de les vérifier. Enfin… ça, c’était avant notre immonde bidouille. Revenons à présent sur des méthodes propres :

public int AjouterUtilisateur(string nom, string motDePasse)
{
    string motDePasseEncode = EncodeMD5(motDePasse);
    Utilisateur utilisateur = new Utilisateur { Prenom = nom, MotDePasse = motDePasseEncode };
    bdd.Utilisateurs.Add(utilisateur);
    bdd.SaveChanges();
    return utilisateur.Id;
}

public Utilisateur Authentifier(string nom, string motDePasse)
{
    string motDePasseEncode = EncodeMD5(motDePasse);
    return bdd.Utilisateurs.FirstOrDefault(u => u.Prenom == nom && u.MotDePasse == motDePasseEncode);
}

public Utilisateur ObtenirUtilisateur(int id)
{
    return bdd.Utilisateurs.FirstOrDefault(u => u.Id == id);
}

public Utilisateur ObtenirUtilisateur(string idString)
{
    int id;
    if (int.TryParse(idString, out id))
        return ObtenirUtilisateur(id);
    return null;
}

Et également :

public bool ADejaVote(int idSondage, string idStr)
{
    int id;
    if (int.TryParse(idStr, out id))
    {
        Sondage sondage = bdd.Sondages.First(s => s.Id == idSondage);
        if (sondage.Votes == null)
            return false;
        return sondage.Votes.Any(v => v.Utilisateur != null && v.Utilisateur.Id == id);
    }
    return false;
}

Voilà, la DAL est à nouveau prête pour gérer l’authentification et tous nos tests fonctionnent à nouveau. :)

Authentification par formulaire ASP.NET

ASP.NET dispose d’un système d’authentification, élaboré à l'époque des Webforms, qui s’intègre parfaitement avec le framework MVC ; on l’appelle l’authentification par formulaire. Elle fonctionne grâce à des cookies.

Le principe est que lorsqu’un client soumet une requête HTTP pour accéder à une ressource sécurisée, ASP.NET vérifie la présence d’un cookie d’authentification. Si l’utilisateur n’est pas authentifié, alors il est redirigé vers une page d’authentification, sinon il peut continuer sa navigation. Lorsque l’utilisateur s’authentifie, ASP.NET génère ce cookie afin de lui autoriser l’accès aux ressources sécurisées.
Les informations de ce cookie sont cryptées par ASP.NET. Tout ce que nous avons besoin de savoir c’est qu’il est possible de récupérer l’utilisateur courant et s’il est authentifié grâce à des propriétés de l’objet :

HttpContext.User.Identity

Par exemple, pour savoir si l’utilisateur est authentifié, nous pourrons utiliser :

HttpContext.User.Identity.IsAuthenticated

Et pour connaitre l’identifiant de l’utilisateur connecté, nous pourrons utiliser :

HttpContext.User.Identity.Name

Pour activer cette authentification, il y a un petit paramétrage à effectuer dans le web.config. Il faut définir la section authentification, sous la section <system.web>  :

<authentication mode="Forms">
  <forms loginUrl="~/Login/Index" />
</authentication>

Le mode d’authentification est Forms (= formulaire) et l’URL permettant de se loguer sera /Login/Index.
Ainsi, toute requête accédant à une ressource protégée, et où l’utilisateur n’est pas authentifié, sera redirigée vers la page de login qui est paramétrée.

Nous devons donc sécuriser les contrôleurs adéquats, puis écrire un contrôleur qui va permettre de s’authentifier et qui sera accessible via l’URL /Login/Index. Allons-y !

Sécuriser un contrôleur

Je vous le dis tout de suite, pour sécuriser un contrôleur c’est très facile. Nous allons sécuriser le contrôleur Vote de manière à ce que seules les personnes authentifiées puissent y accéder. Regardez comme c’est simple :

[Authorize]
public class VoteController : Controller
{
    …
}

Et c’est tout. :waw:
Il suffit d’utiliser l’attribut Authorize  sur la classe et ainsi, dès que vous tenterez d’invoquer une action de ce contrôleur, ASP.NET MVC va vérifier que vous êtes bien authentifié. Si ce n’est pas le cas, alors vous serez redirigés vers la page de login.
Voilà ce qu’il se passe lorsque je clique sur le bouton créer un sondage :

Redirection pour authentification introuvable
Redirection pour authentification introuvable

Ce qu’il est important de voir ici, c’est que nous avons été redirigés vers l’URL /Login/Index, comme prévu.... Mais il y a également autre chose dans l’URL :

http://localhost:60818/Login/Index?ReturnUrl=%2fVote%2fIndex%2f1

Nous pouvons voir un paramètre ReturnUrl qui indique que nous venions de l’URL /Vote/Index/1 (bien sûr l’URL est encodée).

Et voilà, impossible d’accéder au sondage sans être authentifié ; à chaque tentative nous sommes redirigés vers la page d’authentification. Et comme celle-ci n’existe pas, nous allons devoir la créer pour permettre à l’utilisateur de s’authentifier correctement.

Il est également possible de sécuriser des méthodes unitairement, au lieu de sécuriser un contrôleur en entier. Il suffit de déplacer l’attribut Authorize  et de l’utiliser pour décorer les méthodes que vous souhaitez sécuriser :

[Authorize]
public ActionResult Index(int id)
{
    …
}

Inversement, si le contrôleur est sécurisé en entier, alors il est possible de dé-sécuriser des actions individuellement en les décorant de l’attribut AllowAnonymous  :

[Authorize]
public class VoteController : Controller
{
    [AllowAnonymous]
    public ActionResult Index(int id)
    {
        …
    }
}

Remarque : le mécanisme d’autorisation se base sur les filtres d’ASP.NET MVC que nous découvrirons un peu plus loin.

Écrire le contrôleur permettant de s’authentifier

Maintenant, nous allons permettre à l’utilisateur de s’authentifier en écrivant le contrôleur d’authentification.
L’URL que nous avons définie dans le web.config (/Login/Index) vous renseigne déjà sur le contrôleur que nous allons écrire. Il s’agit du contrôleur Login . Voici son code :

public class LoginController : Controller
{
    private IDal dal;

    public LoginController() : this(new Dal())
    {

    }

    private LoginController(IDal dalIoc)
    {
        dal = dalIoc;
    }

    public ActionResult Index()
    {
        UtilisateurViewModel viewModel = new UtilisateurViewModel { Authentifie = HttpContext.User.Identity.IsAuthenticated };
        if (HttpContext.User.Identity.IsAuthenticated)
        {
            viewModel.Utilisateur = dal.ObtenirUtilisateur(HttpContext.User.Identity.Name);
        }
        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(UtilisateurViewModel viewModel, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            Utilisateur utilisateur = dal.Authentifier(viewModel.Utilisateur.Prenom, viewModel.Utilisateur.MotDePasse);
            if (utilisateur != null)
            {
                FormsAuthentication.SetAuthCookie(utilisateur.Id.ToString(), false);
                if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl))
                    return Redirect(returnUrl);
                return Redirect("/");
            }
            ModelState.AddModelError("Utilisateur.Prenom", "Prénom et/ou mot de passe incorrect(s)");
        }
        return View(viewModel);
    }

    public ActionResult CreerCompte()
    {
        return View();
    }

    [HttpPost]
    public ActionResult CreerCompte(Utilisateur utilisateur)
    {
        if (ModelState.IsValid)
        {
            int id = dal.AjouterUtilisateur(utilisateur.Prenom, utilisateur.MotDePasse);
            FormsAuthentication.SetAuthCookie(id.ToString(), false);
            return Redirect("/");
        }
        return View(utilisateur);
    }

    public ActionResult Deconnexion()
    {
        FormsAuthentication.SignOut();
        return Redirect("/");
    }
}

Vous pouvez voir que dans la méthode Index, je vérifie si l’utilisateur est authentifié grâce à la propriété HttpContext.User.Identity.IsAuthenticated . Si c’est le cas, alors je récupère cet utilisateur via notre DAL. À noter que j’ai créé un view-model pour préparer mes données à présenter dans la vue :

public class UtilisateurViewModel
{
    public Utilisateur Utilisateur { get; set; }
    public bool Authentifie { get; set; }
}

Celui-ci me fournit l’utilisateur et également si l’utilisateur est authentifié.
En arrivant sur la vue, si l’utilisateur n’est pas authentifié alors nous lui fournirons un formulaire pour le faire. Il faut donc traiter ce formulaire, c’est le boulot de la méthode Index  qui s'occupe de gérer le POST. Vous aurez remarqué que cette méthode possède deux paramètres :

[HttpPost]
public ActionResult Index(UtilisateurViewModel viewModel, string returnUrl)
{
    …
}

Il y a déjà le view-model, ce qui est plutôt habituel ; il va permettre de traiter le contenu du formulaire. Mais il y a également un autre paramètre de type chaîne de caractères. Vous aurez deviné grâce au nommage que celui-ci va permettre d’attraper l’éventuel paramètre ReturnUrl  qui peut être positionné par ASP.NET lorsqu’il nous redirige vers le formulaire d’authentification après avoir tenté d’accéder à une ressource alors que nous ne le pouvions pas. Rappelez-vous, cette URL était du genre :

/Login/Index?ReturnUrl=%2fVote%2fIndex%2f1

Nous avons donc l’opportunité ici de récupérer cette URL de retour. Pourquoi ? Afin de pouvoir rediriger l’utilisateur vers la ressource qu’il essayait d’obtenir avant d’avoir été redirigé vers l’authentification, ce qui lui simplifie son parcours.

Donc l’action permet dans un premier temps de vérifier que le modèle est valide. En l’occurrence, il va vérifier que le prénom et le mot de passe sont bien renseignés, ceci via la propriété ModelState.IsValid . Ensuite, nous allons authentifier l’utilisateur. Si le prénom et le mot de passe ne le permettent pas, alors nous ajoutons une erreur au ModelState  afin d’en informer la vue. Attention, ici le champ qui est en erreur est Utilisateur.Prenom car c’est le chemin d’accès via le view-model.
Si les login et mot de passe sont valides, il n’y a plus qu’à authentifier sur ASP.NET l’utilisateur en générant un cookie. Cela se fait en appelant la méthode FormsAuthentication.SetAuthCookie . Cette méthode génère le cookie d’authentification à partir de l’identifiant de l’utilisateur. C’est ce que j’ai choisi comme information d’authentification. Le deuxième paramètre positionné à false  permet de faire en sorte que l’authentification n’ait qu’une durée de vie limitée à la session.
L’appel à cette méthode va permettre indirectement de renseigner les propriétés de l’objet HttpContext.User.Identity  qui nous permet de vérifier que l’utilisateur est authentifié.
Ensuite, si le paramètre dans l’URL existe, alors je le traite afin de renvoyer l’utilisateur vers la page qu’il a préalablement demandé. Notez que le contrôle IsLocalUrl  n’est pas fait que pour faire joli, il permet de s’assurer que le paramètre ReturnUrl contient bien un lien vers notre site, ceci afin d’éviter toute tentative de redirection vers un site qui ne serait pas le nôtre.

Une fois ce paramètre correctement récupéré, nous pouvons rediriger l’utilisateur au bon endroit.

J’ai également implémenté des actions permettant de créer un compte. L’action en GET affiche simplement la vue et celle en POST traite le formulaire et appelle la DAL pour créer le compte. Nous utilisons bien sûr la même méthode ensuite pour créer le cookie.

J’en ai profité également pour créer une action permettant la déconnexion de l’utilisateur. Il suffit d’appeler la méthode FormsAuthentication.SignOut()  pour vider le cookie d’authentification.

Côté vue, rien de nouveau, la vue permettant de s’authentifier est très classique :

@model ChoixResto.ViewModels.UtilisateurViewModel

[...]

@if (Model.Authentifie)
{
    <h3>
        Vous êtes déjà authentifié avec le login :
        @Model.Utilisateur.Prenom
    </h3>
    @Html.ActionLink("Voulez-vous vous déconnecter ?", "Deconnexion")
}
else
{
    <p>
        Veuillez vous authentifier :
    </p>
    using (Html.BeginForm())
    {
        <div>
            @Html.LabelFor(m => m.Utilisateur.Prenom)
            @Html.TextBoxFor(m => m.Utilisateur.Prenom)
            @Html.ValidationMessageFor(m => m.Utilisateur.Prenom)
        </div>
        <div>
            @Html.LabelFor(m => m.Utilisateur.MotDePasse)
            @Html.PasswordFor(m => m.Utilisateur.MotDePasse)
            @Html.ValidationMessageFor(m => m.Utilisateur.MotDePasse)
        </div>
        <input type="submit" value="Se connecter" />
        <br />
        @Html.ActionLink("Créer un compte", "CreerCompte")
    }
}

Vous pouvez remarquer que j’ai un gros if en plein milieu qui me permet d’afficher deux choses différentes en fonction de si l’utilisateur est déjà authentifié :

Le formulaire d'authentification
Le formulaire d'authentification

ou non :

La vue lorsqu'on est déjà authentifié
La vue lorsqu'on est déjà authentifié

Note : Pour obtenir ce magnifique formulaire, j’ai dû rajouter des attributs Display  sur le modèle Utilisateur  :

public class Utilisateur
{
    public int Id { get; set; }
    [Required]
    [Display(Name = "Prénom")]
    public string Prenom { get; set; }
    [Required]
    [Display(Name = "Mot de passe")]
    public string MotDePasse { get; set; }
}

afin que les champs Prenom  et MotDePasse  utilisent des chaînes un peu plus adéquates.

Pour la vue de création de compte, c’est le même principe :

@model ChoixResto.Models.Utilisateur

[...]

<p>Créer un compte : </p>
@using (Html.BeginForm())
{
<div>
    @Html.LabelFor(m => m.Prenom)
    @Html.TextBoxFor(m => m.Prenom)
    @Html.ValidationMessageFor(m => m.Prenom)
</div>
<div>
    @Html.LabelFor(m => m.MotDePasse)
    @Html.PasswordFor(m => m.MotDePasse)
    @Html.ValidationMessageFor(m => m.MotDePasse)
</div>
<input type="submit" value="Créer" />
}

Et voilà ;).

Je n’ai pas souhaité en parler ici, mais il est également possible de restreindre des actions à un certain groupe d’utilisateurs. Cela se fait souvent notamment pour les parties d’administrations d’un site. Cela pourrait être le cas dans notre application, nous pourrions vouloir permettre l’accès à l’ajout d’un restaurant ou à la modification qu’aux administrateurs du site. Ici, cela ne me paraissait pas pertinent.

Si vous avez besoin de ceci, sachez qu’il vous faut gérer un système de rôles, par exemple avec le mécanisme de gestion de rôles d’ASP.NET. Puis vous pourrez changer l’attribut Authorize  pour qu’il prenne un groupe en paramètre :

[Authorize(Roles="Administrateur")]
public ActionResult CreerRestaurant()
{
   …
}

Utiliser l’identité

Il ne reste plus qu’une chose à faire. Maintenant que nous savons qui est l’utilisateur, il faut s’en servir dans le contrôleur de Vote (VoteController ), notamment lorsque nous utilisons la méthode ObtenirUtilisateur  :

Utilisateur utilisateur = dal.ObtenirUtilisateur(HttpContext.User.Identity.Name);

Dans la propriété Name  nous retrouvons l’identifiant de l’utilisateur car c’est lui que nous avons mis en appelant la méthode SetAuthCookie  :

FormsAuthentication.SetAuthCookie(id.ToString(), false);

Grâce à cet identifiant stocké dans le cookie, nous pouvons retrouver l’utilisateur qui est en cours de navigation.
Remplacez donc tous les :

Request.Browser.Browser

par :

HttpContext.User.Identity.Name

Vous pourrez également corriger les tests de VoteControllerTests  afin de ne plus utiliser le bouchon de la propriété Browser  mais plutôt un bouchon de la propriété Identity.Name . Changez donc la méthode Init  de la classe VoteControllerTests  pour avoir :

[TestInitialize]
public void Init()
{
    dal = new DalEnDur();
    idSondage = dal.CreerUnSondage();

    Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
    controllerContext.Setup(p => p.HttpContext.User.Identity.Name).Returns("1");

    controleur = new VoteController(dal);
    controleur.ControllerContext = controllerContext.Object;
}

Ainsi que la méthode de test :

[TestMethod]
public void IndexPost_AvecViewModelValideEtUtilisateur_AppelleBienAjoutVoteEtRenvoiBonneAction()
{
    Mock<IDal> mock = new Mock<IDal>();
    mock.Setup(m => m.ObtenirUtilisateur("1")).Returns(new Utilisateur { Id = 1, Prenom = "Nico" });

    Mock<ControllerContext> controllerContext = new Mock<ControllerContext>();
    controllerContext.Setup(p => p.HttpContext.User.Identity.Name).Returns("1");
    controleur = new VoteController(mock.Object);
    controleur.ControllerContext = controllerContext.Object;

    RestaurantVoteViewModel viewModel = new RestaurantVoteViewModel
    {
        ListeDesResto = new List<RestaurantCheckBoxViewModel>
            {
                new RestaurantCheckBoxViewModel { EstSelectionne = true, Id = 2, NomEtTelephone = "Resto pinière (0102030405)"},
                new RestaurantCheckBoxViewModel { EstSelectionne = false, Id = 3, NomEtTelephone = "Resto toro (0102030405)"},
            }
    };
    controleur.ValideLeModele(viewModel);

    RedirectToRouteResult resultat = (RedirectToRouteResult)controleur.Index(viewModel, idSondage);

    mock.Verify(m => m.AjouterVote(idSondage, 2, 1));
    Assert.AreEqual("AfficheResultat", resultat.RouteValues["action"]);
    Assert.AreEqual(idSondage, resultat.RouteValues["id"]);
}

Et voilà, tout est en place désormais pour que la suite de l’application ne soit accessible qu’aux personnes authentifiées.

Les authentifications lors de la création d'un projet

Jusqu'à maintenant, nous n'avions utilisé que le modèle de projet "empty" lorsque nous voulions créer un projet ASP.NET MVC. Et si vous avez bien regardé lors de la création de ce projet, vous avez pu voir sur la droite qu'il y avait un élément grisé avec marqué "pas d'authentification" :

Pas d'authentification pour un modèle vide
Pas d'authentification pour un modèle vide

Par contre, si vous essayez de choisir plutôt un autre modèle, par exemple MVC, vous pourrez voir que le bouton se dégrise, proposant une authentification à base de Comptes utilisateurs individuels :

Authentification de type comptes utilisateurs individuels
Authentification de type comptes utilisateurs individuels

Si vous cliquez sur le bouton "modifier l'authentification", vous pourrez alors voir encore d'autres types d'authentifications :

Les différents types d'authentifications
Les différents types d'authentifications

Nous avons donc à notre disposition :

Pas d'authentification

Pour les applications ne nécessitant pas d'authentification

Comptes d'utilisateurs individuels

Pour les applications qui stockent des profils d'utilisateurs dans une base de données SQL Server. Les utilisateurs peuvent inscrire ou se connecter à l'aide de leur compte Facebook, Twitter, Google, Microsoft, etc. existant

Comptes d'organisation

Pour les applications qui authentifient les utilisateurs auprès d'Active Directory, Windows Azure Active Directory ou Office 365

Authentification Windows

Pour les applications intranet

Si vous créez l'application avec les Comptes d'utilisateurs individuels, alors vous pouvez voir que Visual Studio vous a créé une solution avec pleins de nouveaux fichiers et plus particulièrement si vous allez voir dans le répertoire App_Start, vous pourrez trouver par exemple le fichier StartupAuth.cs qui va configurer l'authentification. Et ce qui est génial, c'est que vous avez déjà plein de choses pour gérer la connexion avec des fournisseurs tiers ; vous avez juste à décommenter des lignes (et à fournir les infos de vos applications tierces, comme les clés et les secrets) pour vous en servir :

// Supprimer les commentaires des lignes suivantes pour autoriser la connexion avec des fournisseurs de connexions tiers
//app.UseMicrosoftAccountAuthentication(
// clientId: "",
// clientSecret: "");

//app.UseTwitterAuthentication(
// consumerKey: "",
// consumerSecret: "");

//app.UseFacebookAuthentication(
// appId: "",
// appSecret: "");

//app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
//{
// ClientId = "",
// ClientSecret = ""
//});

Ce qui simplifie grandement ce type d'authentification, qui sont devenues de plus en plus classiques.

Je vous encourage à aller jeter également un coup d'oeil dans le contrôleur AccountController  que Visual Studio a généré.

ASP.NET Identity

Le système d'authentification d'ASP.NET n'avait pas évolué depuis 2005. C'est assez récemment qu'il s'est fait une nouvelle jeunesse avec l'intégration d'ASP.NET Identity, utilisable bien sûr sans problème avec ASP.NET MVC. Évidemment, il y a toujours moyen d'enregistrer nos utilisateurs dans une base de données et ce mécanisme est simplifié grâce aux API d'ASP.NET Identity, mais il est également possible de se brancher sur d'autres fournisseurs d'identité, comme Facebook, Google,... C'est ce que nous avons vu juste au-dessus, même si je ne suis pas rentré dans les détails d'implémentation.

Pour simplifier, on peut dire qu'ASP.NET Identity est le nouveau système de membership d'ASP.NET. Il se base sur des middlewares d'authentification OWIN, c'est-à-dire une couche indépendante qui va servir à l'authentification.

Ce qui va changer pour notre authentification, c'est que celle-ci va se baser sur des interfaces respectant le standard OWIN, et nous aurons juste à implémenter ces interfaces avec la solution que nous avons choisi pour gérer notre authentification, que ce soit en base de données, dans Azure, avec Active Directory,...

En résumé

  • L’authentification ASP.NET est simple à mettre en place et permet de sécuriser des contrôleurs ou des actions de votre application.

  • On peut utiliser l’authentification par formulaire combinée à l’utilisation de l’attribut [Authorize].

  • L’authentification par formulaire fonctionne grâce à un cookie crypté.

  • ASP.NET Identity apporte un renouveau dans l'authentification, et est capable d'utiliser des services tiers pour s'authentifier, sans être complètement dépendant d'ASP.NET.

Exemple de certificat de réussite
Exemple de certificat de réussite