Jak zaimplementować resetowanie haseł?

81

Pracuję nad aplikacją w ASP.NET i zastanawiałem się konkretnie, w jaki sposób mógłbym zaimplementować Password Resetfunkcję, gdybym chciał utworzyć własną.

W szczególności mam następujące pytania:

  • Jaki jest dobry sposób na wygenerowanie unikalnego identyfikatora, który jest trudny do złamania?
  • Czy powinien być do niego dołączony timer? Jeśli tak, to jak długo powinno to trwać?
  • Czy powinienem zapisać adres IP? Czy to w ogóle ma znaczenie?
  • O jakie informacje mam poprosić na ekranie „Resetowanie hasła”? Tylko adres e-mail? A może adres e-mail i jakieś informacje, które „znają”? (Ulubiona drużyna, imię szczeniaka itp.)

Czy są jakieś inne kwestie, o których muszę wiedzieć?

NB : Pozostałe pytaniapomijane technicznej realizacji całości. Rzeczywiście, przyjęta odpowiedź przeważa nad krwawymi szczegółami. Mam nadzieję, że to pytanie i kolejne odpowiedzi wejdą w krwawe szczegóły i mam nadzieję, że poprzez sformułowanie tego pytania znacznie węższym, odpowiedzi są mniej „puszyste”, a bardziej „krwawe”.

Edycja : mile widziane byłyby odpowiedzi, które również dotyczą tego, jak taka tabela byłaby modelowana i obsługiwana w SQL Server lub dowolnym łączu ASP.NET MVC do odpowiedzi.

George Stocker
źródło
ASP.NET MVC korzysta z domyślnego dostawcy uwierzytelniania ASP.NET, więc każdy przykładowy kod, który znajdziesz wokół tego s, powinien być odpowiedni dla twoich celów.
paulwhit

Odpowiedzi:

66

Wiele dobrych odpowiedzi, nie zawracam sobie głowy powtarzaniem tego wszystkiego ...

Z wyjątkiem jednego problemu, który powtarza prawie każda odpowiedź, mimo że jest błędna:

Przewodniki są (realistycznie) unikalne i statystycznie niemożliwe do odgadnięcia.

To nieprawda, identyfikatory GUID są bardzo słabymi identyfikatorami i NIE powinny być używane w celu umożliwienia dostępu do konta użytkownika.
Jeśli zbadasz strukturę, otrzymasz w sumie maksymalnie 128 bitów ... co obecnie nie jest uważane za dużo.
Z czego pierwsza połowa jest typowa niezmienna (dla systemu generującego), a połowa tego, co pozostało, jest zależna od czasu (lub czegoś podobnego).
Podsumowując, jest to bardzo słaby i łatwy do złamania mechanizm.

Więc nie używaj tego!

Zamiast tego po prostu użyj silnego kryptograficznie generatora liczb losowych ( System.Security.Cryptography.RNGCryptoServiceProvider) i uzyskaj co najmniej 256 bitów surowej entropii.

Cała reszta, podobnie jak wiele innych odpowiedzi.

Zachłanny
źródło
6
Całkowicie się zgadzam, o ile wiem, identyfikatory GUID nigdy nie były projektowane tak, aby były mocne kryptograficznie i niemożliwe do odgadnięcia.
Jan Soltis
5
dobrze powiedziane, AFAIK MSDN wyraźnie stwierdza, że ​​GUID nie powinien być używany do bezpieczeństwa.
dr. zły
2
Identyfikatory UUID w wersji 4 są używane w systemie Windows od 2000 r .: W jaki sposób generowane są identyfikatory GUID platformy .NET 4? - Przepełnienie stosu . Mają w nich 122 losowe bity, co moim zdaniem jest zgodne z zaleceniami NIST. Wystąpiła bardzo zła luka w ataku lokalnym, która według CryptGenRandom - Wikipedia została naprawiona w Vista i XP do 2008 roku. Więc gdzie widzisz problemy z obecnym używaniem identyfikatorów GUID?
nealmcb
4
Ten blog „Old New Thing” opisuje wycofane identyfikatory UUID w wersji 1 i cytuje wersję roboczą Internetu (coś, czego nigdy nie powinno się robić), która wygasła w 1998 roku, 10 lat przed wpisem na blogu. W przyszłości byłbym wobec nich sceptyczny. Toczyliśmy te bitwy dawno temu i wydaje się, że większość z nich wygraliśmy. Nadal zgadzam się, że użycie czystego wywołania API do źródła krypto-losowego jest znacznie lepsze, ale nie jest tak trudne w przypadku identyfikatorów GUID / UUID w wersji 4.
nealmcb
1
Na ile jest to warte, to nie odpowiada na pytanie „jak zresetować hasło”. Właśnie zwymiotowałeś świetne punkty dotyczące identyfikatorów GUID.
Rex Whitten
67

