In previous posts, we’ve got familiar with ASP.NET Web API and ASP.NET Identity. We can use ASP.NET Identity with multiple types of clients such as the desktop app, web app or mobile app for registering and authenticating users. The user will open an account with his email and password and then authenticate himself with services with registered info. If we don’t want to force user enter login data each time he accesses our services, we can get a token and use that token as the authentication key for any call to the server. Today I would like to extend our ASP.NET Web API with “Forgot Password” function so that user can reset his password in case he doesn’t remember it anymore.
1. Prerequisites
I warmly recommend reading these following posts again before getting start with building up new features in our services.
– C# – ASP.NET Web API and ASP.NET Identity
– Android – Introduction to ASP.NET Identity
I use Mailgun as email service for sending reset code to user’s email. You can use your own email provider by replacing code for sending the email with SMTP. It’s pretty easy.
2. Forgot password
We all know how a “Forgot password” in web app works. We go to the websites, say “Hey, I forgot my password. Let’s me reset it”. The websites will email us a link (reset code would be embedded in this link) which we can click on and enter a new password. In ASP.NET Web API, we can do in that way. However integrating that website callback link into a mobile app is not so easy. We have to embed a web browser component, show the website, check if the user enters a new password or cancel the action (difficult part). So I would like to implement “Forgot Password” as following steps.
1. The user will trigger “Forgot password” with his identity (in our case, his email).
2. We’ll email his reset code.
3. He’ll use that reset code for changing his password.
4. He can now log in with his new password.
2.1 Forgot password action
To let user trigger “Forgot password”, we have to create an action in AccountController. The action should be accessible with an anonymous call because the user doesn’t have his password anymore.
[HttpPost] [AllowAnonymous] [Route("ForgotPassword")] public async Task<IHttpActionResult> ForgotPassword(ForgotPasswordViewModel model) { if (ModelState.IsValid) { var user = await UserManager.FindByNameAsync(model.Email); // If user has to activate his email to confirm his account, the use code listing below //if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id))) //{ // return Ok(); //} if (user == null) { return Ok(); } // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771 // Send an email with this link string code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); await UserManager.SendEmailAsync(user.Id, "Reset Password", $"Please reset your password by using this {code}"); return Ok(); } // If we got this far, something failed, redisplay form return BadRequest(ModelState); }
The action receives a ForgotPasswordViewModel as parameter. This model contains only the email of a user. If user hasn’t been found, we just give Ok() result back. If you want you can give another informative message back. However, it’ll give hacker hints to get the correct user from our database.
public class ForgotPasswordViewModel { public string Email { get; set; } }
2.2 EmailService
The ForgotPassword function will email the reset code to our users. To make SendEmailAsync works, we have to create a EmailService and set it for UserManager.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { const string apiKey = "key-ef7a2525b9a4141408b40cd4d4e438e0"; const string sandBox = "sandbox5c2ed57ac7b94f0ea5d372f3194b026c.mailgun.org"; byte[] apiKeyAuth = Encoding.ASCII.GetBytes($"api:{apiKey}"); var httpClient = new HttpClient { BaseAddress = new Uri("https://api.mailgun.net/v3/") }; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(apiKeyAuth)); var form = new Dictionary<string, string> { ["from"] = "postmaster@sandbox5c2ed57ac7b94f0ea5d372f3194b026c.mailgun.org", ["to"] = message.Destination, ["subject"] = message.Subject, ["text"] = message.Body }; HttpResponseMessage response = httpClient.PostAsync(sandBox + "/messages", new FormUrlEncodedContent(form)).Result; return Task.FromResult((int)response.StatusCode); } }
As I mentioned above, I use Mailgun for sending the emails to registered users. You can use your own SMTP server or any other email services. When the email arrives, it looks like as the following image.
Remember to set EmailService for UserManager, otherwise no email will be sent out. In IdentityConfig.cs, create new instance of EmailService and set it to UserManager.
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; manager.EmailService = new EmailService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity")); } return manager; }
Note: MailGun service doesn’t allow to send email to any recipient with development api key anymore. Use your own MailGun api key with your authorized recipients to test the function or just rewrite the EmailService to authenticate yourself with SMTP server to send email for Forgot Password function.
2.3 ResetPassword
The user has now the code for resetting his password. We need an action to check this code and receive the new password. So in AccountController.cs, create ResetPassword action receving ResetPasswordViewModel as his parameter.
[HttpPost] [AllowAnonymous] [Route("ResetPassword")] public async Task<IHttpActionResult> ResetPassword(ResetPasswordViewModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = await UserManager.FindByNameAsync(model.Email); if (user == null) { // Don't reveal that the user does not exist return Ok(); } var result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password); if (result.Succeeded) { return Ok(); } return Ok(); }
Of course, the ResetPasswordViewModel must be sent over a HTTP Post to server.
public class ResetPasswordViewModel { public string Code { get; set; } public string ConfirmPassword { get; set; } public string Email { get; set; } public string Password { get; set; } }
2.4 Client
The code listing below shows the steps how the client should call the service to reset the password of a user.
private string email = "20131226@hintdesk.com"; public override void Run() { InitializeHttpClient(); Register().Wait(); ForgotPasswordToGetCode().Wait(); ReadCodeFromEmailAndResetPassword().Wait(); GetToken(newSecurePassword).Wait(); } private async Task ForgotPasswordToGetCode() { Console.WriteLine("--- Forgot password to get code ---"); Console.WriteLine(); Console.WriteLine("Step 2: Get code..."); responseMessage = await httpClient.PostAsJsonAsync("api/Account/ForgotPassword", new ForgotPasswordViewModel() { Email = email }); responseContent = await responseMessage.Content.ReadAsStringAsync(); Console.WriteLine(responseMessage.IsSuccessStatusCode ? "Step 2: Get code...Sucessfully" : responseContent); } private async Task GetToken(string password) { Console.WriteLine("Step 3: Get Token..."); responseMessage = await httpClient.PostAsync("Token", new FormUrlEncodedContent( new[] { new KeyValuePair<string, string>("grant_type", "password"), new KeyValuePair<string, string>("username", email), new KeyValuePair<string, string>("password", password), } )); tokenModel = await responseMessage.Content.ReadAsAsync<TokenModel>(); if (tokenModel != null && tokenModel.AccessToken != null) { Console.WriteLine("Step 3: Get Token...Sucessfully"); } else Console.WriteLine("Step 3: Get Token...Failed"); } private async Task ReadCodeFromEmailAndResetPassword() { Console.WriteLine("--- Read code from email and reset password ---"); Console.WriteLine(); Console.WriteLine("Step 4: Enter code from your email:"); string code = Console.ReadLine(); Console.WriteLine("Step 5: Reset password..."); responseMessage = await httpClient.PostAsJsonAsync("api/Account/ResetPassword", new ResetPasswordViewModel() { Code = code, ConfirmPassword = newSecurePassword, Email = email, Password = newSecurePassword }); responseContent = await responseMessage.Content.ReadAsStringAsync(); Console.WriteLine(responseMessage.IsSuccessStatusCode ? "Step 5: Reset password...Sucessfully" : responseContent); } private async Task Register() { Console.WriteLine("--- Register ---"); Console.WriteLine(); Console.WriteLine("Step 1: Register..."); responseMessage = await httpClient.PostAsJsonAsync("api/Account/Register", new RegisterBindingModel() { Email = email, Password = securePassword, ConfirmPassword = securePassword }); responseContent = await responseMessage.Content.ReadAsStringAsync(); Console.WriteLine(responseMessage.IsSuccessStatusCode ? "Step 1: Register...Sucessfully" : responseContent); }
3. Source code
It’s not difficult to build “Forgot password” feature into ASP.NET Web API Identity. The reset code in the demo above is pretty long, it is not suitable for using in a real application. You can extend the demo by writing your own UserManager and generate a short reset code format so that user can enter easily with his mobile device.
Source code: https://bitbucket.org/hintdesk/dotnet-asp.net-web-api-and-asp.net-identity
nice post
Hi! Thanks for the tutorial. I couldn’t understand when the code is verified. Can you help me?
@Leonardo: If you read the code carefully you’ll see that “api/Account/ResetPassword” verifies the reset-code
Hi.I am developing an app in which i want to send the edit text value to visual studio via post method.How can i send it .Edit text value should bind with visual studio parameter.Please help me
In your example you are passing ChangePasswordBindingModel, which will fail 100% of time since CurrentPassword would be empty (user forgot password).
So how do you verify the token and change the password at same time?
Also you are calling ReadCodeFromEmailAndResetPassword with following line:
httpClient.DefaultRequestHeaders.Add(“Authorization”,
$”Bearer {tokenModel.AccessToken}”);
This is not possible in this scenario since you need to be authorized to get token, which means you need username and password (again user forgot password).
@Rob: You’re right, I made a mistake when writing the post. I mentioned ChangePassword instead of ResetPassword. I edited the post and the workflow. It doesn’t required old password anymore.
nice post, but you must change FindByNameAsync by FindByEmailAsync
Great Article Helped me a lot!
how the reset password view will be should we create a view or not
Very nicely explained. Just one thing, we don’t need Confirm Password in ResetPasswordViewModel I guess that will be validated on client side and we will just pass this as one paramter.
new ResetPasswordViewModel()
{
Code = code,
ConfirmPassword = newSecurePassword,
Email = email,
Password = newSecurePassword
});