Today, we will talk about how to migrate Old ASP.net authentication to Asp.net core identity with open id connect.
High-level concept
When you strip everything back to its most fundamental components, you will see that any authentication system simply depends on a database with a primary table called “User” or “Accounts” that contains comparable data such a username, email, password, biography, etc. In light of this, our goal is to enable seamless user authentication for every user of the old web app on the new web app. This means that we should abstract the login process such that OpenID connect and ASP.net Identity may take the place of the old authentication system without affecting how end users interact with the system.
The database model needs to be changed as our initial action after taking into account everything above.
Updating Database Schema
In order for the old authentication system to work with ASP.net core Identity, the old database structure must be modified. Two strategies exist for doing this. Open id connect isn’t actually discussed in this article because it can be utilized with ASP.net core Identity, which is the main emphasis of this lesson.
NOTE: Before making any adjustments to the production database, test this in a fake database first.
- Either you carry out the action with SQL commands (you manually add columns to the users table that already exists and make additional tables for roles, claims, etc.),
- You migrate with code first.
We will use the second approach since it is faster. For this, we will use Entity framework’s migration feature.
- If you wish to incorporate open Id connect into your project, I advise using the server templates offered by Duende by watching this video. https://www.youtube.com/watch?v=eLRGlnGGUQs
- If you want a template that has been initially configured with ASP.net Identity, select the “isaspid” template. Read this documentation for more information about open Id connect with identity server: https://docs.duendesoftware.com/identityserver/v6
- Create an application user using the properties required by the previous authentication method in the new ASP.NET core project. The identity user should be the class this one inherits from.
Note that Asp.net core Identity utilizes strings by default, in contrast to our situation where the old User database used the “integer” type as Id. So, we do the following to build our custom entities.
NOTE: Only perform the actions listed below if the Ids in your prior user table were integers or other numeric types.
public class ApplicationIdentityRole : IdentityRole<int> { public ApplicationIdentityRole() { } public ApplicationIdentityRole(string name) { Name = name; } }
Then, we create our identity user.
public class ApplicationUser: IdentityUser<int> { /// <summary> /// TODO: Add the properties of your former User (in the old ASP.net web app) /// </summary> }
Then we create the db context to access the database.
public class ApplicationDbContext: IdentityDbContext<ApplicationUser, ApplicationIdentityRole, int>
{}
- Additionally, you can have saved properties with names different from those used in Identity user in your old users database. For instance, the field UserName of my users was saved in the column “UName” in my database, although the column name in ASP.net core Identity is “UserName”. Entity framework needs to be informed about this distinction. Open your previously established DBcontext, and then add the upcoming lines.
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<ApplicationUser>(b => { b.ToTable("users");//If the name of your old table is “users” b.Property(e => e.UserName).HasColumnName("UName"); //TODO: Tell entity framework all the naming differences that might exist between your database and the Identity models. }); }
dotnet ef migrations add IdpMigration
migrationBuilder.AddColumn<string>( name: "NormalizedEmail", type: "character varying(256)", maxLength: 256, table: "account", nullable: true); migrationBuilder.AddColumn<string>( name: "NormalizedUserName", type: "character varying(256)", maxLength: 256, table: "account", nullable: true); migrationBuilder.AddColumn<string>( name: "SecurityStamp", type: "text", table: "account", nullable: true); migrationBuilder.AddColumn<string>( name: "ConcurrencyStamp", table: "account", type: "text", nullable: true); migrationBuilder.AddColumn<bool>( name: "EmailConfirmed", table: "account", defaultValue: false, type: "boolean", nullable: false); migrationBuilder.AddColumn<bool>( name: "PhoneNumberConfirmed", table: "account", defaultValue: false, type: "boolean", nullable: false); migrationBuilder.AddColumn<bool>( name: "AccessFailedCount", table: "account", type: "integer", defaultValue: false, nullable: false); migrationBuilder.AddColumn<DateTimeOffset>( name: "LockoutEnd", type: "timestamp with time zone", table: "account", nullable: true); migrationBuilder.AddColumn<bool>( name: "LockoutEnabled", table: "account", defaultValue: false, type: "boolean", nullable: false); migrationBuilder.AddColumn<bool>( name: "TwoFactorEnabled", table: "account", defaultValue: false, type: "boolean", nullable: false);
This will add new columns to your users table and make new tables for user roles, claims, etc. in your database.
Your database ought to be prepared after you’re finished. Use the UserManager to create a new user to test this out. Run this demo code to create a new user to accomplish this at server startup.
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>(); var alice = userMgr.FindByNameAsync("alice").Result; if (alice == null) { alice = new ApplicationUser { UserName = "alice", Email = "[email protected]", EmailConfirmed = true, }; var result = userMgr.CreateAsync(alice, "Pass123$").Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } result = userMgr.AddClaimsAsync(alice, new Claim[]{ new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), }).Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } Log.Debug("alice created"); }
If it doesn’t work, carefully review the error messages and fix them. These will most likely be mistakes resulting from restrictions on the database that are already in place but which your models have ignored.
If you used the open id connect template I described before, launch the project once the user has been successfully created, and then try authenticating with the user credentials you just created. Due to restrictions, you may still see problems in this step, although they will be small.
Converting Your Legacy Users to ASP.net Identity
We can authenticate freshly formed users thanks to a changed database schema, but what about long-time users of the previous web app? Even if they are present in the database, they will not be able to authenticate using ASP.net Identity. This is due to the fact that no matter how many times you try, the UserManager’s findbyemail or username function will never return these old users. Use these steps to make your previous users compatible.
- Run through every row of the users table in your database (Only rows for old users that where not ceated with ASP.net identity’s usermanager).
- For each user, apply the following computations. And add the appropriate claims to your users in the database. You might do this with a background job on your servers, or something similar.
foreach (var user in users) { var result = await _userManager.UpdateSecurityStampAsync(user); if (result != null && !result.Succeeded) { throw new Exception($"Failed to make user: {user.Id} Compatible. Error: {result.Errors.First().Description}"); } await _userManager.UpdateNormalizedEmailAsync(user); await _userManager.UpdateNormalizedUserNameAsync(user); result = await _userManager.SetLockoutEnabledAsync(user, true);//Either true or false, depending on your usecase if (result != null && !result.Succeeded) { throw new Exception($"Failed to make user: {user.Id} Compatible. Error: {result.Errors.First().Description}"); } //Add every required claim too for each user. result = _userManager.AddClaimsAsync(user, new Claim[]{ new Claim(JwtClaimTypes.Name, user.name ).Result; if (!result.Succeeded) { throw new Exception(result.Errors.First().Description); } }
After you run it, the database will have the fields that are necessary for users to authenticate using their ASP.net identities. Once that happens, you can load users with ASP.net identities.
Password hash and validation
You must let ASP.net identity know if your old authentication system utilized a different technique for hashing and comparing passwords. If you don’t, the system won’t recognize the passwords of your previous users.
- Find your old password hashing method, then construct a new “PasswordHasher” that combines it with the most recent Microsoft password validation algorithm, including all security upgrades and patches. The password hasher is shown here:
public class YSPasswordHasher : IPasswordHasher<ApplicationUser> { //an instance of the default password hasher used by Asp.net core identity IPasswordHasher<ApplicationUser> _defaultPasswordHasher; public YSPasswordHasher(IOptions<PasswordHasherOptions>? optionsAccessor = null) { _defaultPasswordHasher = new PasswordHasher<ApplicationUser>(optionsAccessor); } internal string HashPasswordWithOldAlgorithm(string password) { var sha1 = SHA1.Create(); // TODO: Add your old password hashing algorithm here return s.ToString(); } public string HashPassword(ApplicationUser user, string password) { return _defaultPasswordHasher.HashPassword(user, password); } public PasswordVerificationResult VerifyHashedPassword(ApplicationUser user, string hashedPassword, string providedPassword) { string pwdHash2 = HashPasswordWithOldAlgorithm(providedPassword); //Depending on the password format, one of the hashes should work. Either the old or the new password. if (hashedPassword == pwdHash2) { return PasswordVerificationResult.Success; } else { return _defaultPasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword); } }
- Then instruct Asp.net core to use your updated password hasher, which is suitable for both new and experienced users. To accomplish this, register the service with DI as described below:
builder.Services.AddSingleton<IPasswordHasher<ApplicationUser>, YSPasswordHasher>();
Conclusion
With the information above, switching to ASP.net identity should be simple. The method was simpler than anticipated the first time I conducted it, demonstrating how adaptable ASP.net core is.
Get your ASP.NET hosting as low as $1.00/month with ASPHostPortal. Our fully featured hosting already includes
- Easy setup
- 24/7/365 technical support
- Top level speed and security
- Super cache server performance to increase your website speed
- Top 9 data centers across the world that you can choose.
Yury Sobolev is Full Stack Software Developer by passion and profession working on Microsoft ASP.NET Core. Also he has hands-on experience on working with Angular, Backbone, React, ASP.NET Core Web API, Restful Web Services, WCF, SQL Server.