Co się stało to się nie odstanie i hasło się zapomniało. Co możemy z tym zrobić? Trzeba mieć wcześniej ustaloną formę komunikacji, może email, może sms, może adres pocztowy.
Jeśli mamy coś takiego, to spoko, jesteśmy uratowani i nasz użytkownik też. Teraz już pozostało już tylko kilka kroków które należy wykonać, aby użytkownik mógł bezpiecznie zmienić swoje zapomniane hasło, na nowe które zapomni kiedy indziej.
Robimy tak, przynajmniej ja tak robię:
Grzecznie prosimy o podanie nam adresu na który mamy przysłać (mając nadzieję, że chociaż tyle pamięta).
Po tym jak poda nam adres, warto sprawdzić czy mamy taki adres. Jeśli go posiadamy, generujemy jednorazowy token, który zostanie przypisany do właśnie tego przypomnienia i tego użytkownika.
Ja robię to tak (co nie oznacza, że jest to jedyny słuszny sposób czy najlepszy):
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: public ViewModelResult<RequestResetPasswordViewModel> RequestResetPassword(string emailAddress, string baseUrl)
2: {
3: var userToResetPassword = this.database.Users.Get(x => x.Email == emailAddress).FirstOrDefault();
4: if (userToResetPassword == null)
5: {
6: var result = new ViewModelResult<RequestResetPasswordViewModel>
7: {
8: Success = false,
9: ViewModel = null,
10: ErrorMessage =
11: Translations
12: .AccoutService_ResetPassword_Email_NotExists
13: };
14: return result;
15: }
16: else
17: {
18: var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
19: var passwordResetRequest = new PasswordResetRequest
20: {
21: Created = DateTime.Now,
22: Token = token,
23: Used = false,
24: UserId = userToResetPassword.Id
25: };
26: this.database.PasswordResetRequests.Insert(passwordResetRequest);
27: this.emailService.SendPasswordRequestEmail(
28: userToResetPassword.Email,
29: userToResetPassword.DisplayName,
30: baseUrl + token);
31: this.database.Save();
32: var result = new ViewModelResult<RequestResetPasswordViewModel>
33: {
34: Success = true,
35: ViewModel =
36: new RequestResetPasswordViewModel
37: {
38: Message =
39: Translations
40: .AccoutService_ResetPassword_EmailSend
41: }
42: };
43: return result;
44: }
45: }
Co jest powyżej? Szukam gościa z podanego email’a, jeśli go znajdę, to generuje token. W moim przypadku jest to guid bez kresek (może warto to przemyśleć). Później taką informację zapisuje do bazy, a potem korzystając z dostępu do serwisu mailowego wysyłam informację.
W baseUrl ukrywa się podstawowy link go którego zostanie doklejony token – średnio ładne, jestem otwarty na propozycje.
Produkcja mail jest raczej prosta:
1: public void SendPasswordRequestEmail(string email, string displayName, string url)
2: {
3: var smtpClient = new SmtpClient(this.communicationConfiguration.Host, this.communicationConfiguration.Port);
4:
5: smtpClient.EnableSsl = this.communicationConfiguration.MailSsl;
6: smtpClient.Credentials = new NetworkCredential(
7: this.communicationConfiguration.HostUser,
8: this.communicationConfiguration.HostPassword);
9:
10:
11: var from = new MailAddress(
12: this.communicationConfiguration.PasswordResetRequestAddress,
13: this.communicationConfiguration.PasswordResetRequestName);
14:
15: var to = new MailAddress(email, displayName);
16:
17: var bodyFormat = Translations.EmailsService_PasswordResetRequestBody_DisplayName_Url;
18: var subject = Translations.EmailsService_PasswordResetRequestSubject;
19: var body = string.Format(bodyFormat, displayName, url);
20:
21: var message = new MailMessage(from, to);
22:
23: message.Subject = subject;
24: message.Body = body;
25: smtpClient.Send(message);
26: }
Zwykły plain text do wysłania informacji, część ludzi ładnie wysyła piękne maile, ja tego na razie nie robię. Pewnie tak będzie, jak już zdobędę miliony użytkowników.
To w zasadzie pierwszy etap, kolejna część do obsługa takiego żądania. Ja, nowicjusz robię to tak:
1: [HttpGet]
2: public ActionResult ResetPassword(string token)
3: {
4: var serviceResult = this.accountService.IsRequestTokenValid(token);
5: var viewModel = new ResetPasswordViewModel2();
6: viewModel.Token = token;
7: this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
8:
9: return this.View("ResetPassword", viewModel);
10: }
Wybaczcie mi ResetPasswordViewModel2 – coś testowałem i mi zostało – oops!
Zwykły get do kontrolera, co ważne należy pamiętać żeby dostęp do tej metody był możliwy także dla nie zalogowanych użytkowników.
Sprawdzenie poprawności tokena wygląda tak:
1: public MessagesResult IsRequestTokenValid(string token)
2: {
3: var passwordResetRequest = this.database.PasswordResetRequests.Get(x => x.Token == token).FirstOrDefault();
4:
5: if (passwordResetRequest != null && passwordResetRequest.Used == false)
6: {
7: return new MessagesResult { Success = true };
8: }
9: else
10: {
11: return new MessagesResult
12: {
13: Success = false,
14: ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
15: };
16: }
17: }
Opcje z przetrzymywaniem tokena są różne, ja aktualnie postanowiłem trzymać token w bazie i po użyciu oznaczać go jako użyty. Można też go usuwać, można go też trzymać w tabeli użytkowników jako dodatkowe pole. Tysiąc programistów, tysiąc problemów, tysiąc rozwiązań.
Jeśli token jest poprawny renderowany jest widok i czekamy aż pan zapominalski wprowadzi nowe hasło, po tym jak się zdecyduje zostanie obsłużony w taki sposób:
1: [HttpPost]
2: [ValidateAntiForgeryToken]
3: public ActionResult ResetPassword(ResetPasswordViewModel2 model)
4: {
5: if (ModelState.IsValid)
6: {
7: var serviceResult = this.accountService.HandleResetPassword(model);
8: if (serviceResult.Success)
9: {
10: return this.RedirectToAction("Login", "Account");
11: }
12: this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
13: }
14:
15: return this.View("ResetPassword", model);
16: }
Oraz serwis, który wykonuje właściwą pracę:
1: public MessagesResult HandleResetPassword(ResetPasswordViewModel2 resetPasswordViewModel)
2: {
3: if (resetPasswordViewModel.NewPassword != resetPasswordViewModel.ConfirmNewPassword)
4: {
5: return new MessagesResult
6: {
7: Success = false,
8: ErrorMessage =
9: Translations.AccoutService_HandlePasswordReset_PasswordDoesNotMatch
10: };
11: }
12:
13: var passwordResetRequest =
14: this.database.PasswordResetRequests.Get(x => x.Token == resetPasswordViewModel.Token && x.Used == false)
15: .FirstOrDefault();
16:
17: if (passwordResetRequest == null)
18: {
19: return new MessagesResult
20: {
21: Success = false,
22: ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
23: };
24: }
25:
26: var user = this.database.Users.Get(x => x.Id == passwordResetRequest.UserId).First();
27: var hash = this.hashProvider.Get(resetPasswordViewModel.NewPassword);
28:
29: user.Hash = hash;
30: passwordResetRequest.Used = true;
31:
32: this.database.Users.Update(user);
33: this.database.PasswordResetRequests.Update(passwordResetRequest);
34:
35: this.database.Save();
36:
37: return new MessagesResult { Success = true };
38: }
Nie pamiętam czemu mam powtórzoną walidację pól view-modelu, pamiętam że miałem poważny powód żeby tak zrobić – deal with it!
Ale do rzeczy, jeśli podane hasła się zgadzają i token jest poprawny, generowany jest nowy hash dla hasła i jest ono zapisywane dla użytkownika, z którym powiązany jest token. Po wszystkim w moim przypadku MUSZĘ pamiętać o oznaczeniu tokena jako użytego.
Będzie?!