Wie implementiere ich das Zurücksetzen von Passwörtern?

81

Ich arbeite an einer Anwendung in ASP.NET und habe mich speziell gefragt, wie ich eine Password ResetFunktion implementieren könnte, wenn ich meine eigene rollen möchte.

Insbesondere habe ich folgende Fragen:

  • Was ist ein guter Weg, um eine eindeutige ID zu generieren, die schwer zu knacken ist?
  • Sollte ein Timer daran angeschlossen sein? Wenn ja, wie lange sollte es dauern?
  • Soll ich die IP-Adresse aufzeichnen? Ist es überhaupt wichtig?
  • Welche Informationen sollte ich im Bildschirm "Passwort zurücksetzen" anfordern? Nur E-Mail-Adresse? Oder vielleicht eine E-Mail-Adresse und einige Informationen, die sie "kennen"? (Lieblingsteam, Name des Welpen usw.)

Gibt es noch andere Überlegungen, die ich beachten muss?

NB : Andere Fragen haben die technische Implementierung vollständig beschönigt . In der Tat beschönigt die akzeptierte Antwort die blutigen Details. Ich hoffe, dass diese Frage und die nachfolgenden Antworten in die blutigen Details eingehen, und ich hoffe, dass die Antworten, indem ich diese Frage viel enger formuliere, weniger "Flusen" und mehr "Gore" sind.

Bearbeiten : Antworten, die auch darauf eingehen, wie eine solche Tabelle in SQL Server oder in ASP.NET MVC-Links zu einer Antwort modelliert und verarbeitet wird, sind willkommen.

George Stocker
quelle
ASP.NET MVC verwendet den Standard-ASP.NET-Authentifizierungsanbieter, sodass alle Codebeispiele, die Sie in der Umgebung finden, für Ihre Zwecke relevant sein sollten.
Paulwhit

Antworten:

66

Viele gute Antworten hier, ich werde nicht die Mühe machen, alles zu wiederholen ...

Bis auf ein Problem, das von fast jeder Antwort hier wiederholt wird, obwohl es falsch ist:

Guids sind (realistisch) einzigartig und statistisch nicht zu erraten.

Dies ist nicht der Fall. GUIDs sind sehr schwache Bezeichner und sollten NICHT verwendet werden, um den Zugriff auf das Konto eines Benutzers zu ermöglichen.
Wenn Sie die Struktur untersuchen, erhalten Sie höchstens 128 Bits ... was heutzutage nicht viel berücksichtigt wird.
Davon ist die erste Hälfte typisch invariant (für das Erzeugungssystem) und die Hälfte von dem, was übrig bleibt, ist zeitabhängig (oder etwas Ähnliches).
Alles in allem ist es ein sehr schwacher und leicht zu erzwingender Mechanismus.

Also benutze das nicht!

Verwenden Sie stattdessen einfach einen kryptografisch starken Zufallszahlengenerator ( System.Security.Cryptography.RNGCryptoServiceProvider) und erhalten Sie mindestens 256 Bit Rohentropie.

Alles andere, wie die zahlreichen anderen Antworten lieferten.

AviD
quelle
6
Ich stimme absolut zu, dass GUIDs meines Wissens niemals so konzipiert wurden, dass sie kryptografisch stark und unmöglich zu erraten sind.
Jan Soltis
5
Gut gesagt, AFAIK MSDN stellt klar, dass GUID nicht für die Sicherheit verwendet werden sollte.
dr. böse
2
UUIDs der Version 4 werden seit 2000 in Windows verwendet: Wie werden .NET 4-GUIDs generiert? - Stapelüberlauf . Sie enthalten 122 zufällige Bits, was meiner Meinung nach den NIST-Empfehlungen entspricht. Es gab eine sehr schlimme Sicherheitsanfälligkeit für einen lokalen Angriff, der laut CryptGenRandom - Wikipedia bis 2008 in Vista und XP behoben wurde. Wo sehen Sie also Probleme mit der aktuellen Verwendung von GUIDs?
Nealmcb
4
Dieser "Old New Thing" -Blog beschreibt veraltete UUIDs der Version 1 und zitiert einen Internet-Entwurf (etwas, das Sie niemals tun sollten), der 1998, 10 Jahre vor dem Blog-Beitrag, abgelaufen ist. Ich würde ihnen in Zukunft skeptisch gegenüberstehen. Wir haben diese Schlachten vor langer Zeit geschlagen und scheinen die meisten davon gewonnen zu haben. Ich bin immer noch damit einverstanden, dass die Verwendung eines sauberen API-Aufrufs an eine Crypto-Random-Quelle viel besser ist, aber GUID / UUIDs in Version 4 nicht so hart behandeln.
Nealmcb
1
Für was es wert ist, beantwortet dies nicht die Frage, "wie man ein Passwort zurücksetzt". Sie haben gerade großartige Punkte über GUIDs erbrochen.
Rex Whitten
67

