Upgrade to ASP.NET Identity
For our first post, we'll dive into some technical challenges we encountered when upgrading our Authentication.
Our site was using a custom Forms Authentication implementation before Microsoft released ASP.NET Identity 2.0. Since we recently upgraded our solution to MVC 5, we took the next step of upgrading the authentication components and benefit from the simplified 3rd party integration. If you happen to face a similar situation, use the steps below as a guideline to help you complete the upgrade.
Hope this helps!
Our site was using a custom Forms Authentication implementation before Microsoft released ASP.NET Identity 2.0. Since we recently upgraded our solution to MVC 5, we took the next step of upgrading the authentication components and benefit from the simplified 3rd party integration. If you happen to face a similar situation, use the steps below as a guideline to help you complete the upgrade.
Hope this helps!
Assumptions
- You're using Forms Authentication with ASP.NET
- You've upgraded your web solution to ASP.NET MVC5+
- You have a custom USER table with an Id of type integer
Outline
- Database
- Add new columns to the USER table
- Add new tables
- Update EDMX or Code-First POCO for USER table only
- Packages
- Add nuget packages to your solution
- Configuration
- Add a NEW database connection string to Web.Config
- Remove Forms Authentication & Membership sections
- (optional) Remove DotNetOpenAuth in case you were using it
- Code
- Add new identity folder/classes
- Setup your Startup classes
- (optional) Setup Unity IoC
- Create a separate default ASP.NET MVC 5 solution
- Manage Account ViewModels
- Manage Account Controller
- Manage Account Views
- Tips
Database
- Add new columns to the USER table
ALTER Table [dbo].[USER] ADD
[EmailConfirmed] [bit] NOT NULL DEFAULT ((1)),
[SecurityStamp] [nvarchar](max) NULL,
[PhoneNumber] [nvarchar](50) NULL,
[PhoneNumberConfirmed] [bit] NOT NULL DEFAULT ((0)),
[TwoFactorEnabled] [bit] NOT NULL DEFAULT ((0)),
[LockoutEndDateUtc] [datetime2](7) NULL,
[LockoutEnabled] [bit] NOT NULL DEFAULT ((0)),
[AccessFailedCount] [int] NOT NULL DEFAULT ((0));
- Add new tables
CREATE TABLE [dbo].[AspNetRoles] (
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (MAX) NOT NULL,
CONSTRAINT [PK_AspNetRoles] PRIMARY KEY CLUSTERED ([Id] ASC)
)
CREATE TABLE [dbo].[AspNetUserRoles] (
[UserId] BIGINT NOT NULL,
[RoleId] BIGINT NOT NULL,
CONSTRAINT [PK_AspNetUserRoles] PRIMARY KEY CLUSTERED ([UserId] ASC, [RoleId] ASC),
CONSTRAINT [FK_AspNetUserRoles_IdentityRole] FOREIGN KEY ([RoleId]) REFERENCES [dbo].[AspNetRoles] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_AspNetUserRoles_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE
)
CREATE TABLE [dbo].[AspNetUserLogins] (
[UserId] BIGINT NOT NULL,
[LoginProvider] NVARCHAR (128) NOT NULL,
[ProviderKey] NVARCHAR (128) NOT NULL,
CONSTRAINT [PK_AspNetUserLogins] PRIMARY KEY CLUSTERED ([UserId] ASC, [LoginProvider] ASC, [ProviderKey] ASC),
CONSTRAINT [FK_AspNetUserLogins_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE
)
CREATE TABLE [dbo].[AspNetUserClaims] (
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
[UserId] BIGINT NOT NULL,
[ClaimType] NVARCHAR (MAX) NULL,
[ClaimValue] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_AspNetUserClaims] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_AspNetUserClaims] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE
)
- Update your Entity Framework EDMX (or Code-First POCO) with the USER table changes
- DO NOT INCLUDE the newly added AspNet* tables since they'll be using a separate context (see Code section below)
Packages
- Include the following Nuget Packages
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.OAuth (for 3rd Party)
- Microsoft.Owin.Security.Google (for 3rd Party)
- Microsoft.AspNet.Identity.EntityFramework (dependency on EF6.1)
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.OAuth (for 3rd Party)
- Microsoft.Owin.Security.Google (for 3rd Party)
- Microsoft.AspNet.Identity.EntityFramework (dependency on EF6.1)
Configuration
- Add a NEW database connection string to Web.Config
<add name="TodoConnection" connectionString="Data Source=[TODO];initial catalog=[TODO];integrated security=True;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
- Remove the following Forms Authentication & Membership sections
- <authentication mode="Forms">...
- <membership>...
- <profile>...
- <roleManager enabled="false">...
- Add the following within the <system.webServer><modules> section
<add name="TodoConnection" connectionString="Data Source=[TODO];initial catalog=[TODO];integrated security=True;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
- <authentication mode="Forms">...
- <membership>...
- <profile>...
- <roleManager enabled="false">...
<remove name="FormsAuthenticationModule" />
<remove name="FormsAuthenticationModule" />
- (optional) Remove DotNetOpenAuth in case you were using it
- Remove project reference to DotNetOpenAuth
- Remove the following entries from web.config
- <section name="dotNetOpenAuth"...
- <dotNetOpenAuth>...
- <assemblyIdentity name="DotNetOpenAuth.AspNet"...
- <assemblyIdentity name="DotNetOpenAuth.Core"...
- Remove any related classes & views within the solution
- Remove project reference to DotNetOpenAuth
- Remove the following entries from web.config
- <section name="dotNetOpenAuth"...
- <dotNetOpenAuth>...
- <assemblyIdentity name="DotNetOpenAuth.AspNet"...
- <assemblyIdentity name="DotNetOpenAuth.Core"...
- Remove any related classes & views within the solution
Code
- Add a new folder i.e. AUTH within your solution
- Add the following new classes within that folder
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Create the MailMessage instance
var mm = new MailMessage();
mm.From = new MailAddress("todo@todo.todo");
mm.IsBodyHtml = true;
mm.To.Add(message.Destination);
mm.Subject = message.Subject;
mm.Body = message.Body;
var smtp = new SmtpClient();
return smtp.SendMailAsync(mm);
}
}
}
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Todo.Models;
namespace Todo.Auth
{
public class AspNetDbContext : IdentityDbContext<AspNetUser, AspNetRole, long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>
{
public AspNetDbContext()
: base("TodoConnection")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Ignore<User>();
// TODO: (optional) Map Entities to their tables
modelBuilder.Entity<AspNetUser>().ToTable("User");
modelBuilder.Entity<AspNetRole>().ToTable("AspNetRoles");
modelBuilder.Entity<AspNetUserClaim>().ToTable("AspNetUserClaims");
modelBuilder.Entity<AspNetUserLogin>().ToTable("AspNetUserLogins");
modelBuilder.Entity<AspNetUserRole>().ToTable("AspNetUserRoles");
// TODO: (optional) Set AutoIncrement-Properties
modelBuilder.Entity<AspNetUser>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<AspNetUserClaim>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<AspNetRole>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// TODO: (optional) Override some column mappings that do not match our default
modelBuilder.Entity<AspNetUser>().Property(r => r.EmailConfirmed).HasColumnName("AspNetEmailConfirmed");
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetRole : IdentityRole<long, AspNetUserRole> { }
}
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUser : IdentityUser<long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>
{
// TODO: Add your own custom properties
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(AspNetUserManager userManager)
{
var userIdentity = await userManager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserClaim : IdentityUserClaim<long> { }
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserLogin : IdentityUserLogin<long> { }
}
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.DataProtection;
namespace Todo.Auth
{
public class AspNetUserManager : UserManager<AspNetUser, long>
{
public AspNetUserManager(IUserStore<AspNetUser, long> store)
: base(store)
{
UserValidator = new UserValidator<AspNetUser, long>(this)
{
// TODO: Add your own custom logic
};
PasswordValidator = new PasswordValidator
{
// TODO: Add your own custom logic
};
EmailService = new EmailService();
PasswordHasher = new TodoPasswordHasher();
}
public override Task<AspNetUser> FindAsync(UserLoginInfo login)
{
return base.FindAsync(login);
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserRole : IdentityUserRole<long> { }
}
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class TodoPasswordHasher : PasswordHasher
{
public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
return (hashedPassword == HashPassword(providedPassword))
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
public override string HashPassword(string password)
{
// TODO: Add your own custom logic
}
}
}
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
return Task.FromResult(0);
}
}
}
- Setup your Startup classes
using Microsoft.Owin;
using Owin;
[assembly: OwinStartupAttribute(typeof(Todo.Startup))]
namespace Todo
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using Todo.Auth;
namespace Todo
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(
TimeSpan.FromMinutes(30),
(manager, user) => user.GenerateUserIdentityAsync(manager),
identity => long.Parse(identity.GetUserId()))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
...
}
}
}
- (optional) Setup Unity IoC in the UnityConfig class
// register asp.net identity user store
container.RegisterType<IUserStore<AspNetUser, long>, UserStore<AspNetUser, AspNetRole, long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>>(new PerRequestLifetimeManager(), new InjectionConstructor(new AspNetDbContext()));
// register asp.net identity user token provider
container.RegisterType<IUserTokenProvider<AspNetUser, long>, EmailTokenProvider<AspNetUser, long>>(new PerRequestLifetimeManager());
- Create a separate clean default ASP.NET MVC 5 solution as we're going to copy over some files from it
- Manage Account ViewModels
- Remove the existing Models/AccountModels.cs class
- Add a new default AccountViewModels.cs class (from ASP.NET MVC 5 default solution)
- Manage Account Controller
- Add a new default AccountController (from ASP.NET MVC 5 default solution)
- Inject the UserManager via the AccountController
public class AccountController : Controller
{
private AspNetUserManager UserManager;
public AccountController()
{
}
public AccountController(AspNetUserManager userManager, IUserTokenProvider<AspNetUser, long> userTokenProvider)
{
UserManager = userManager;
UserManager.UserTokenProvider = userTokenProvider;
}
- Manage Account Views
- Remove ALL existing views under /Views/Account
- Add all new views (from ASP.NET MVC 5 default solution)
- Note: Depending on your solution you may have to do some refactoring to the AccountViewModel / AccountController & Account Views
Tips
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Create the MailMessage instance
var mm = new MailMessage();
mm.From = new MailAddress("todo@todo.todo");
mm.IsBodyHtml = true;
mm.To.Add(message.Destination);
mm.Subject = message.Subject;
mm.Body = message.Body;
var smtp = new SmtpClient();
return smtp.SendMailAsync(mm);
}
}
}
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Todo.Models;
namespace Todo.Auth
{
public class AspNetDbContext : IdentityDbContext<AspNetUser, AspNetRole, long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>
{
public AspNetDbContext()
: base("TodoConnection")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Ignore<User>();
// TODO: (optional) Map Entities to their tables
modelBuilder.Entity<AspNetUser>().ToTable("User");
modelBuilder.Entity<AspNetRole>().ToTable("AspNetRoles");
modelBuilder.Entity<AspNetUserClaim>().ToTable("AspNetUserClaims");
modelBuilder.Entity<AspNetUserLogin>().ToTable("AspNetUserLogins");
modelBuilder.Entity<AspNetUserRole>().ToTable("AspNetUserRoles");
// TODO: (optional) Set AutoIncrement-Properties
modelBuilder.Entity<AspNetUser>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<AspNetUserClaim>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<AspNetRole>().Property(r => r.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
// TODO: (optional) Override some column mappings that do not match our default
modelBuilder.Entity<AspNetUser>().Property(r => r.EmailConfirmed).HasColumnName("AspNetEmailConfirmed");
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetRole : IdentityRole<long, AspNetUserRole> { }
}
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUser : IdentityUser<long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>
{
// TODO: Add your own custom properties
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(AspNetUserManager userManager)
{
var userIdentity = await userManager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserClaim : IdentityUserClaim<long> { }
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserLogin : IdentityUserLogin<long> { }
}
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.DataProtection;
namespace Todo.Auth
{
public class AspNetUserManager : UserManager<AspNetUser, long>
{
public AspNetUserManager(IUserStore<AspNetUser, long> store)
: base(store)
{
UserValidator = new UserValidator<AspNetUser, long>(this)
{
// TODO: Add your own custom logic
};
PasswordValidator = new PasswordValidator
{
// TODO: Add your own custom logic
};
EmailService = new EmailService();
PasswordHasher = new TodoPasswordHasher();
}
public override Task<AspNetUser> FindAsync(UserLoginInfo login)
{
return base.FindAsync(login);
}
}
}
using Microsoft.AspNet.Identity.EntityFramework;
namespace Todo.Auth
{
public class AspNetUserRole : IdentityUserRole<long> { }
}
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class TodoPasswordHasher : PasswordHasher
{
public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
return (hashedPassword == HashPassword(providedPassword))
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
public override string HashPassword(string password)
{
// TODO: Add your own custom logic
}
}
}
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
namespace Todo.Auth
{
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
// Plug in your sms service here to send a text message.
return Task.FromResult(0);
}
}
}
using Microsoft.Owin;
using Owin;
[assembly: OwinStartupAttribute(typeof(Todo.Startup))]
namespace Todo
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using Todo.Auth;
namespace Todo
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
// Configure the sign in cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity(
TimeSpan.FromMinutes(30),
(manager, user) => user.GenerateUserIdentityAsync(manager),
identity => long.Parse(identity.GetUserId()))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
...
}
}
}
// register asp.net identity user store
container.RegisterType<IUserStore<AspNetUser, long>, UserStore<AspNetUser, AspNetRole, long, AspNetUserLogin, AspNetUserRole, AspNetUserClaim>>(new PerRequestLifetimeManager(), new InjectionConstructor(new AspNetDbContext()));
// register asp.net identity user token provider
container.RegisterType<IUserTokenProvider<AspNetUser, long>, EmailTokenProvider<AspNetUser, long>>(new PerRequestLifetimeManager());
- Remove the existing Models/AccountModels.cs class
- Add a new default AccountViewModels.cs class (from ASP.NET MVC 5 default solution)
- Add a new default AccountController (from ASP.NET MVC 5 default solution)
- Inject the UserManager via the AccountController
public class AccountController : Controller
{
private AspNetUserManager UserManager;
public AccountController()
{
}
public AccountController(AspNetUserManager userManager, IUserTokenProvider<AspNetUser, long> userTokenProvider)
{
UserManager = userManager;
UserManager.UserTokenProvider = userTokenProvider;
}
- Remove ALL existing views under /Views/Account
- Add all new views (from ASP.NET MVC 5 default solution)
Tips
Comments
Post a Comment