EDYCJA 2012/05/22: W ramach kontynuacji tej popularnej odpowiedzi sam już nie używam identyfikatorów GUID w tej procedurze. Podobnie jak w przypadku innej popularnej odpowiedzi, teraz używam własnego algorytmu haszującego do generowania klucza do wysłania w adresie URL. Ma to tę zaletę, że jest również krótszy. Zajrzyj do System.Security.Cryptography, aby je wygenerować, której zwykle używam również SALT.

Po pierwsze, nie resetuj od razu hasła użytkownika.

Po pierwsze, nie resetuj hasła użytkownika od razu, gdy o to poprosi. Jest to naruszenie bezpieczeństwa, ponieważ ktoś mógłby odgadnąć adresy e-mail (tj. Twój adres e-mail w firmie) i zresetować hasła w razie potrzeby. Najlepsze praktyki w dzisiejszych czasach zwykle obejmują łącze „potwierdzające” wysłane na adres e-mail użytkownika, potwierdzające chęć zresetowania go. To łącze jest miejscem, w którym chcesz wysłać unikalny link do klucza. Moje wysyłam z linkiem takim jak:domain.com/User/PasswordReset/xjdk2ms92

Tak, ustaw limit czasu dla łącza i zapisz klucz i limit czasu na swoim zapleczu (i soli, jeśli go używasz). 3-dniowe przerwy są normą i pamiętaj, aby powiadomić użytkownika o 3 dniach na poziomie sieci, gdy zażądają resetowania.

Użyj unikalnego klucza skrótu

Moja poprzednia odpowiedź mówiła, że ​​używam identyfikatora GUID. Teraz edytuję to, aby doradzić wszystkim, aby używali losowo generowanego skrótu, np. Używając RNGCryptoServiceProvider. I pamiętaj, aby wyeliminować wszystkie „prawdziwe słowa” z hasha. Przypominam sobie specjalny telefon o 6 rano, kiedy kobieta otrzymała pewne słowo „c” w swoim kluczu „przypuszczalnie przypadkowym”, które zrobił programista. No!

Cała procedura

  • Użytkownik klika hasło „resetuj”.
  • Użytkownik jest proszony o e-mail.
  • Użytkownik wpisuje e-mail i klika wyślij. Nie potwierdzaj ani nie odrzucaj wiadomości e-mail, ponieważ jest to również zła praktyka. Powiedz po prostu: „Wysłaliśmy prośbę o zresetowanie hasła, jeśli adres e-mail jest zweryfikowany”. lub coś podobnego.
  • Tworzysz hash na podstawie RNGCryptoServiceProvider, przechowujesz go jako oddzielną jednostkę w ut_UserPasswordRequeststabeli i łączysz z powrotem do użytkownika. Dzięki temu możesz śledzić stare żądania i informować użytkownika, że ​​starsze linki wygasły.
  • Wyślij link na e-mail.

Użytkownik otrzymuje łącze, lubi je http://domain.com/User/PasswordReset/xjdk2ms92i klika.

Jeśli link jest zweryfikowany, poprosisz o nowe hasło. Proste, a użytkownik może ustawić własne hasło. Lub ustaw tutaj swoje własne tajemnicze hasło i poinformuj ich o nowym haśle tutaj (i wyślij je e-mailem).