EDIT 2012/05/22: Im Anschluss an diese beliebte Antwort verwende ich selbst keine GUIDs mehr in diesem Verfahren. Wie die andere beliebte Antwort verwende ich jetzt meinen eigenen Hashing-Algorithmus, um den Schlüssel zum Senden der URL zu generieren. Dies hat den Vorteil, dass es auch kürzer ist. Schauen Sie in System.Security.Cryptography nach, um sie zu generieren. Normalerweise verwende ich auch ein SALZ.

Setzen Sie das Kennwort des Benutzers nicht sofort zurück.

Setzen Sie das Kennwort des Benutzers nicht sofort zurück, wenn er es anfordert. Dies ist eine Sicherheitsverletzung, da jemand E-Mail-Adressen (dh Ihre E-Mail-Adresse im Unternehmen) erraten und Kennwörter nach Belieben zurücksetzen kann. Zu den Best Practices gehört heutzutage normalerweise ein "Bestätigungs" -Link, der an die E-Mail-Adresse des Benutzers gesendet wird und bestätigt, dass er ihn zurücksetzen möchte. Über diesen Link möchten Sie den eindeutigen Schlüssellink senden. Ich sende meine mit einem Link wie:domain.com/User/PasswordReset/xjdk2ms92

Ja, legen Sie eine Zeitüberschreitung für den Link fest und speichern Sie den Schlüssel und die Zeitüberschreitung in Ihrem Backend (und Salt, wenn Sie eine verwenden). Zeitüberschreitungen von 3 Tagen sind die Norm. Benachrichtigen Sie den Benutzer über 3 Tage auf Webebene, wenn er zum Zurücksetzen auffordert.

Verwenden Sie einen eindeutigen Hash-Schlüssel

In meiner vorherigen Antwort wurde die Verwendung einer GUID angegeben. Ich bearbeite dies jetzt, um jedem zu raten, einen zufällig generierten Hash zu verwenden, z RNGCryptoServiceProvider. B. mit dem . Und stellen Sie sicher, dass Sie keine "echten Wörter" aus dem Hash entfernen. Ich erinnere mich an einen speziellen 6-Uhr-Anruf, bei dem eine Frau ein bestimmtes "c" -Wort in ihrem Hash-Schlüssel "Angenommen, zufällig zu sein" erhalten hat, den ein Entwickler durchgeführt hat. Doh!

Gesamtes Verfahren

  • Der Benutzer klickt auf "Passwort zurücksetzen".
  • Der Benutzer wird nach einer E-Mail gefragt.
  • Der Benutzer gibt eine E-Mail ein und klickt auf Senden. Bestätigen oder verweigern Sie die E-Mail nicht, da dies ebenfalls eine schlechte Praxis ist. Sagen Sie einfach: "Wir haben eine Anfrage zum Zurücksetzen des Passworts gesendet, wenn die E-Mail bestätigt wurde." oder etwas Kryptisches.
  • Sie erstellen einen Hash aus dem RNGCryptoServiceProvider, speichern ihn als separate Entität in einer ut_UserPasswordRequestsTabelle und verknüpfen ihn erneut mit dem Benutzer. Auf diese Weise können Sie alte Anfragen verfolgen und den Benutzer darüber informieren, dass ältere Links abgelaufen sind.
  • Senden Sie den Link zur E-Mail.

Der Benutzer erhält den Link wie http://domain.com/User/PasswordReset/xjdk2ms92und klickt darauf.

Wenn der Link überprüft wurde, fragen Sie nach einem neuen Passwort. Einfach, und der Benutzer kann sein eigenes Passwort festlegen. Oder legen Sie hier Ihr eigenes kryptisches Passwort fest und informieren Sie sie hier über ihr neues Passwort (und senden Sie es per E-Mail an sie).

