[ASP.NET MVC4] Comment implémenter la réinitialisation de mot de passe

26. May 2013 19:04 by Renaud in ASP.NET MVC  //  Tags: , , ,   //   Comments (1)

Le template "Internet Application" de ASP.NET MVC4 est hyper complet en ce qui concerne la gestion des comptes utilisateurs. De base, ils permet de créer une application web avec tout ce qu'il faut pour qu'un utilisateur puisse s'inscrire, se connecter, gérer son profil et modifier son mot de passe. Avec un minimum de configuration, on peut également permettre aux utilisateurs de choisir entre un compte local (nom d'utilisateur + mot de passe) et une identification au travers d'un fournisseur d'identité tel que Facebook, Twitter, Microsoft (Live), Google et Yahoo (ou n'importe quel autre service à condition d'écrire soi-même quelques lignes de code...).

Tout le code nécessaire au bon fonctionnement de ces fonctionnalité est intégré dans le template. Il n'y a donc pour ainsi dire rien à faire! :) Vous avez sans doute déjà jeté un oeil dans le contrôleur AccountController. C'est effectivement là que tout se passe.  

Une autre fonctionnalité disponible dans ASP.NET MVC4 est la récupération de compte. Par récupération de compte, j'entends ici le fait de pouvoir modifier son mot de passe lorsque l'on a perdu son mot de passe actuel, et que l'on ne peut donc plus se connecter.

Mais contrairement à ce qui a été cité ci-dessus, il n'y a pas d'action prévue pour cela dans le template.

La gestion des comptes avec WebSecurity

Il y a eu du changement depuis ASP.NET MVC3. Précédemment, tout ce qui concernait la gestion des compte se faisait à l'aide de la classe Membership. Mais si vous regardez le code de AccountController dans un projet MVC4, vous verrez qu'on utilise désormais deux autres classes : WebSecurity, et OAuthWebSecurity. Ces classes sont une nouvelle couche d'abstraction qui permettent de réaliser les actions courantes de gestion de compte utilisateur comme la création, la modification de mot de passe lorsque l'on est connecté, etc... 

Scénario de récupération

Avant de nous lancer dans l'implémentation de cette procédure, réfléchissons deux secondes à ce dont nous avons besoin...

  • Un moyen d'identifier l'utilisateur: son email ou son mot de passe

En principe l'utilisateur ayant perdu son mot de passe devrait se souvenir de sa propre adresse email. Cela permettra d'identifier le compte que l'on veut récupérer.

  • Un jeton de modification de mot de passe, avec une date d'expiration.

Lorsque l'utilisateur lancera la procédure de récupération, un jeton sera créé et sera valable pour une durée limitée. Sans ce jeton, pas de récupération possible.

  • Un moyen de contacter l'utilisateur pour lui envoyer son jeton.

Évidemment, il est hors de question de donner ce jeton à n'importe quelle personne le demandant. On l'enverra donc à l'adresse email de l'utilisateur qui a fait la requête. Ainsi, à moins qu'une autre personne se soit emparée du compte email, on peut être sûr que la requête émane bien du propriétaire du compte.

Comme je vous le disais, la classe WebSecurity implémente les tâches les plus courantes! :) Et ce mécanisme aussi!

L'implémentation

1/ Obtenir l'adresse email de l'utilisateur

On l'a vu, nous aurons besoin d'une adresse email! Par défaut, le UserProfile (la classe servant à représenter un utilisateur) ne possède pas cet attribut. Mais ce n'est pas un soucis: pour étoffer le profil utilisateur avec ASP.NET MVC4, il suffit d'ajouter des attributs à cette classe.

/Models/AccountModels.cs

[Table("UserProfile")]
public class UserProfile
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int UserId { get; set; }
    public string UserName { get; set; }

    public string Email { get; set; }
}

 On va également modifier légèrement la page d'inscription pour demander à l'utilisateur cette information supplémentaire:

/Models/AccountModels.cs

public class RegisterModel
{
    [Required]
    [Display(Name = "User name")]
    public string UserName { get; set; }

    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

/Views/Account/Register.cshtml

@model RecoverPasswordMVC4.Models.RegisterModel
@{
    ViewBag.Title = "Register";
}

<hgroup class="title">
    <h1>@ViewBag.Title.</h1>
    <h2>Create a new account.</h2>
</hgroup>

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

    <fieldset>
        <legend>Registration Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.UserName)
                @Html.TextBoxFor(m => m.UserName)
            </li>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li>
            <li>
                @Html.LabelFor(m => m.Password)
                @Html.PasswordFor(m => m.Password)
            </li>
            <li>
                @Html.LabelFor(m => m.ConfirmPassword)
                @Html.PasswordFor(m => m.ConfirmPassword)
            </li>
        </ol>
        <input type="submit" value="Register" />
    </fieldset>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

À ce stade, si l'on exécute l'application et que l'on essaie de se loguer, on pourra voir le résultat suivant : 

Il reste encore à modifier l'action POST Register pour sauver l'Email. Une fois que le compte est créé à l'aide de WebSecurity, on va récupérer l'enregistrement UserProfile correspondant, et le mettre à jour.