eduncan911
źródło
1
Zastanawiałem się, jeśli rzeczywiste hasło użytkownika jest zaszyfrowane, po co generować nowy klucz HASH? Czy wysłanie e-maila do użytkownika z linkiem do zresetowania hasła i przekazanie zaszyfrowanego hasła nie byłoby poprawne? Zaszyfrowanego hasła nie można cofnąć, gdy użytkownik kliknie łącze, serwer otrzyma zaszyfrowane hasło, porówna je z faktycznie przechowywanym, a następnie umożliwi użytkownikowi zmianę hasła.
Daniel
I kolejną całkiem dobrą rzeczą jest to, że nie musisz ustawiać limitu czasu, po zmianie hasła przez użytkownika stary link nie będzie już automatycznie ważny, ponieważ zaszyfrowane hasło przechowywane w bazie danych zostało zmienione.
Daniel
@Daniel to naprawdę zły pomysł. Myślę, że w Google trzeba wpisać „ataki siłowe”. Ponadto powodem, dla którego CHCESZ, aby wygasło, jest to, że czyjeś e-maile zostaną przejęte rok później (i nigdy ich nie zresetują), haker zyskuje prawo do zmiany hasła.
eduncan911
@ Educan911. Znam ataki brutalnej siły, ale także, aby mieć dostęp do klucza Hashed, osoba Bad Intented musi mieć dostęp do poczty elektronicznej, a jeśli ma do niego dostęp, nie ma potrzeby przywracania zaszyfrowanego hasła. Ponadto, aby uczynić to prawie niemożliwym, możesz zaszyfrować hasło, a nawet lepiej, zaszyfrować hasło czymś więcej. Nie zgadzam się z tobą, po prostu próbuję zrobić burzę mózgów na ten temat
Daniel
8

Po pierwsze, musimy wiedzieć, co już wiesz o użytkowniku. Oczywiście masz nazwę użytkownika i stare hasło. Co jeszcze wiesz? Masz adres email? Czy masz dane dotyczące ulubionego kwiatu użytkownika?

Zakładając, że masz nazwę użytkownika, hasło i działający adres e-mail, musisz dodać dwa pola do swojej tabeli użytkowników (zakładając, że jest to tabela bazy danych): datę o nazwie new_passwd_expire i ciąg new_passwd_id.

Zakładając, że masz adres e-mail użytkownika, gdy ktoś żąda zresetowania hasła, aktualizujesz tabelę użytkowników w następujący sposób:

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

Następnie wyślij wiadomość e-mail do użytkownika pod tym adresem:

Drogi taki a taki

Ktoś zażądał nowego hasła do konta użytkownika <nazwa użytkownika> w <nazwa Twojej witryny>. Jeśli poprosiłeś o zresetowanie hasła, kliknij ten link:

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

Jeśli ten link nie działa, możesz przejść do http://example.com/yourscript.lang i wprowadzić następujące informacje w formularzu: <new_password_id>

Jeśli nie prosiłeś o zresetowanie hasła, możesz zignorować tę wiadomość e-mail.

Dzięki, yada yada

Teraz kodowanie yourscript.lang: Ten skrypt potrzebuje formularza. Jeśli aktualizacja zmiennej przekazała adres URL, formularz po prostu zapyta o nazwę użytkownika i adres e-mail użytkownika. Jeśli aktualizacja nie zostanie pomyślnie zakończona, zostanie wyświetlony monit o podanie nazwy użytkownika, adresu e-mail i kodu identyfikacyjnego przesłanego w wiadomości e-mail. Pytasz również o nowe hasło (oczywiście dwukrotnie).

Aby zweryfikować nowe hasło użytkownika, należy sprawdzić, czy nazwa użytkownika, adres e-mail i kod identyfikacyjny są zgodne, czy żądanie nie wygasło, a dwa nowe hasła są zgodne. Jeśli się powiedzie, zmień hasło użytkownika na nowe hasło i wyczyść pola resetowania hasła z tabeli użytkowników. Pamiętaj również, aby wylogować użytkownika / wyczyścić wszelkie pliki cookie związane z logowaniem i przekierować użytkownika na stronę logowania.

Zasadniczo pole new_passwd_id to hasło, które działa tylko na stronie resetowania hasła.

Jedna potencjalna poprawa: możesz usunąć <nazwa użytkownika> z wiadomości e-mail. „Ktoś zażądał zresetowania hasła dla konta pod tym adresem e-mail…” W ten sposób nazwa użytkownika jest znana tylko użytkownikowi, jeśli wiadomość e-mail zostanie przechwycona. Nie zacząłem w ten sposób, ponieważ jeśli ktoś atakuje konto, to już zna nazwę użytkownika. To dodatkowe niejasności powstrzymują ataki typu man-in-the-middle na wypadek, gdyby ktoś złośliwy przechwycił wiadomość e-mail.

