web-dev-qa-db-fra.com

Comment implémenter les réinitialisations de mot de passe?

Je travaille sur une application dans ASP.NET et je me demandais précisément comment je pourrais implémenter une fonction Password Reset si je voulais lancer la mienne. 

Plus précisément, j'ai les questions suivantes:

  • Quel est le bon moyen de générer un identifiant unique difficile à résoudre? 
  • Devrait-il y avoir une minuterie attachée? Si oui, combien de temps devrait-il être?
  • Devrais-je enregistrer l'adresse IP? Est-ce même important?
  • Quelles informations dois-je demander dans l'écran "Réinitialisation du mot de passe"? Juste une adresse email? Ou peut-être l'adresse e-mail plus une information qu'ils "connaissent"? (Équipe favorite, nom du chiot, etc.)

Y a-t-il d'autres considérations dont je dois être conscient?

NB: Autres questions ont sont passées sous silence mise en œuvre technique. En effet, la réponse acceptée passe sous silence les détails sanglants. J'espère que cette question et les réponses ultérieures entreront dans les détails sanglants, et j'espère qu'en formulant cette question de manière beaucoup plus étroite que les réponses sont moins "fluff" et plus "gore".

Edit: les réponses qui expliquent également la manière dont une telle table serait modélisée et gérée dans SQL Server ou tout lien ASP.NET MVC vers une réponse seraient les bienvenues.

81
George Stocker

Beaucoup de bonnes réponses ici, je ne me dérangerai pas de tout répéter ...

À l'exception d'un problème qui est repris par presque toutes les réponses ici, même si c'est faux:

Les guids sont (en réalité) uniques et statistiquement impossibles à deviner.

Ce n'est pas vrai, les GUID sont des identificateurs très faibles, et devraient NOT être utilisés pour autoriser l'accès au compte d'un utilisateur.
Si vous examinez la structure, vous obtenez un total de 128 bits au maximum ... ce qui n’est pas considéré beaucoup de nos jours.
Dont la première moitié est invariante typique (pour le système générateur) et la moitié de ce qui reste dépend du temps (ou de quelque chose de similaire).
En résumé, c’est un mécanisme très faible et facilement renforcé.

Alors n'utilisez pas ça!

Au lieu de cela, utilisez simplement un générateur de nombre aléatoire doté d'une force de cryptographie élevée (System.Security.Cryptography.RNGCryptoServiceProvider), et obtenez au moins 256 bits d'entropie brute.

Tout le reste, comme les nombreuses autres réponses fournies.

66
AviD

EDIT 2012/05/22: Suite à cette réponse populaire, je n'utilise plus moi-même de GUID dans cette procédure. À l'instar de l'autre réponse populaire, j'utilise maintenant mon propre algorithme de hachage pour générer la clé à envoyer dans l'URL. Cela a aussi l'avantage d'être plus court. Regardez dans System.Security.Cryptography pour les générer, ce que j'utilise habituellement aussi sous SALT.

Tout d'abord, ne réinitialisez pas immédiatement le mot de passe de l'utilisateur.

Tout d'abord, ne réinitialisez pas immédiatement le mot de passe de l'utilisateur lorsqu'il le demande. Il s’agit d’une faille de sécurité, car une personne pourrait deviner des adresses électroniques (c’est-à-dire votre adresse électronique au sein de la société) et réinitialiser les mots de passe à volonté. Les meilleures pratiques de nos jours incluent généralement un lien de "confirmation" envoyé à l'adresse électronique de l'utilisateur, confirmant qu'il souhaite le réinitialiser. Ce lien est l'endroit où vous souhaitez envoyer le lien de clé unique. J'envoie le mien avec un lien du genre: domain.com/User/PasswordReset/xjdk2ms92

Oui, définissez un délai d’expiration sur le lien et stockez la clé et le délai d’expiration sur votre serveur principal (et salez si vous en utilisez un). Les délais d'attente de 3 jours sont la norme et assurez-vous d'avertir l'utilisateur de 3 jours au niveau Web lorsqu'il demande la réinitialisation.

Utilisez une clé de hachage unique

Ma réponse précédente a dit d'utiliser un GUID. J'édite maintenant ceci pour conseiller à tout le monde d'utiliser un hachage généré aléatoirement, par exemple. en utilisant RNGCryptoServiceProvider. Et, assurez-vous d’éliminer tous les "vrais mots" du hash. Je me souviens d'un appel téléphonique spécial de 6 heures du matin où une femme recevait un certain "c" mot dans sa "suppose être aléatoire" clé hachée qu'un développeur a fait. Doh!

