basquang™ on clouds

July 11, 2014

[MVC]: Migrate an existing project using ASP.NET Identity 1.0 to 2.0 Complete

Filed under: Entity Framework (EF),MVC — basquang @ 4:13 PM
Tags: ,

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: 

 

Line 10:     public class ApplicationDbContext : IdentityDbContext<ApplicationUser>

Line 11:     {

Line 12:         public ApplicationDbContext()

Line 13:             : base("DefaultConnection")

Line 14:         {


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!

Advertisements

6 Comments »

  1. 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

    Comment by Jeetash — July 16, 2014 @ 2:15 PM | Reply

  2. Many thanks. Just what i was looking for after i upgraded my MVC4 app to MVC5!

    Comment by JdeC — October 17, 2014 @ 3:10 AM | Reply

  3. Thank you very much for this. You saved my life big time.

    Comment by S Ng — March 20, 2015 @ 12:41 AM | Reply

  4. Nicely done!!

    Comment by Justin — March 21, 2015 @ 11:54 AM | Reply

  5. Good stuff, here’s what we did in case it helps anyone.
    http://sftool.blogspot.com/2015/02/upgrade-to-aspnet-identity.html

    Comment by Ivan — March 31, 2015 @ 1:27 AM | Reply

  6. thanks! thank you very much!

    Comment by magno — February 20, 2016 @ 7:27 AM | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: