Pracuję nad aplikacją w ASP.NET i zastanawiałem się konkretnie, w jaki sposób mógłbym zaimplementować Password Reset
funkcję, 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 pytania są pomijane 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.
źródło
Odpowiedzi:
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:
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.
źródło
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
RNGCryptoServiceProvider
, przechowujesz go jako oddzielną jednostkę wut_UserPasswordRequests
tabeli i łączysz z powrotem do użytkownika. Dzięki temu możesz śledzić stare żądania i informować użytkownika, że starsze linki wygasły.Użytkownik otrzymuje łącze, lubi je
http://domain.com/User/PasswordReset/xjdk2ms92
i 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).
źródło
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:
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.
źródło
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ć.
źródło
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.
źródło
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
źródło
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; } }
źródło