eduncan911
quelle
1
Ich habe mich gefragt, warum ich einen neuen HASH-Schlüssel generieren sollte, wenn das tatsächliche Benutzerkennwort gehasht ist. Wäre es nicht richtig, eine E-Mail an den Benutzer mit einem Link zum Zurücksetzen des Passworts zu senden, indem das Hashed-Passwort übergeben wird? Das Hash-Passwort kann nicht zurückgesetzt werden. Wenn der Benutzer auf den Link klickt, erhält der Server das Hash-Passwort, vergleicht es mit dem tatsächlich gespeicherten und ermöglicht dem Benutzer, das Passwort zu ändern.
Daniel
Und eine weitere schöne Sache ist, dass Sie kein Timeout einstellen müssen. Sobald der Benutzer das Passwort geändert hat, ist der alte Link automatisch nicht mehr gültig, da das in der Datenbank gespeicherte Hash-Passwort geändert wurde.
Daniel
@ Daniel, das ist eine wirklich schlechte Idee. Ich denke, Sie müssen den Begriff "Brute-Force-Angriffe" googeln. Der Grund, warum Sie möchten, dass es abläuft, ist, dass der Hacker das Recht erhält, das Passwort zu ändern, falls jemandes E-Mail ein Jahr später kompromittiert wird (und er sie niemals zurücksetzt).
eduncan911
@ Educan911. Ich weiß, dass Brute-Force-Angriffe, aber auch, um Zugriff auf den Hashed-Schlüssel zu haben, muss die Person mit böser Absicht Zugriff auf die E-Mail haben, und wenn sie Zugriff darauf hat, muss das Hash-Passwort nicht zurückgesetzt werden. Um dies fast unmöglich zu machen, können Sie auch das Hash-Passwort oder noch besser das Passwort mit etwas mehr hashen. Ich bin nicht anderer Meinung als du, ich versuche nur ein Brainstorming darüber zu machen
Daniel
8

Zunächst müssen wir wissen, was Sie bereits über den Benutzer wissen. Offensichtlich haben Sie einen Benutzernamen und ein altes Passwort. Was weißt du noch? Hast du eine Email adresse? Haben Sie Daten zur Lieblingsblume des Benutzers?

Angenommen, Sie haben einen Benutzernamen, ein Kennwort und eine funktionierende E-Mail-Adresse, müssen Sie Ihrer Benutzertabelle zwei Felder hinzufügen (vorausgesetzt, es handelt sich um eine Datenbanktabelle): ein Datum mit dem Namen new_passwd_expire und eine Zeichenfolge new_passwd_id.

Angenommen, Sie haben die E-Mail-Adresse des Benutzers. Wenn jemand ein Zurücksetzen des Kennworts anfordert, aktualisieren Sie die Benutzertabelle wie folgt:

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

Als Nächstes senden Sie eine E-Mail an den Benutzer unter folgender Adresse:

Lieber so und so

Jemand hat ein neues Passwort für das Benutzerkonto <Benutzername> unter <Name Ihrer Website> angefordert. Wenn Sie dieses Zurücksetzen des Passworts angefordert haben, folgen Sie diesem Link:

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

Wenn dieser Link nicht funktioniert, können Sie zu http://example.com/yourscript.lang gehen und Folgendes in das Formular eingeben: <new_password_id>

Wenn Sie kein Zurücksetzen des Kennworts angefordert haben, können Sie diese E-Mail ignorieren.

Danke, yada yada

Codieren Sie jetzt Ihrecript.lang: Dieses Skript benötigt ein Formular. Wenn das var-Update die URL weitergegeben hat, werden im Formular nur der Benutzername und die E-Mail-Adresse des Benutzers abgefragt. Wenn das Update nicht bestanden wird, werden Sie nach Benutzername, E-Mail-Adresse und dem in der E-Mail gesendeten ID-Code gefragt. Sie fragen auch nach einem neuen Passwort (natürlich zweimal).

Um das neue Kennwort des Benutzers zu überprüfen, überprüfen Sie, ob Benutzername, E-Mail-Adresse und ID-Code übereinstimmen, ob die Anforderung nicht abgelaufen ist und ob die beiden neuen Kennwörter übereinstimmen. Bei Erfolg ändern Sie das Kennwort des Benutzers in das neue Kennwort und löschen die Felder zum Zurücksetzen des Kennworts aus der Benutzertabelle. Stellen Sie außerdem sicher, dass Sie den Benutzer abmelden / alle anmeldebezogenen Cookies löschen und den Benutzer auf die Anmeldeseite umleiten.

Im Wesentlichen ist das Feld new_passwd_id ein Kennwort, das nur auf der Seite zum Zurücksetzen des Kennworts funktioniert.

Eine mögliche Verbesserung: Sie können <Benutzername> aus der E-Mail entfernen. "Jemand hat ein Zurücksetzen des Passworts für ein Konto unter dieser E-Mail-Adresse angefordert ..." Damit wird der Benutzername nur dem Benutzer bekannt, wenn die E-Mail abgefangen wird. Ich habe nicht so angefangen, denn wenn jemand das Konto angreift, kennt er den Benutzernamen bereits. Diese zusätzliche Dunkelheit verhindert Man-in-the-Middle-Angriffe auf Gelegenheiten, falls jemand Böswilliges die E-Mail abfängt.