Procédure complète

  • L'utilisateur clique sur "réinitialiser" le mot de passe.
  • L'utilisateur est invité à envoyer un courrier électronique. 
  • L'utilisateur entre un email et clique sur envoyer. Ne confirmez pas ni ne refusez le courrier électronique, car c’est également une mauvaise pratique. Dites simplement: "Nous avons envoyé une demande de réinitialisation du mot de passe si l'e-mail est vérifié." ou quelque chose de cryptique semblable.
  • Vous créez un hachage à partir de RNGCryptoServiceProvider, vous le stockez en tant qu'entité distincte dans une table ut_UserPasswordRequests et vous le liez à l'utilisateur. Vous pouvez donc suivre les anciennes demandes et informer l'utilisateur que les anciens liens ont expiré.
  • Envoyez le lien à l'email.

L'utilisateur obtient le lien, comme http://domain.com/User/PasswordReset/xjdk2ms92, et clique dessus. 

Si le lien est vérifié, vous demandez un nouveau mot de passe. C'est simple et l'utilisateur peut définir son propre mot de passe. Ou bien, définissez votre propre mot de passe cryptique ici et informez-les de leur nouveau mot de passe ici (et envoyez-le par courrier électronique).

67
eduncan911

Tout d'abord, nous devons savoir ce que vous savez déjà sur l'utilisateur. De toute évidence, vous avez un nom d'utilisateur et un ancien mot de passe. Que savez-vous d'autre? As-tu une adresse e-mail? Avez-vous des données concernant la fleur préférée de l'utilisateur?

En supposant que vous ayez un nom d'utilisateur, un mot de passe et une adresse électronique valide, vous devez ajouter deux champs à votre table d'utilisateurs (en supposant qu'il s'agit d'une table de base de données): une date appelée new_passwd_expire et une chaîne new_passwd_id.

En supposant que vous avez l'adresse électronique de l'utilisateur, lorsque quelqu'un demande une réinitialisation du mot de passe, vous mettez à jour le tableau d'utilisateurs comme suit:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

Ensuite, vous envoyez un courrier électronique à l'utilisateur à cette adresse:

Cher untel

Quelqu'un a demandé un nouveau mot de passe pour le compte d'utilisateur <nom d'utilisateur> situé dans <nom de votre site Web>. Si vous avez demandé cette réinitialisation du mot de passe, suivez ce lien:

http://example.com/yourscript.lang?update=<new_password_id >

Si ce lien ne fonctionne pas, vous pouvez accéder à http://example.com/yourscript.lang et entrer les informations suivantes dans le formulaire: <new_password_id>

Si vous n'avez pas demandé de réinitialisation de mot de passe, vous pouvez ignorer cet email.

Merci, yada yada

Maintenant, codez yourscript.lang: Ce script a besoin d’un formulaire. Si la mise à jour var est transmise à l'URL, le formulaire demande simplement le nom d'utilisateur et l'adresse électronique de l'utilisateur. Si la mise à jour n'est pas transmise, le système vous demande le nom d'utilisateur, l'adresse e-mail et le code d'identification envoyé dans l'e-mail. Vous demandez également un nouveau mot de passe (deux fois bien sûr).

Pour vérifier le nouveau mot de passe de l'utilisateur, vérifiez que le nom d'utilisateur, l'adresse électronique et le code d'identification correspondent tous, que la demande n'a pas expiré et que les deux nouveaux mots de passe correspondent. En cas de succès, vous remplacez le mot de passe de l'utilisateur par le nouveau et effacez les champs de réinitialisation du mot de passe de la table des utilisateurs. Veillez également à déconnecter l’utilisateur/à effacer tous les cookies liés à la connexion et à le rediriger vers la page de connexion.

En gros, le champ new_passwd_id est un mot de passe qui ne fonctionne que sur la page de réinitialisation du mot de passe.

Une amélioration potentielle: vous pourriez supprimer <nom d'utilisateur> de l'email. "Quelqu'un a demandé une réinitialisation du mot de passe pour un compte à cette adresse électronique ...." Ainsi, le nom d'utilisateur devient quelque chose que seul l'utilisateur sait si le courrier électronique est intercepté. Je n'ai pas commencé comme ça parce que si quelqu'un attaque le compte, il connaît déjà le nom d'utilisateur. Cette obscurité supplémentaire met fin aux attaques d'opportunités au cas où une personne malveillante intercepterait le courrier électronique.

Quant à vos questions:

générer la chaîne aléatoire: il n'est pas nécessaire que ce soit extrêmement aléatoire. Tout générateur GUID ou même md5 (concat (salt, current_timestamp ())) est suffisant, où salt est quelque chose sur l'enregistrement de l'utilisateur comme le compte timestamp a été créé. Ce doit être quelque chose que l'utilisateur ne peut pas voir.

timer: Oui, vous en avez besoin pour garder votre base de données saine. Pas plus d'une semaine n'est vraiment nécessaire, mais au moins deux jours, car on ne sait jamais combien de temps un courrier électronique risque de durer.

Adresse IP: l'adresse e-mail pouvant être retardée de plusieurs jours, l'adresse IP n'est utile que pour la journalisation, pas pour la validation. Si vous voulez le connecter, faites-le, sinon vous n'en avez pas besoin.

Écran de réinitialisation: voir ci-dessus.

J'espère que ça couvre. Bonne chance.

8
jmucchiello

Un GUID envoyé à l'adresse e-mail de l'enregistrement est probablement suffisant pour la plupart des applications classiques - avec un délai d'expiration encore meilleur.

Après tout, si la boîte aux lettres de l’utilisateur a été compromise (c’est-à-dire qu’un pirate dispose du nom de connexion/du mot de passe de l’adresse électronique), vous ne pouvez rien faire à ce sujet.

3
E.J. Brennan

Vous pouvez envoyer un email à l'utilisateur avec un lien. Ce lien contient des chaînes difficiles à deviner (comme le GUID). Du côté du serveur, vous stockerez également la même chaîne que celle que vous avez envoyée à l'utilisateur. Désormais, lorsque l'utilisateur appuie sur le lien, vous pouvez trouver dans votre entrée de base de données une même chaîne secrète et réinitialiser son mot de passe.

2
Sergej Andrejev

1) Pour générer l'identifiant unique, vous pouvez utiliser l'algorithme Secure Hash Algorithm . 2) timer attaché? Voulez-vous dire une Expiration pour le lien de réinitialisation pwd? Oui, vous pouvez avoir un ensemble d'expiration 3) Vous pouvez demander des informations supplémentaires autres que l'emailId pour valider .. Comme la date de naissance ou des questions de sécurité 4) Vous pouvez également générer des caractères aléatoires et demander à les saisir également avec le request .. pour vous assurer que la demande de mot de passe n'est pas automatisée par certains logiciels espions ou des choses comme ça ..

