Scenario:
You have an existing ASP.NET MVC project that using previous version of ASP.NET Identity (v1.0.0). You want to use new features which announced in ASP.NET Identity 2.0 RTM versions:
- Two-Factor Authentication
- Account Lockout
- Account Confirmation
- Password Reset
- Security Stamp (Sign out everywhere)
- Make the type of Primary Key be extensible for Users and Roles
- Support IQueryable on Users and Roles
- Delete User account
- IdentityFactory Middleware/ CreatePerOwinContext
- Indexing on Username
- Enhanced Password Validator
For the full announcement please visit the official link here Announcing RTM of ASP.NET Identity 2.0.0
Solution:
As mentioned in the release, you need to migrate existing project using ASP.NET Identity 1.0 to 2.0 version. There are some guide here you can follow:
Updating ASP.NET applications from ASP.NET Identity 1.0 to 2.0.0-alpha1
Upgrading an Existing Project from ASP.NET Identity 1.0 to 2.0
But these guides is not completely, just steps to migrate ASP.NET Identity Code First database from v1.0 to v2.0.
So this article will describe complete steps to migrate your existing MVC project using ASP.NET Identity 1.0 to 2.0 RTM which include “Password Reset” function.
Existing project
My current MVC project using ASP.NET Identity 1.0 as noted in packages.config here:
<package id="Microsoft.AspNet.Identity.Core" version="1.0.0" targetFramework="net45" /> <package id="Microsoft.AspNet.Identity.EntityFramework" version="1.0.0" targetFramework="net45" /> <package id="Microsoft.AspNet.Identity.Owin" version="1.0.0" targetFramework="net45" />
The AspNetUsers table in ASP.NET Identity 1.0 has 5 columns: Id, UserName, PasswordHash, SecurityStamp and Discriminator:
Migrating ASP.NET Identity Database from 1.0 to 2.0
Step 1: Update ASP.NET Identity Packages
Open Package Manager Console then run following command in order
PM> Update-Package Microsoft.AspNet.Identity.Core
PM> Update-Package Microsoft.AspNet.Identity.EntityFramework
Restart Visual Studio then run this command in Package Manager Console
PM> Update-Package Microsoft.AspNet.Identity.Owin
Now run the application an try to logging on old user, you will see the error:
Server Error in ‘/’ Application.
The model backing the ‘ApplicationDbContext’ context has changed since the database was created. This could have happened because the model used by ASP.NET Identity Framework has changed or the model being used in your application has changed. To resolve this issue, you need to update your database. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=301867). Before you update your database using Code First Migrations, please disable the schema consistency check for ASP.NET Identity by setting throwIfV1Schema = false in the constructor of your ApplicationDbContext in your application.
public ApplicationDbContext() : base(“ApplicationServices”, throwIfV1Schema:false)
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: The model backing the ‘ApplicationDbContext’ context has changed since the database was created. This could have happened because the model used by ASP.NET Identity Framework has changed or the model being used in your application has changed. To resolve this issue, you need to update your database. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=301867). Before you update your database using Code First Migrations, please disable the schema consistency check for ASP.NET Identity by setting throwIfV1Schema = false in the constructor of your ApplicationDbContext in your application.
public ApplicationDbContext() : base(“ApplicationServices”, throwIfV1Schema:false)
Source Error:
|
Source File: c:\Users\Administrator\Documents\Visual Studio 2013\Projects\MvcIdentity2Migration\MvcIdentity2Migration\Models\IdentityModels.cs Line: 12
To resolve this error, we need to migrate existing code first database to 2.0 version
Step 2: Migrate Code First database.
Open IdentityModels.cs class file then change to the ApplicationDbContext constructor as below:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
Open Package Manager Console then run these command
PM> Enable-Migrations
PM> Add-Migration Update1
Here Update1 is an identifier you can have for this migration. The migrations code generated should as below
public partial class Update1 : DbMigration { public override void Up() { RenameColumn(table: "dbo.AspNetUserClaims", name: "User_Id", newName: "UserId"); RenameIndex(table: "dbo.AspNetUserClaims", name: "IX_User_Id", newName: "IX_UserId"); DropPrimaryKey("dbo.AspNetUserLogins"); AddColumn("dbo.AspNetUsers", "Email", c => c.String(maxLength: 256)); AddColumn("dbo.AspNetUsers", "EmailConfirmed", c => c.Boolean(nullable: false)); AddColumn("dbo.AspNetUsers", "PhoneNumber", c => c.String()); AddColumn("dbo.AspNetUsers", "PhoneNumberConfirmed", c => c.Boolean(nullable: false)); AddColumn("dbo.AspNetUsers", "TwoFactorEnabled", c => c.Boolean(nullable: false)); AddColumn("dbo.AspNetUsers", "LockoutEndDateUtc", c => c.DateTime()); AddColumn("dbo.AspNetUsers", "LockoutEnabled", c => c.Boolean(nullable: false)); AddColumn("dbo.AspNetUsers", "AccessFailedCount", c => c.Int(nullable: false)); AlterColumn("dbo.AspNetRoles", "Name", c => c.String(nullable: false, maxLength: 256)); AlterColumn("dbo.AspNetUsers", "UserName", c => c.String(nullable: false, maxLength: 256)); AddPrimaryKey("dbo.AspNetUserLogins", new[] { "LoginProvider", "ProviderKey", "UserId" }); CreateIndex("dbo.AspNetRoles", "Name", unique: true, name: "RoleNameIndex"); CreateIndex("dbo.AspNetUsers", "UserName", unique: true, name: "UserNameIndex"); DropColumn("dbo.AspNetUsers", "Discriminator"); } public override void Down() { AddColumn("dbo.AspNetUsers", "Discriminator", c => c.String(nullable: false, maxLength: 128)); DropIndex("dbo.AspNetUsers", "UserNameIndex"); DropIndex("dbo.AspNetRoles", "RoleNameIndex"); DropPrimaryKey("dbo.AspNetUserLogins"); AlterColumn("dbo.AspNetUsers", "UserName", c => c.String()); AlterColumn("dbo.AspNetRoles", "Name", c => c.String(nullable: false)); DropColumn("dbo.AspNetUsers", "AccessFailedCount"); DropColumn("dbo.AspNetUsers", "LockoutEnabled"); DropColumn("dbo.AspNetUsers", "LockoutEndDateUtc"); DropColumn("dbo.AspNetUsers", "TwoFactorEnabled"); DropColumn("dbo.AspNetUsers", "PhoneNumberConfirmed"); DropColumn("dbo.AspNetUsers", "PhoneNumber"); DropColumn("dbo.AspNetUsers", "EmailConfirmed"); DropColumn("dbo.AspNetUsers", "Email"); AddPrimaryKey("dbo.AspNetUserLogins", new[] { "UserId", "LoginProvider", "ProviderKey" }); RenameIndex(table: "dbo.AspNetUserClaims", name: "IX_UserId", newName: "IX_User_Id"); RenameColumn(table: "dbo.AspNetUserClaims", name: "UserId", newName: "User_Id"); } }
Next we need to persist it to the database. Run the command ‘Update-Database –verbose’. The verbose flag lets you view the SQL queries generated. This should pass as expected.
The AspNetUsers in ASP.NET Identity 2.0 is as image below
Now run application and try to logon with old user. It will logged on as expected.
Adding “Password Reset” function to existing project.
Step 1: Right click on App_Start folder then add new IdentityConfig.cs with code bellow
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store) { } 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, }; // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "Security Code", BodyFormat = "Your security code is: {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity")); } return manager; } } public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. MailMessage email = new MailMessage("xxx@hotmail.com", message.Destination); email.Subject = message.Subject; email.Body = message.Body; email.IsBodyHtml = true; var mailClient = new SmtpClient("smtp.live.com", 587) { Credentials = new NetworkCredential("xxx@hotmail.com", "password"), EnableSsl = true }; return mailClient.SendMailAsync(email); } } public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your sms service here to send a text message. return Task.FromResult(0); } }
Step 2: Open the IdentityModels.cs class file then change code as below:
public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here return userIdentity; } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema:false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
Step 3: Open the Startup.Auth.cs class file then add these following code in the start of ConfigureAuth method
public void ConfigureAuth(IAppBuilder app) { // Configure the db context and user manager to use a single instance per request app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); // Enable the application to use a cookie to store information for the signed in user app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); // Use a cookie to temporarily store information about a user logging in with a third party login provider app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Uncomment the following lines to enable logging in with third party login providers //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(); }
Step 4: Change AccountViewModels.cs class file. Open the file then add to view model for the Reset Password function
public class ResetPasswordViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } public string Code { get; set; } } public class ForgotPasswordViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } }
Step 5: Change the AccountController.cs class file
Modify AccountController constructor method as bellow
private ApplicationUserManager _userManager; public AccountController() { } public AccountController(ApplicationUserManager userManager) { UserManager = userManager; } public ApplicationUserManager UserManager { get { return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>(); } private set { _userManager = value; } }
Remember to add this line of code in using code
using Microsoft.AspNet.Identity.Owin;
Step 6: Add Forgot Password Actions
// GET: /Account/ForgotPassword [AllowAnonymous] public ActionResult ForgotPassword() { return View(); } // // POST: /Account/ForgotPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model) { if (ModelState.IsValid) { var user = await UserManager.FindByEmailAsync(model.Email); if (user == null || !(await UserManager.IsEmailConfirmedAsync(user.Id))) { ModelState.AddModelError("", "The user either does not exist or is not confirmed."); return View(); } // 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 var code = await UserManager.GeneratePasswordResetTokenAsync(user.Id); var callbackUrl = Url.Action("ResetPassword", "Account", new { UserId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "Reset Password", "Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>"); return RedirectToAction("ForgotPasswordConfirmation", "Account"); } // If we got this far, something failed, redisplay form return View(model); } // // GET: /Account/ForgotPasswordConfirmation [AllowAnonymous] public ActionResult ForgotPasswordConfirmation() { return View(); }
Step 7: Add Reset Password Actions
// GET: /Account/ResetPassword [AllowAnonymous] public ActionResult ResetPassword(string code) { if (code == null) { return View("Error"); } return View(); } // // POST: /Account/ResetPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model) { if (ModelState.IsValid) { var user = await UserManager.FindByEmailAsync(model.Email); if (user == null) { ModelState.AddModelError("", "No user found."); return View(); } IdentityResult result = await UserManager.ResetPasswordAsync(user.Id, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction("ResetPasswordConfirmation", "Account"); } else { AddErrors(result); return View(); } } // If we got this far, something failed, redisplay form return View(model); } // // GET: /Account/ResetPasswordConfirmation [AllowAnonymous] public ActionResult ResetPasswordConfirmation() { return View(); }
Step 8: Add view ForgotPassword.cshtml
@model MvcIdentity2Migration.Models.ForgotPasswordViewModel @{ ViewBag.Title = "Forgot your password?"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() <h4>Enter your email.</h4> <hr /> @Html.ValidationSummary("", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" class="btn btn-default" value="Email Link" /> </div> </div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
Step 9: Add view ForgotPasswordConfirmation.cshtml
@{ ViewBag.Title = "Forgot Password Confirmation"; } <hgroup class="title"> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> Please check your email to reset your password. </p> </div>
Step 10: Add view ResetPassword.cshtml
@model MvcIdentity2Migration.Models.ResetPasswordViewModel @{ ViewBag.Title = "Reset password"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() <h4>Reset your password.</h4> <hr /> @Html.ValidationSummary("", new { @class = "text-danger" }) @Html.HiddenFor(model => model.Code) <div class="form-group"> @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) </div> </div> <div class="form-group"> @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) </div> </div> <div class="form-group"> @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" class="btn btn-default" value="Reset" /> </div> </div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
Step 11: Add view ResetPasswordConfirmation.cshtml
@{ ViewBag.Title = "Reset password confirmation"; } <hgroup class="title"> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) </p> </div>
Testing Password Reset function
Open AspNetUsers table then make sure old account has valid email and EmailConfirmed value is set to True
Run the application and test your Password Reset Function
Forgot Password page
Email Received
Reset Password page
Done!
Hope this help!
Thanks. Finally solved my problem after reviewing many sites. Your solution is the best. Thanks for your help. You really helped me. Appreciate it a lot
Many thanks. Just what i was looking for after i upgraded my MVC4 app to MVC5!
Thank you very much for this. You saved my life big time.
Nicely done!!
Good stuff, here’s what we did in case it helps anyone.
http://sftool.blogspot.com/2015/02/upgrade-to-aspnet-identity.html
thanks! thank you very much!