Wie für Ihre Fragen:

Generieren der zufälligen Zeichenfolge: Sie muss nicht extrem zufällig sein. Jeder GUID-Generator oder sogar md5 (concat (salt, current_timestamp ())) ist ausreichend, wenn Salt im Benutzerdatensatz etwas ist, wie ein Zeitstempelkonto erstellt wurde. Es muss etwas sein, das der Benutzer nicht sehen kann.

Timer: Ja, Sie benötigen dies nur, um Ihre Datenbank gesund zu halten. Nicht mehr als eine Woche ist wirklich notwendig, aber mindestens 2 Tage, da Sie nie wissen, wie lange eine E-Mail-Verzögerung dauern kann.

IP-Adresse: Da sich die E-Mail um Tage verzögern kann, ist die IP-Adresse nur für die Protokollierung und nicht für die Validierung nützlich. Wenn Sie es protokollieren möchten, tun Sie dies, andernfalls benötigen Sie es nicht.

Bildschirm zurücksetzen: Siehe oben.

Hoffe das deckt es ab. Viel Glück.

jmucchiello
quelle
Wäre ein potenzieller Angreifer nicht in der Lage, das MD5 des aktuellen Datenstempels zu verwenden, um einzusteigen?
George Stocker
Ich würde dringend empfehlen, kein Passwort per E-Mail über das Internet zu senden. Die meisten Benutzer lassen diese E-Mails nicht gelöscht, was eine Sicherheitsverletzung darstellt. Einige von ihnen möchten sie jedes Mal aus ihren bevorzugten E-Mails kopieren und einfügen. Was passiert, wenn das Zertifikat des Mailservers des Unternehmens des Benutzers abgelaufen ist und der Datenverkehr abgehört wird? Um diesen möglichen Verstoß zu minimieren, müssen Sie (1) eine kurze Ablaufzeit für dieses bestimmte Kennwort festlegen - 1 Stunde, und (2) den Benutzer zwingen, es bei der nächsten Anmeldung zu aktualisieren.
Ognyan Dimitrov
Ognyan, das per E-Mail gesendete Passwort funktioniert nur einmal. Sie müssen ihr Passwort nach der Anmeldung ändern und die E-Mail enthält nicht den Benutzernamen. Nein, sie können es nicht jedes Mal kopieren und einfügen. Das Nichtlöschen der E-Mail ist kein Sicherheitsproblem, da es sich nur um eine bedeutungslose Folge von Buchstaben / Zahlen handelt, die dem Angreifer NICHTS einbringt, nachdem das Kennwort zurückgesetzt wurde.
jmucchiello
3

Eine GUID, die an die E-Mail-Adresse des Datensatzes gesendet wird, reicht wahrscheinlich für die meisten herkömmlichen Anwendungen aus - mit einer noch besseren Zeitüberschreitung.

Wenn die E-Mail-Box des Benutzers kompromittiert wurde (dh ein Hacker hat die Anmeldung / das Passwort für die E-Mail-Adresse), können Sie nicht viel dagegen tun.

EJ Brennan
quelle
2

Sie können dem Benutzer eine E-Mail mit einem Link senden. Dieser Link würde einige schwer zu erratende Zeichenfolgen enthalten (wie GUID). Auf der Serverseite würden Sie auch dieselbe Zeichenfolge speichern, die Sie an den Benutzer gesendet haben. Wenn der Benutzer auf den Link drückt, können Sie ihn in Ihrem Datenbankeintrag mit derselben geheimen Zeichenfolge finden und sein Passwort zurücksetzen.

Sergej Andrejev
quelle
Weitere Details wären hilfreich.
George Stocker
2

1) Zum Generieren der eindeutigen ID können Sie den Secure Hash-Algorithmus verwenden. 2) Timer angeschlossen? Meinten Sie einen Ablauf für den zurückgesetzten pwd-Link? Ja, Sie können einen Ablaufsatz festlegen. 3) Sie können andere Informationen als die E-Mail-ID zur Validierung anfordern. Wie das Geburtsdatum oder einige Sicherheitsfragen. 4) Sie können auch zufällige Zeichen generieren und diese zusammen mit der Anfrage eingeben .. um sicherzustellen, dass die Passwortanforderung nicht durch Spyware oder ähnliches automatisiert wird ..

Java Guy
quelle
0

Ich denke, Microsoft Guide für ASP.NET Identity ist ein guter Anfang.

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

Code, den ich für die ASP.NET-Identität verwende:

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; }
}
Ogglas
quelle