2
Java Guy

Je pense que le guide Microsoft pour ASP.NET Identity est un bon début.

https://docs.Microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery- with-aspnet-identity

Code que j'utilise pour l'identité ASP.NET:

Web.Config:

<add key="AllowedHosts" value="example.com,example2.com" />

AccountController.cs:

[Route("RequestResetPasswordToken/{email}/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);

    var user = await UserManager.FindByEmailAsync(email);
    if (user == null)
    {
        Logger.Warn("Password reset token requested for non existing email");
        // Don't reveal that the user does not exist
        return NoContent();
    }

    //Prevent Host Header Attack -> Password Reset Poisoning. 
    //If the IIS has a binding to accept connections on 80/443 the Host parameter can be changed.
    //See https://security.stackexchange.com/a/170759/67046
    if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) {
            Logger.Warn($"Non allowed Host detected for password reset {Request.RequestUri.Scheme}://{Request.Headers.Host}");
            return BadRequest();
    }

    Logger.Info("Creating password reset token for user id {0}", user.Id);

    var Host = $"{Request.RequestUri.Scheme}://{Request.Headers.Host}";
    var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
    var callbackUrl = $"{Host}/resetPassword/{HttpContext.Current.Server.UrlEncode(user.Email)}/{HttpContext.Current.Server.UrlEncode(token)}";

    var subject = "Client - Password reset.";
    var body = "<html><body>" +
               "<h2>Password reset</h2>" +
               $"<p>Hi {user.FullName}, <a href=\"{callbackUrl}\"> please click this link to reset your password </a></p>" +
               "</body></html>";

    var message = new IdentityMessage
    {
        Body = body,
        Destination = user.Email,
        Subject = subject
    };

    await UserManager.EmailService.SendAsync(message);

    return NoContent();
}

[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)
{
    if (!ModelState.IsValid)
        return NoContent();

    var user = await UserManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        Logger.Warn("Reset password request for non existing email");
        return NoContent();
    }            

    if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
    {
        Logger.Warn("Reset password requested with wrong token");
        return NoContent();
    }

    var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);

    if (result.Succeeded)
    {
        Logger.Info("Creating password reset token for user id {0}", user.Id);

        const string subject = "Client - Password reset success.";
        var body = "<html><body>" +
                   "<h1>Your password for Client was reset</h1>" +
                   $"<p>Hi {user.FullName}!</p>" +
                   "<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
                   "</body></html>";

        var message = new IdentityMessage
        {
            Body = body,
            Destination = user.Email,
            Subject = subject
        };

        await UserManager.EmailService.SendAsync(message);
    }

    return NoContent();
}

public class ResetPasswordRequestModel
{
    [Required]
    [Display(Name = "Token")]
    public string Token { 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 = 10)]
    [DataType(DataType.Password)]
    [Display(Name = "New password")]
    public string NewPassword { get; set; }

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