/Controllers/AccountController.cs

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        try
        {
            WebSecurity.CreateUserAndAccount(model.UserName, model.Password);

            using (UsersContext context = new UsersContext())
            {
                UserProfile user = context.UserProfiles.FirstOrDefault(u => u.UserName == model.UserName);
                user.Email = model.Email;
                context.SaveChanges();
            }

            WebSecurity.Login(model.UserName, model.Password);

            return RedirectToAction("Index", "Home");
        }
        catch (MembershipCreateUserException e)
        {
            ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

2/ Demander à l'utilisateur ses informations de compte

On va créer une nouvelle action RecupAccount, et le formulaire correspondant.

/Controllers/AccountController.cs

[AllowAnonymous]
public ActionResult RecupAccount()
{
    return View(new RecupAccountModel());
}

L'attribut AllowAnonymous indique que les utilisateurs peuvent faire appel à cette action sans être connectés. Il est nécessaire ici de le déclarer explicitement car le AccountController est décoré de l'attribut [Authorized], ce qui fait que toutes ses actions nécessitent par défaut d'être identifié.

La classe RecupAccountModel possède deux propriétés. L'utilisateur devra nous fournir ses données s'il veut qu'on essaie de s'occuper de son problème! :)

/Models/RecupAccountModel.cs

public class RecupAccountModel
{
    [Required]
    public string UserName { get; set; }
    [Required]
    public string Email { get; set; }
}

/Views/Account/RecupAccount.cshtml

@model RecoverPasswordMVC4.Models.RecupAccountModel

@{
    ViewBag.Title = "Récupération de compte";
}

<h2>Mot de passe perdu?</h2>

@using (@Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()
    
    @Html.LabelFor(m => m.UserName)
    @Html.EditorFor(m => m.UserName)
    @Html.ValidationMessageFor(m => m.UserName)

    @Html.LabelFor(m => m.Email)
    @Html.EditorFor(m => m.Email)
    @Html.ValidationMessageFor(m => m.Email)

    <input type="submit" value="Aidez-moi!" />
}

Notre utilisateur malheureux va donc pouvoir nous envoyer ses infos, que nous allons évidemment vérifier lors de l'envoi du formulaire. 

3/ Envoyer le jeton de réinitialisation à l'utilisateur

On va donc vérifier les données, et si elles sont correctes, générer un jeton de réinitialisation de mot de passe grâce à la classe WebSecurity. Pour cela, on fait appel à la méthode GeneratePasswordResetToken qui prend en paramètre le nom d'utilisateur et le temps avant expiration (facultatif). Par défaut, le token expire après 1440 minutes (24 heures), mais comme je suis un peu vache j'ai décidé de ne laisser que 5 minutes à l'utilisateur pour faire la manip'! :)

/Controllers/AccountController.cs

[HttpPost]
[AllowAnonymous]
public ActionResult RecupAccount(RecupAccountModel model)
{
    // On vérifie que les deux champs sont remplis
    if (ModelState.IsValid)
    {
        using (UsersContext context = new UsersContext())
        {
            // Si on trouve un compte qui match avec les infos entrées par l'utilisateur...
            if (context.UserProfiles.Any(
                    u => u.UserName.Equals(model.UserName, StringComparison.OrdinalIgnoreCase) &&
                            u.Email.Equals(model.Email, StringComparison.OrdinalIgnoreCase)))
            {
                // On crée un token permettant de réinitialiser le mot de passe ...
                string token = WebSecurity.GeneratePasswordResetToken(model.UserName, 5);

                // ... et on l'envoit à l'utilisateur par email
                MailingService mailingService = new MailingService();
                mailingService.SendTokenByEmail(token, model.Email, model.UserName);

                return View("RecupAccountConfirmed");
            }
            else
            {
                ModelState.AddModelError("", "Le nom d'utilisateur ou l'email est incorrect.");
            }
        }
    }
    return View(model);
}

Ensuite on va envoyer ce token par email. En fait, on va envoyer à l'utilisateur un lien sur lequel il n'aura qu'à cliquer pour consommer ce token et réinitialiser son mot de passe:

var url = String.Format("{0}/account/resetpassword/?token={1}", SitewebUrl, token);

Si vous ne savez pas comment envoyer de mail en ASP.NET MVC, vous trouverez l'implémentation de la classe MailingService dans le projet d'exemple téléchargeable à la fin de l'article.

 

A ce stade, soit l'utilisateur nous a donné des informations correctes et il reçoit un email, soit les informations sont bidons et il revient sur le formulaire avec un message d'erreur.

4/ Vérifier le jeton

Lorsque l'utilisateur aura reçu son email, il naviguera vers un lien du genre: http://monsupersite.be/account/resetpassword/?token=Ddsn32jd3jLJsnd3

On va donc créer cette action, chargée de vérifier le jeton de l'utilisateur. Encore une fois nous allons faire appel à la classe WebSecurity. La méthode GetUserIdFromPasswordResetToken permet de récupérer l'Id de l'utilisateur lié au jeton. Si ce dernier a expiré ou ne correspond à aucun jeton existant, l'Id retourné sera -1.