Jeśli chodzi o Twoje pytania:

generowanie losowego ciągu: Nie musi być skrajnie losowy. Dowolny generator GUID lub nawet md5 (concat (salt, current_timestamp ())) jest wystarczający, gdy sól jest czymś w rekordzie użytkownika, takim jak konto znacznika czasu zostało utworzone. To musi być coś, czego użytkownik nie może zobaczyć.

timer: Tak, potrzebujesz tego tylko po to, aby zachować rozsądną bazę danych. Naprawdę potrzebny jest nie więcej niż tydzień, ale co najmniej 2 dni, ponieważ nigdy nie wiadomo, jak długo może potrwać opóźnienie wiadomości e-mail.

Adres IP: ponieważ wiadomość e-mail może być opóźniona o kilka dni, adres IP jest przydatny tylko do logowania, a nie do weryfikacji. Jeśli chcesz to zarejestrować, zrób to, w przeciwnym razie nie będziesz go potrzebować.

Ekran resetowania: patrz wyżej.

Mam nadzieję, że to wszystko. Powodzenia.

jmucchiello
źródło
Czy potencjalny napastnik nie byłby w stanie użyć MD5 aktualnego datownika, aby się dostać?
George Stocker
Zdecydowanie odradzam wysyłanie hasła pocztą elektroniczną przez sieć. Większość użytkowników nie usuwa tych wiadomości e-mail, co stanowi naruszenie bezpieczeństwa - niektórzy z nich będą chcieli po prostu skopiować i wkleić je za każdym razem ze swoich „ulubionych” wiadomości e-mail. Co się stanie, jeśli wygasła ważność certyfikatu serwera poczty firmy użytkownika i ruch jest podsłuchiwany? Aby zminimalizować to możliwe naruszenie, należy (1) ustawić krótki czas wygaśnięcia tego konkretnego hasła - 1 godzinę i (2) zmusić użytkownika do jego aktualizacji przy następnym logowaniu.
Ognyan Dimitrov,
Ognyan, hasło wysłane w e-mailu działa tylko raz. Muszą zmienić swoje hasło po zalogowaniu, a e-mail nie zawiera nazwy logowania użytkownika. Więc nie, nie mogą go po prostu skopiować i wkleić za każdym razem. Brak usunięcia wiadomości e-mail nie jest kwestią bezpieczeństwa, ponieważ jest to po prostu bezsensowny ciąg liter / cyfr, który po zresetowaniu hasła NIC nie da atakującemu.
jmucchiello,
3

Identyfikator GUID wysłany na adres e-mail rekordu jest prawdopodobnie wystarczający dla większości aplikacji typu run-of-the-mill - z jeszcze lepszym limitem czasu.

W końcu, jeśli skrzynka e-mail użytkownika została przejęta (np. Haker ma login / hasło do adresu e-mail), niewiele można z tym zrobić.

EJ Brennan
źródło
2

Możesz wysłać e-mail do użytkownika z linkiem. To łącze zawierałoby trudny do odgadnięcia ciąg (taki jak GUID). Po stronie serwera możesz również przechowywać ten sam ciąg, który wysłałeś do użytkownika. Teraz, gdy użytkownik naciśnie link, możesz znaleźć w swoim wpisie db ten sam tajny ciąg i zresetować jego hasło.

Sergej Andrejev
źródło
Więcej szczegółów byłoby pomocnych.
George Stocker
2

1) Do wygenerowania unikalnego identyfikatora można użyć bezpiecznego algorytmu haszowania. 2) załączony timer? Czy chodziło Ci o wygaśnięcie linku resetowania pwd? Tak, możesz mieć ustawiony okres ważności 3) Możesz poprosić o dodatkowe informacje inne niż identyfikator e-mail w celu potwierdzenia. Na przykład data urodzenia lub pytania zabezpieczające 4) Możesz również wygenerować losowe znaki i poprosić o ich wprowadzenie również wraz z żądaniem .. aby upewnić się, że żądanie hasła nie jest zautomatyzowane przez niektóre programy szpiegowskie lub podobne rzeczy

Java Guy
źródło
0

Myślę, że przewodnik firmy Microsoft dotyczący ASP.NET Identity to dobry początek.

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

Kod, którego używam dla ASP.NET Identity:

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
źródło