/Controllers/AccountController.cs

[AllowAnonymous]
public ActionResult ResetPassword(string token)
{
    var userId = WebSecurity.GetUserIdFromPasswordResetToken(token);

    using (UsersContext context = new UsersContext())
    {
        UserProfile user = context.UserProfiles.Find(userId);
        if (user == null)
            return View("Error");

        var model = new ResetPasswordModel()
            {
                Token = token,
                UserName = user.UserName
            };

        return View(model);
    }
}

Si l'Id ne correspond à rien, on peut rediriger l'utilisateur vers une page d'erreur, ou plutôt lui proposer de refaire une demande de jeton. Le sien ayant peut-être tout simplement expiré.

5/ Modifier le mot de passe

Quoiqu'il en soit, si tout se passe bien, l'utilisateur sera amené sur la page de réinitialisation de son mot de passe. La classe ResetPasswordModel est définie comme ceci. On va avoir besoin de toutes ces valeurs par la suite. On connait déjà le Token et le UserName, et l'utilisateur va nous donner le nouveau mot de passe.

/Models/Account/ResetPasswordModel.cs

public class ResetPasswordModel
{
    [Required]
    public string Token { get; set; }
    [Required]
    public string UserName { get; set; }
    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Nouveau mot de passe")]
    public string NewPassword { get; set; }
    [Required]
    [DataType(DataType.Password)]
    [Display(Name = "Confirmation mot de passe")]
    public string ConfirmNewPassword { get; set; }
}

 /Views/Account/ResetPassword.cshtml

 

@model RecoverPasswordMVC4.Models.ResetPasswordModel

@{
    ViewBag.Title = "Réinitialisation du mot de passe";
}

<h2>Réinitialisation du mot de passe</h2>

@using (@Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()
    
    @Html.HiddenFor(m => m.Token)
    @Html.HiddenFor(m => m.UserName)
    
    @Html.LabelFor(m => m.NewPassword)
    @Html.PasswordFor(m => m.NewPassword)
    @Html.ValidationMessageFor(m => m.NewPassword)

    @Html.LabelFor(m => m.ConfirmNewPassword)
    @Html.PasswordFor(m => m.ConfirmNewPassword)
    @Html.ValidationMessageFor(m => m.ConfirmNewPassword)

    <input type="submit" value="Changer mot de passe" />
}

Dans cette vue, les champs Token et UserName seront invisibles à l'utilisateur, mais ils seront envoyés avec les autres informations du formulaire.

Et pour finir, on va vérifier que l'utilisateur a bien entré deux fois le même mot de passe, et on va faire une dernière fois appel à la classe WebSecurity. La méthode ResetPassword va mettre à jour le mot de passe en consommant le jeton, et la méthode Login va connecter l'utilisateur à l'aide de son nom et de son nouveau mot de passe.

/Controllers/AccountController.cs

[HttpPost]
[AllowAnonymous]
public ActionResult ResetPassword(ResetPasswordModel model)
{
    if (ModelState.IsValid)
    {
        if (!model.NewPassword.Equals(model.ConfirmNewPassword))
        {
            ModelState.AddModelError("", "Les mots de passe ne correspondent pas.");
            return View(model);
        }
        WebSecurity.ResetPassword(model.Token, model.NewPassword);
        WebSecurity.Login(model.UserName, model.NewPassword);
        return RedirectToAction("Index", "Home");
    }
    return View(model);
}

 

Et voilà, l'utilisateur pourra à nouveau se connecter. Il ne reste plus qu'à ajouter un petit lien "Mot de passe oublié?" sur la page de login :

@Html.ActionLink("Mot de passe oublié ?", "RecupAccount", "Account")

 

Pour résumer, les différentes étapes de la procédure sont :

1/ Avoir un moyen de contacter l'utilisateur (email).

2/ Mettre en place une page "Mot de passe oublié?"

3/ Générer un jeton de réinitialisation et l'envoyer à l'utilisateur à l'aide de WebSecurity.

4/ Vérifier que le jeton est correct avec WebSecurity et montrer une page demandant un nouveau mot de passe.

5/ Réinitialiser le mot de passe avec WebSecurity en consommant le jeton

 

Ressources

Vous pouvez télécharger la solution d'exemple ici: 

RecoverPasswordMVC4.zip

TextBox

About the author

I'm a developer, blog writer, and author, mainly focused on Microsoft technologies (but not only Smile). I'm Microsoft MVP Client Development since July 2013.

Microsoft Certified Professional

I'm currently working as an IT Evangelist with an awesome team at the Microsoft Innovation Center Belgique, where I spend time and energy helping people to develop their projects. I also give training to enthusiastic developers and organize afterworks with the help of the Belgian community.

MIC Belgique

Take a look at my first book (french only): Développez en HTML 5 pour Windows 8

Développez en HTML5 pour Windows 8

Membre de l'association Fier d'être développeur

TextBox

Month List