Image by Josh Cowper | Some Rights Reserved
The ASP.NET Identity framework was released to manufacture on March 20 2014, bringing with it a slew of long-awaited enhancements, delivering a fully-formed authentication and authorization platform to the ASP.NET developer community.
In previous posts, we have taken a broad look at the structure of the new framework, and how it differs from the 1.0 release. We’ve also walked through implementing email account confirmation and two-factor authentication, as well as extending the basic User and Role models (which requires a bit more effort than you might think).
- ASP.NET MVC and Identity 2.0: Understanding the Basics
- ASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authorization
- ASP.NET Identity 2.0: Customizing Users and Roles
In this post we’re going take a deeper look at extending the core set of models afforded by the Identity 2.0 framework, and re-implementing the basic Identity Samples project using integer keys for all of our models, instead of the default string keys which are the default.
Source Code on Github
In the course of this article, we will basically re-implement the Identity Samples project with integer keys. you can clone the completed source code from my Github repo. Also, if you find bugs and/or have suggestions, please do open an issue and/or shoot me a pull request!
- Why Does ASP.NET Identity Use String Keys in the First Place?
- Identity 2.0 Core Classes use Generic Type Arguments
- Implementing Integer Keys Using Identity 2.0 and the Identity Samples Project
- Re-Engineering the Basic Identity Models
- Cookie Authentication Configuration
- Update Admin View Models
- Update Controller Method Parameter Arguments
- Add Integer Type Argument to GetUserId() Calls
- Update Roles Admin Views
- A Note on Security
Why Does ASP.NET Identity Use String Keys in the First Place?
A popular, and somewhat confounding question is “why did the Identity team choose string keys as the default for the Identity framework models? Many of us who grew up using databases tend towards easy, auto-incrementing integers as database primary keys, because it’s easy, and at least in theory, there are some performance advantages with respect to table indexes and such.
The decision of the Identity Team to use strings as keys is best summarized in a Stack Overflow answer by Rick Anderson, writer for ASP.NET at Microsoft:
- The Identity runtime prefers strings for the user ID because we don’t want to be in the business of figuring out proper serialization of the user IDs (we use strings for claims as well for the same reason), e.g. all (or most) of the Identity interfaces refer to user ID as a string.
- People that customize the persistence layer, e.g. the entity types, can choose whatever type they want for keys, but then they own providing us with a string representation of the keys.
- By default we use the string representation of GUIDs for each new user, but that is just because it provides a very easy way for us to automatically generate unique IDs.
The decision is not without its detractors in the community. The default string key described above is essentially a string representation of a Guid. As this discussion on Reddit illustrates, there is contention about the performance aspects of this against a relational database backend.
The concerns noted in the Reddit discussion focus mainly on database index performance, and are unlikely to be an issue for a large number of smaller sites and web applications, and particularly for learning projects and students. However, as noted previously, for many of us, the auto-incrementing integer is the database primary key of choice (even in cases where it is not the BEST choice), and we want our web application to follow suit.
Identity 2.0 Core Classes use Generic Type Arguments
As we discussed in the post on customizing ASP.NET Identity 2.0 Users and Roles, the framework is built up from a structure of generic Interfaces and base classes. At the lowest level, we find interfaces, such as IUser<TKey>
and IRole<TKey>
. These, and related Interfaces and base classes are defined in the Microsoft.AspNet.Identity.Core library
.
Moving up a level of abstraction, we can look at the Microsoft.AspNet.Identity.EntityFramework
library, which uses the components defined in …Identity.Core
to build the useful, ready-to-use classes commonly used in applications, and in particular by the Identity Samples project we have been using to explore Identity 2.0.
The “…Identity.EntityFramework library gives us some Generic base classes, as well as a default concrete implementation for each. For example, Identity.EntityFramework gives us the following generic base implementation for a class IdentityRole…”:
Generic Base for IdentityRole:
public class IdentityRole<TKey, TUserRole> : IRole<TKey> where TUserRole : IdentityUserRole<TKey> { public TKey Id { get; set; } public string Name { get; set; } public ICollection<TUserRole> Users { get; set; } public IdentityRole() { this.Users = new List<TUserRole>(); } }
As we can see, the above defines IdentityRole
in terms of generic type arguments for the key and UserRole
, and must implement the interface IRole<TKey>
. Note that Identity defines both an IdentityRole
class, as well as an IdentityUserRole
class, both of which are required to make things work. More on this later.
The Identity team also provides what amounts to a default implementation of this class:
Default Implementation of IdentityRole with non-generic type arguments:
public class IdentityRole : IdentityRole<string, IdentityUserRole> { public IdentityRole() { base.Id = Guid.NewGuid().ToString(); } public IdentityRole(string roleName) : this() { base.Name = roleName; } }
Notice how the default implementation class is defined in terms of a string
key and a specific implementation of IdentityUserRole
?
This means that we can only pass strings as keys, and in fact the IdentityRole
model will be defined in our database with a string-type primary key. It also means that the specific, non-generic implementation of IdentityUserRole
will be what is passed to the type argument into the base class.
If we steal a page from the previous post, and take a look at the default type definitions provided by Identity 2.0, we find the following (it’s not exhaustive, but these are what we will be dealing with later):
Default Identity 2.0 Class Signatures with Default Type Arguments:
public class IdentityUserRole : IdentityUserRole<string> {} public class IdentityRole : IdentityRole<string, IdentityUserRole> {} public class IdentityUserClaim : IdentityUserClaim<string> {} public class IdentityUserLogin : IdentityUserLogin<string> {} public class IdentityUser : IdentityUser<string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUser, IUser<string> {} public class IdentityDbContext : IdentityDbContext<IdentityUser, IdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim> {} public class UserStore<TUser> : UserStore<TUser, IdentityRole, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>, IUserStore<TUser>, IUserStore<TUser, string>, IDisposable where TUser : IdentityUser {} public class RoleStore<TRole> : RoleStore<TRole, string, IdentityUserRole>, IQueryableRoleStore<TRole>, IQueryableRoleStore<TRole, string>, IRoleStore<TRole, string>, IDisposable where TRole : IdentityRole, new() {}
We can see that, starting with IdentityUserRole
, the types are defined with string keys, and as importantly, progressively defined in terms of the others. This means that if we want to use integer keys instead of string keys for all of our models (and corresponding database tables), we need to basically implement our own version of the stack above.
Implementing Integer Keys Using Identity 2.0 and the Identity Samples Project
As in previous posts, we are going to use the Identity Samples project as our base for creating an Identity 2.0 MVC application. The Identity team has put together the Identity Samples project primarily (I assume) as a demonstration platform, but in fact it contains everything one might need (after a few tweaks, anyway) in order to build out a complete ASP.NET MVC project using the Identity 2.0 framework.
The concepts we are going to look at here apply equally well if you are building up your own Identity-based application from scratch. The ways and means might vary according to your needs, but in general, much of what we see here will apply whether you are starting from the Identity Samples project as a base, or “rolling your own” so to speak.
The important thing to bear in mind is that the generic base types and interfaces provided by Identity framework allow great flexibility, but also introduced complexity related to the dependencies introduced by the generic type arguments. In particular, the type specified as the key for each model must propagate through the stack, or the compiler gets angry.
Getting Started – Installing the Identity Samples Project
The Identity Samples project is available on Nuget. First, create an empty ASP.NET Web Project (It is important that you use the “Empty” template here, not MVC, not Webforms, EMPTY). Then open the Package Manager console and type:
Install Identity Samples from the Package Manager Console:
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
This may take a minute or two to run. When complete, your will see a basic ASP.NET MVC project in the VS Solution Explorer. Take a good look around the Identity 2.0 Samples project, and become familiar with what things are and where they are at.
Re-Engineering the Basic Identity Models
To get started, we need to re-engineer the basic model classes defined in the Identity Samples project, as well as add a few new ones. Because Identity Samples uses string-based keys for entity models, the authors, in many cases get away with depending upon the default class implementations provided by the framework itself. Where they extend, they extend from the default classes, meaning the string-based keys are still baked in to the derived classes.
Since we want to use integer keys for all of our models, we get to provide our own implementations for most of the models.
In many cases, this isn’t as bad as it sounds. For example, there are a handful of model classes we need only define in terms of the generic arguments, and from there the base class implementation does the rest of the work.
NOTE: As we proceed to modify/add new classes here, the error list in Visual Studio will begin to light up like a Christmas tree until we are done. Leave that be for the moment. If we do this correctly, there should be no errors left when we finish. IF there are, they will help us find things we missed.
In the Models => IdentityModels.cs file, we find the model classes used by the Identity Samples application. To get started, we are going to add our own definitions for IndentityUserLogin
, IdentityUserClaim
, and IdentityUserRole
. The Identity Samples project simply depended upon the default framework implementations for these classes, and we need our own integer based versions. Add the following to the IdentityModels.cs file:
Integer-Based Definitions for UserLogin, UserClaim, and UserRole:
public class ApplicationUserLogin : IdentityUserLogin<int> { } public class ApplicationUserClaim : IdentityUserClaim<int> { } public class ApplicationUserRole : IdentityUserRole<int> { }
Now, with that out of the way, we can define our own implementation of IdentityRole
. The Samples project also depended upon the framework version for IdentityRole
, and we are going to provide our own again. This time, though, there’s a little more to it:
Integer-Based Definition for IdentityRole:
public class ApplicationRole : IdentityRole<int, ApplicationUserRole>, IRole<int> { public string Description { get; set; } public ApplicationRole() { } public ApplicationRole(string name) : this() { this.Name = name; } public ApplicationRole(string name, string description) : this(name) { this.Description = description; } }
Notice above, we have defined ApplicationRole
in terms of an integer key, and also in terms of our custom class ApplicationUserRole
? This is important, and will continue on up the stack as we re-implement the Identity classes we need for the Identity Samples project to run as expected.
Next, we are going to modify the existing definition for ApplicationUser
. Currently, the IdentitySamples.cs file includes a fairly simple definition for ApplicationUser
which derives from the default IdentityUser
class provided by the framework, which requires no type arguments because they have already been provided in the default implementation. We need to basically re-define ApplicationUser
starting from the ground up.
The existing ApplicationUser
class in the IdentityModels.cs file looks like this:
Existing ApplicationUser Class in IdentityModels.cs:
public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager) { var userIdentity = await manager .CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); return userIdentity; } }
We need to replace the above in its entirety with the following:
Custom Implementation for ApplicationUser:
public class ApplicationUser : IdentityUser<int, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>, IUser<int> { public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager) { var userIdentity = await manager .CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); return userIdentity; } }
Once again, instead of deriving from the default Identity framework implementation for IdentityUser
, we have instead used the generic base, and provided our own custom type arguments. Also again, we have defined our custom ApplicationUser
in terms of an integer key, and our own custom types.
Modified Application Db Context
Also in the IdentityModels.cs file is an ApplicationDbContext
class.
Now that we have built out the basic models we are going to need, we also need to re-define the ApplicationDbContext
in terms of these new models. As previously, the existing ApplicationDbContext
used in the Identity Samples application is expressed only in terms of ApplicationUser
, relying (again) upon the default concrete implementation provided by the framework.
If we look under the covers, we find the ApplicationDbContext<ApplicationUser>
actually inherits from IdentityDbContext<ApplicationUser>,
which in turn is derived from:
IdentityDbContext<TUser, IdentityRole, string, IdentityUserLogin,
IdentityUserRole, IdentityUserClaim>
where TUser : Microsoft.AspNet.Identity.EntityFramework.IdentityUser
In other words, we once again have a default concrete implementation which is defined in terms of the other default framework types, all of which further depend upon a string-based key.
In order to define a DbContext
which will work with our new custom types, we need to express our concrete class in terms of integer keys, and our own custom derived types.
Replace the existing ApplicationDbContext
code with the following:
Modified ApplicationDbContext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim> { public ApplicationDbContext() : base("DefaultConnection") { } static ApplicationDbContext() { Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer()); } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
Once again, we have now expressed ApplicationDbContext
in terms of our own custom types, all of which use an integer key instead of a string.
Custom User and Role Stores
I am willing to bet that if you take a look at the Visual Studio Error Window right now, it is likely a block of what seems to be endless red error indicators. As mentioned previously, that’s fine for now – ignore it.
Identity framework defines the notion of User and Role stores for accessing user and role information. As with most everything else to this point, the default framework implementations for UserStore and RoleStore are defined in terms of the other default classes we have seen to this point – in other words, they won’t work with our new custom classes. We need to express a custom User store, and a custom Role store, in terms of integer keys and our own custom classes.
Add the following to the IdentityModels.cs file:
Adding a Custom User Store:
public class ApplicationUserStore : UserStore<ApplicationUser, ApplicationRole, int, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>, IUserStore<ApplicationUser, int>, IDisposable { public ApplicationUserStore() : this(new IdentityDbContext()) { base.DisposeContext = true; } public ApplicationUserStore(DbContext context) : base(context) { } } public class ApplicationRoleStore : RoleStore<ApplicationRole, int, ApplicationUserRole>, IQueryableRoleStore<ApplicationRole, int>, IRoleStore<ApplicationRole, int>, IDisposable { public ApplicationRoleStore() : base(new IdentityDbContext()) { base.DisposeContext = true; } public ApplicationRoleStore(DbContext context) : base(context) { } }
Re-Engineering Identity Configuration Classes
The Identity Samples project includes a file named App_Start => IdentityConfig.cs. In this file is a bunch of code which basically configures the Identity System for use in your application. The changes we introduced on our IdentityModels.cs file will cause issues here (and basically, throughout the application) until they are addressed in the client code.
In most cases, we will either be replacing a reference to a default Identity class with one of our new custom classes, and/or calling method overrides which allow the passing of custom type arguments.
In the IdentityConfig.cs file, we find an ApplicationUserManager
class, which contains code commonly called by our application to, well, manage users and behaviors. we will replace the existing code with the following, which essentially expresses ApplicationUserManager
in terms of integer keys, and our new custom UserStore
. If you look closely, we have added an int type argument to many of the method calls.
Customized ApplicationUserManager Class:
// *** PASS IN TYPE ARGUMENT TO BASE CLASS: public class ApplicationUserManager : UserManager<ApplicationUser, int> { // *** ADD INT TYPE ARGUMENT TO CONSTRUCTOR CALL: public ApplicationUserManager(IUserStore<ApplicationUser, int> store) : base(store) { } public static ApplicationUserManager Create( IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { // *** PASS CUSTOM APPLICATION USER STORE AS CONSTRUCTOR ARGUMENT: var manager = new ApplicationUserManager( new ApplicationUserStore(context.Get<ApplicationDbContext>())); // Configure validation logic for usernames // *** ADD INT TYPE ARGUMENT TO METHOD CALL: manager.UserValidator = new UserValidator<ApplicationUser, int>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // 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. // *** ADD INT TYPE ARGUMENT TO METHOD CALL: manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser, int> { MessageFormat = "Your security code is: {0}" }); // *** ADD INT TYPE ARGUMENT TO METHOD CALL: manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser, int> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { // *** ADD INT TYPE ARGUMENT TO METHOD CALL: manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser, int>( dataProtectionProvider.Create("ASP.NET Identity")); } return manager; } }
That’s a lot of code there. Fortunately, modifying the ApplicationRoleManager
class is not such a big deal. We’re essentially doing the same thing – expressing ApplicationRoleManager
in terms of integer type arguments, and our custom classes.
Replace the ApplicationRoleManager
code with the following:
Customized ApplicationRoleManager Class:
// PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO BASE: public class ApplicationRoleManager : RoleManager<ApplicationRole, int> { // PASS CUSTOM APPLICATION ROLE AND INT AS TYPE ARGUMENTS TO CONSTRUCTOR: public ApplicationRoleManager(IRoleStore<ApplicationRole, int> roleStore) : base(roleStore) { } // PASS CUSTOM APPLICATION ROLE AS TYPE ARGUMENT: public static ApplicationRoleManager Create( IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context) { return new ApplicationRoleManager( new ApplicationRoleStore(context.Get<ApplicationDbContext>())); } }
Modify The Application Database Initializer and Sign-in Manager
The ApplicationDbInitializer
class is what manages the creation and seeding of the backing database for our application. In this class we create a basic admin role user, and set up additional items such as the Email and SMS messaging providers.
The only thing we need to change here is where we initialize an instance of ApplicationRole
. In the existing code, the ApplicationDbInitializer
class instantiates an instance of IdentityRole
, and we need to create an instance of our own ApplicationRole
instead.
Replace the existing code with the following, or make the change highlighted below:
Modify the ApplicationDbInitializer Class:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role public static void InitializeIdentityForEF(ApplicationDbContext db) { var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>(); var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>(); const string name = "admin@example.com"; const string password = "Admin@123456"; const string roleName = "Admin"; //Create Role Admin if it does not exist var role = roleManager.FindByName(roleName); if (role == null) { // *** INITIALIZE WITH CUSTOM APPLICATION ROLE CLASS: role = new ApplicationRole(roleName); var roleresult = roleManager.Create(role); } var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = name }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } // Add user admin to Role Admin if not already added var rolesForUser = userManager.GetRoles(user.Id); if (!rolesForUser.Contains(role.Name)) { var result = userManager.AddToRole(user.Id, role.Name); } } }
Fixing up the ApplicationSignInManager
is even more simple. Just change the string
type argument in the class declaration to int
:
Modify the ApplicationSignInManager Class:
// PASS INT AS TYPE ARGUMENT TO BASE INSTEAD OF STRING: public class ApplicationSignInManager : SignInManager<ApplicationUser, int> { public ApplicationSignInManager( ApplicationUserManager userManager, IAuthenticationManager authenticationManager) : base(userManager, authenticationManager) { } public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user) { return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager); } public static ApplicationSignInManager Create( IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context) { return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication); } }
Cookie Authentication Configuration
In the file App_Start => Startup.Auth there is a partial class definition, Startup. in the single method call defined in the partial class, there is a call to app.UseCookieAuthentication()
. Now that our application is using integers as keys instead of strings, we need to make a modification to the way the CookieAuthenticationProvider
is instantiated.
The existing call to app.UseCookieAuthentication
(found smack in the middle of the middle of the ConfigureAuth()
method) needs to be modified. Where the code calls OnVlidateIdentity
the existing code passes ApplicationUserManager
and ApplicationUser
as type arguments. What is not obvious is that this is an override which assumes a third, string type argument for the key (yep – we’re back to that whole string keys thing again).
We need to change this code to call another override, which accepts a third type argument, and pass it an int
argument.
The existing code looks like this:
Existing Call to app.UseCookieAuthentication:
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user logs in. // This is a security feature which is used when you change a // password or add an external login to your account. OnValidateIdentity = SecurityStampValidator .OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } });
We need to modify this code in a couple of non-obvious ways. First, as mentioned above, we need to add a third type argument specifying that TKey
is an int.
Less obvious is that we also need to change the name of the second argument from regenerateIdentity
to regenerateIdentityCallback
. Same argument, but different name in the overload we are using.
Also less than obvious is the third Func
we need to pass into the call as getUserIdCallback
. Here, we need to retreive a user id from a claim, which stored the Id as a string. We need to parse the result back into an int
.
Replace the existing code above with the following:
Modified Call to app.UseCookieAuthentication:
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user logs in. // This is a security feature which is used when you change a // password or add an external login to your account. OnValidateIdentity = SecurityStampValidator // ADD AN INT AS A THIRD TYPE ARGUMENT: .OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>( validateInterval: TimeSpan.FromMinutes(30), // THE NAMED ARGUMENT IS DIFFERENT: regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager), // Need to add THIS line because we added the third type argument (int) above: getUserIdCallback: (claim) => int.Parse(claim.GetUserId())) } });
With that, most of the Identity infrastructure is in place. Now we need to update a few things within our application.
Update Admin View Models
The Models => AdminViewModels.cs file contains class definitions for a RolesAdminViewModel
and a UsersAdminViewModel
. In both cases, we need to change the type of the Id property from string to int:
Modify the Admin View Models:
public class RoleViewModel { // Change the Id type from string to int: public int Id { get; set; } [Required(AllowEmptyStrings = false)] [Display(Name = "RoleName")] public string Name { get; set; } } public class EditUserViewModel { // Change the Id Type from string to int: public int Id { get; set; } [Required(AllowEmptyStrings = false)] [Display(Name = "Email")] [EmailAddress] public string Email { get; set; } public IEnumerable<SelectListItem> RolesList { get; set; } }
Update Controller Method Parameter Arguments
A good many of the controller action methods currently expect an id argument of type string. We need to go through all of the methods in our controllers and change the type of the id argument from string to int.
In each of the following controllers, we need to change the existing Id from string to int as shown for the action methods indicated (we’re only showing the modified method signatures here):
Account Controller:
public async Task<ActionResult> ConfirmEmail(int userId, string code)
Roles Admin Controller:
public async Task<ActionResult> Edit(int id) public async Task<ActionResult> Details(int id) public async Task<ActionResult> Delete(int id) public async Task<ActionResult> DeleteConfirmed(int id, string deleteUser)
Users Admin Controller:
public async Task<ActionResult> Details(int id) public async Task<ActionResult> Edit(int id) public async Task<ActionResult> Delete(int id) public async Task<ActionResult> DeleteConfirmed(int id)
Update the Create Method on Roles Admin Controller
Anywhere we are creating a new instance of a Role, we need to make sure we are using our new ApplicationRole
instead of the default IdentityRole
. Specifically, in the Create()
method of the RolesAdminController
:
Instantiate a new ApplicationRole Instead of IdentityRole:
[HttpPost] public async Task<ActionResult> Create(RoleViewModel roleViewModel) { if (ModelState.IsValid) { // Use ApplicationRole, not IdentityRole: var role = new ApplicationRole(roleViewModel.Name); var roleresult = await RoleManager.CreateAsync(role); if (!roleresult.Succeeded) { ModelState.AddModelError("", roleresult.Errors.First()); return View(); } return RedirectToAction("Index"); } return View(); }
Add Integer Type Argument to GetUserId() Calls
If we take a look at our Error list now, we see the preponderance of errors are related to calls to User.Identity.GetUserId()
. If we take a closer look at this method, we find that once again, the default version of GetUserId()
returns a string, and that there is an overload which accepts a type argument which determines the return type.
Sadly, calls to GetUserId()
are sprinkled liberally throughout ManageController
, and a few places in AccountController
as well. We need to change all of the calls to reflect the proper type argument, and the most efficient way to do this is an old fashioned Find/Replace.
Fortunately, you can use Find/Replace for the entire document on both ManageController
and AccountController
, and get the whole thing done in one fell swoop. Hit Ctrl + H, and in the “Find” box, enter the following:
Find all instances of:
Identity.GetUserId()
Replace with:
Identity.GetUserId<int>()
If we’ve done this properly, most of the glaring red errors in our error list should now be gone. There are a few stragglers, though. In these cases, we need to counter-intuitively convert the int Id back into a string.
Return a String Where Required
There are a handful of methods which call to GetUserId()
, but regardless of the type the Id represents (in our case, now, an int
) want a string representation of the Id passed as the argument. All of these methods are found on ManageController
, and in each case, we just add a call to .ToString()
.
First, in the Index()
method of ManageController
, we find a call to AuthenticationManager.TwoFactorBrowserRemembered()
. Add the call to .ToString()
after the call to GetUserId()
:
Add Call to ToString() to TwoFactorBrowserRemembered:
public async Task<ActionResult> Index(ManageMessageId? message) { ViewBag.StatusMessage = message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." : message == ManageMessageId.SetTwoFactorSuccess ? "Your two factor provider has been set." : message == ManageMessageId.Error ? "An error has occurred." : message == ManageMessageId.AddPhoneSuccess ? "The phone number was added." : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." : ""; var model = new IndexViewModel { HasPassword = HasPassword(), PhoneNumber = await UserManager.GetPhoneNumberAsync(User.Identity.GetUserId<int>()), TwoFactor = await UserManager.GetTwoFactorEnabledAsync(User.Identity.GetUserId<int>()), Logins = await UserManager.GetLoginsAsync(User.Identity.GetUserId<int>()), // *** Add .ToString() to call to GetUserId(): BrowserRemembered = await AuthenticationManager .TwoFactorBrowserRememberedAsync(User.Identity.GetUserId<int>().ToString()) }; return View(model); }
Similarly, do the same for the RememberBrowser
method, also on ManageController
:
Add Call to ToString() to RememberBrowser Method:
[HttpPost] public ActionResult RememberBrowser() { var rememberBrowserIdentity = AuthenticationManager .CreateTwoFactorRememberBrowserIdentity( // *** Add .ToString() to call to GetUserId(): User.Identity.GetUserId<int>().ToString()); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true }, rememberBrowserIdentity); return RedirectToAction("Index", "Manage"); }
Lastly,the same for the LinkLogin()
and LinkLoginCallback()
methods:
Add Call to ToString() to LinkLogin():
[HttpPost] [ValidateAntiForgeryToken] public ActionResult LinkLogin(string provider) { return new AccountController .ChallengeResult(provider, Url.Action("LinkLoginCallback", "Manage"), // *** Add .ToString() to call to GetUserId(): User.Identity.GetUserId<int>().ToString()); }
Add Call to ToString() to LinkLoginCallback():
public async Task<ActionResult> LinkLoginCallback() { var loginInfo = await AuthenticationManager .GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId<int>().ToString()); if (loginInfo == null) { return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error }); } var result = await UserManager // *** Add .ToString() to call to GetUserId(): .AddLoginAsync(User.Identity.GetUserId<int>().ToString(), loginInfo.Login); return result.Succeeded ? RedirectToAction("ManageLogins") : RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error }); }
With that, we have addressed most of the egregious issues, and we basically taken a project built against a model set using all string keys and converted it to using integers. The integer types will be propagated as auto-incrementing integer primary keys in the database backend as well.
But there are still a few things to clean up.
Fix Null Checks Against Integer Types
Scattered throughout the primary identity controllers are a bunch of null checks against the Id values received as arguments in the method calls. If you rebuild the project, the error list window in Visual Studio should now contain a bunch of the yellow “warning” items about this very thing.
You can handle this in your preferred manner, but for me, I prefer to check for a positive integer value. We’ll look at the Details()
method from the UserAdminController
as an example, and you can take it from there.
The existing code in the Details()
method looks like this:
Existing Details() Method from UserAdminController:
public async Task<ActionResult> Details(int id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var user = await UserManager.FindByIdAsync(id); ViewBag.RoleNames = await UserManager.GetRolesAsync(user.Id); return View(user); }
In the above, we can see that previously, the code checked for a null value for the (formerly) string-typed Id argument. Now that we are receiving an int
, the check for null is meaningless. Instead, we want to check for a positive integer value. If the check is true, then we want to process accordingly. Otherwise, we want to return the BadRequest
result.
In other words, we need to invert the method logic. Previously, if the conditional evaluated to true, we wanted to return the error code. Now, is the result is true, we want to proceed, and only return the error result if the conditional is false. So we’re going to swap our logic around.
Replace the code with the following:
Modified Details() Method with Inverted Conditional Logic:
public async Task<ActionResult> Details(int id) { if (id > 0) { // Process normally: var user = await UserManager.FindByIdAsync(id); ViewBag.RoleNames = await UserManager.GetRolesAsync(user.Id); return View(user); } // Return Error: return new HttpStatusCodeResult(HttpStatusCode.BadRequest); }
We can do something similar for the other cases in UserAdminController
, RolesAdminController
, and AccountController
. Think through the logic carefully, and all should be well.
Update Roles Admin Views
Several of the View Templates currently use the default IdentityRole model instead of our new, custom ApplicationRole
. We need to update the Views in Views => RolesAdmin to reflect our new custom model.
The Create.cshtml and Edit.cshtml Views both depend upon the RoleViewModel
, which is fine. However, the Index.cshtml, Details.cshtml, and Delete.cshtml Views all currently refer to IdentityRole
. Update all three as follows
The Index.cshtml View currently expects an IEnumerable<IdentityRole>
. We need to change this to expect an IEnumerable<ApplicationRole
> . Note that we need to include the project Models namespace as well:
Update the RolesAdmin Index.cshtml View:
@model IEnumerable<IdentitySample.Models.ApplicationRole> // ... All the view code ...
All we need to change here is the first line, so I omitted the rest of the View code.
Similarly, we need to update the Details.cshtml and Delete.cshtml Views to expect ApplicationRole
instead of IdentityRole
. Change the first line in each to match the following:
Update the Details.cshtml and Delete.cshtml Views:
@model IdentitySample.Models.ApplicationRole // ... All the view code ...
Obviously, if your default project namespace is something other than IdentitySamples
, change the above to suit.
Additional Extensions are Easy Now
Now that we have essentially re-implemented most of the Identity object models with our own derived types, it is easy to add custom properties to the ApplicationUser and/.or ApplicationRole models. All of our custom types already depend upon each other in terms of the interrelated generic type arguments, so we are free to simply add what properties we wish to add, and then update our Controllers, ViewModels, and Views accordingly.
To do so, review the previous post on extending Users and Roles, but realize all of the type structure stuff is already done. Review that post just to see what goes on with updating the Controllers, Views, and ViewModels.
A Note on Security
The basic Identity Samples application is a great starting point for building out your own Identity 2.0 application. However, realize that, as a demo, there are some things built in that should not be present in production code. For example, the database initialization currently includes hard-coded admin user credentials.
Also, the Email confirmation and two-factor authentication functionality currently circumvents the actual confirmation and two-factor process, by including links on each respective page which short-circuit the process.
The above items should be addressed before deploying an actual application based upon the Identity Samples project.
Wrapping Up
We’ve taken a rather exhaustive look at how to modify the Identity Samples application to use integer keys instead of strings. Along the way, we (hopefully) gained a deeper understanding of the underlying structure in an Identity 2.0 based application. There’s a lot more there to learn, but this is a good start.
Additional Resources and Items of Interest
- Source Code on Github
- .ASP.NET Identity Recommended Resources by Rick Anderson
- ASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authorization
- ASP.NET MVC and Identity 2.0: Understanding the Basics
- Routing Basics in ASP.NET MVC
- Customizing Routes in ASP.NET MVC
Comments
Luca Gabi
AuthorWill external login with google work ?
Incredibly much work to do ..
Deba
AuthorHi ,
Great post it really help me most of my doubt and working fine for me any way i have small question which i want to apply on my demo if i need to use sendgrid and twilo/sms provider do i had to set in ApplicationDbInitializer class.
thanks again for the post
Nick D
AuthorThanks! I think there’s a typo, you mention ‘IdentitySamples.cs’ I think you mean IdentityModels.cs?
alex gan
Authorgot a error when I add migration:
One or more validation errors were detected during model generation:
testIdentityEmpty.Models.IdentityUserRole: : EntityType ‘IdentityUserRole’ has no key defined. Define the key for this EntityType.
testIdentityEmpty.Models.IdentityUserLogin: : EntityType ‘IdentityUserLogin’ has no key defined. Define the key for this EntityType.
IdentityUserRoles: EntityType: EntitySet ‘IdentityUserRoles’ is based on type ‘IdentityUserRole’ that has no keys defined.
IdentityUserLogins: EntityType: EntitySet ‘IdentityUserLogins’ is based on type ‘IdentityUserLogin’ that has no keys defined.
alex gan
AuthorI know what happened,it works
Mohamed
AuthorAnother question, if I want to just add a Description property to the IdentityRole entity with preserving the string type for the id, will I have to do all these implementation?
Mohamed
AuthorThanks for this excellent tutorial, very useful and easy to follow.
I still find the MVC identity very complex, I hope they could simplify it in the future.
Could you please tell me how to reorder the description column of the ApplicationRole entity, as it comes before the Name column in the database?
Joebet Mutia
AuthorI dont really do some comments for a blog but damn.. your tutorial here is so precise with no bugs and 100% working.. thanks man!
John Atten
AuthorHey thanks for taking the time to comment! Cheers!
Mark Worrall
AuthorArrived here from your other article on this. Really appreciate the effort you have put in to try and shed some light on how ASP.NET Identity works, and was onboard most of the way, but I have to agree with the Reddit discussion and Bart Calixto comments, Microsoft have once again way over-engineered this. What is the point in making this so complicated and not even being scalable?
Anyone who develops serious commercial grade .Net applications knows what a nightmare it is using Guids for keys, and on so many levels, why would MS build it this way? Because it was easy for them, I suspect, not us.
Most of us build apps to use SQL Server, and the way this is architected means we have to juggle users and roles straddling 2 DBs (potentially on different servers), and all the relationship and performance issues that come with that, and do rocket science code to get it all to work (just look at all the issues in the comments). Not to mention related ongoing maintenance issues. This should just be a simple drop in component 80% of the time.
Why cant MS provide a dedicated .Net to SQL Server quick and simple way to generate the tables into any/an existing SQL Server db, for Users and Roles, etc, that we can populate/extend as required, using integer IDs they know we all use, and a simple .Net component to drop into the code to hook up authentication and authorsation?
I get the distinct feeling this has been designed by people who only play with these technologies and don’t realise DBs just can’t be dropped and recreated after they are live, have never worked on commercial sized dbs dealing with tables of 100,000+s rows, or responsible for taking it through to production and it’s performance.
I think I will probably try and only use it for simple registration and authentication, extend the User object to include an integer ID (basically to map their Guid to my db ID), and build authorisation the tried and tested way.
John Atten
AuthorAgreed to a point, on all points. I’m not sure if the out-of-the-box Identity faremwork is actually intended for giant, at-scale, commercial usage. I think the target here is smaller sites with simple auth requirements. Even at that,. it can be complex. Bear in mind, though, that to my way of thinking, EF (and particularly Code First generation) is also of limited utility at large scale. But yeah, the GUID thing…never understood that particular design choice.
Nice comment, thanks for taking the time!
Donald Fraser
AuthorUpdate to my previous comments.
I have forked and committed fixes to the code base and hopefully someone from MS will release a new version (2.2.2) with these fixes.
In the mean time you can overcome the short comings by implementing the following methods in your “ApplicationSignInManager” class.
/**
* MUST USE THIS VERSION TO FIX BUG in version 2.2.1
* (Pity MS didn’t make this method virtual, else we could fix extension method too)
**/
public new async Task HasBeenVerifiedAsync()
{
int userID = await GetVerifiedUserIdAsync();
if (userID == 0)
return false;
return true;
}
/**
* MUST OVERRIDE TO FIX BUG in version 2.2.1
**/
public override async Task SendTwoFactorCodeAsync(string provider)
{
if (await HasBeenVerifiedAsync())
{
return await base.SendTwoFactorCodeAsync(provider);
}
return false;
}
These methods will fix most use cases, however it should be noted that the bug extends to the following methods in the code base:
SecurityStampValidator.OnValidateIdentity
This is non critical because the fault is captured further on in the code with a reject identity result, which could be misleading, but at least it fails.
SignInManager.TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
Again non-critical because the bug is captured, in further steps of the code, with identical return results.
SignInManager.HasBeenVerified (via SignInManagerExtensions)
DO NOT USE THIS EXTENSION METHOD AS you will always get a true result. Had MS made the base class method virtual we could have fixed this.
Donald Fraser
AuthorHi, first off thanks for the great article. However I would like to advise, at least with version 2 of the ASP.Identity framework, that you cannot use type “int” as the identity type. Well you can but you will soon find that it doesn’t work as expected. Why, because MS expects the identity type to be nullable, and “int” is not nullable and nor can you use System.Nullable because this does not convert to System.IEquatable.
To prove that it does not work, try calling this method for the SignInManager class before you log in:
bool bVerified = await HasBeenVerifiedAsync();
You will note that this method always returns true.
I discovered this by inspecting the source code and found that it heavily relies on TKey being nullable. In other words the code is littered with tests such as: if (userId == null)…
Sorry to report this, but its fairly critical that people know that there is a major flaw in the MS code base!
John Atten
AuthorWow. Nice catch. I’ll have to investigate, and probably update this post then… thanks for taking the time to comment! Cheers!
fred
Authorfirst, i want to thank you for your efforts on extending the Identity framework. I’ll appreciate it if you can tell me how to make this work with integer keys, i followed your tutorial on using integer key with the Identity framework, I need extra guidance on extending the Identity framework using integer keys for newly added classes, Thank You.
John Atten
Author@Fred – You should be able to follow the pattern used in this tutorial, and apply a little ingenuity. Note, though, that things have changed a little since this was written, so you may have to adapt a little.
John Butler
AuthorCarlos and Mahmud, I’m sure you have solved the issue, but if anyone else should run into the problem you’re describing, it is because you ran the site where the id was a string, and then when you would launch it again, the information was stored in a browser/cookie. Run something like CCleaner or launch the site in a different browser and see if it will work after that.
Abdullah
AuthorThe problem about running the application is fixed but how can I run “ApplicationDbInitializer” Method because I tried but it does not initiate any data to my database. I am using Entity Framework code First
John Atten
AuthorWithout seeing your code, I can’t tell what the issue is. Do you have the DbInitializer set up to DropCreateAlways?
Abdullah
AuthorThank you so much for the help but I have trouble opining the website I have the error
============
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /Account/Login
===============================
John Atten
AuthorNot much I can tell here without seeing your code. What version of ASP.NET/MVC are you using? Are you able to get to any of the pages in the application at all?
Edgar Ricardez
AuthorHi John,
Thanks for sharing this post, is excellent.
I am a beginner and would like to find a similar article for ASP.NET Web API identity (Individual Accounts).
I created a solution in Visual Studio 2013 and follow your instructions, but I can not get the same result.
Do you have information for modify the ASP.NET Web API Identity?
Thanks a lot
John Atten
AuthorYup. Normal SQL rules apply (just hidden behind the ORM), and in SQL Server (and most other RDBMS's you cannot make a column an Identity column after data has been inserted.
Thanks for following up with the solution!
Cheers!
Joris van Gestel
AuthorAh typical, shortly after posting I find the solution. It appears the migration is indeed the problem, you can NOT alter a column to become identity, so if you are migrating you have to either drop the table, or the column and then recreate them (remember this has to be done for both the AspNetRoles table and the AspNetUsers table). Luckily we have not deployed any of this code to a production environment yet so I don't have to worry about the dropped data.
Joris van Gestel
AuthorI'm trying to apply this change on an existing database (based off of the original identity 2.0 example). I'm running into the problem that my seed method now cannot create the Admin role (the key Id cannot be NULL), this appears the be the result of the database not having the Id column set as identity and therefore not generating a value. Could this be the result of a step I missed in the guide, or the result of the migration simply not being correct (it wouldnt be the first time that a migration has to be adjusted manually). The migration generates the following line:
AlterColumn("dbo.AspNetRoles", "Id", c => c.Int(nullable: false, identity: true));
but when I checked the database IsIdentity was still false. Maybe its a bug in the framework itself?
John Atten
AuthorSee this discussion:
https://github.com/TypecastException/AspNet-Identity-2-With-Integer-Keys/issues/2
mahmud kakl
AuthorGetting an error in this line:
// Need to add THIS line because we added the third type argument (int) above:
getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
Error:
"Input string was not in a correct format"
Can you help me?
John Atten
Author@Toan –
Thinking on a whole blog makeover. Understand your point. May be a bit, but I want to get a little less dated theme happening at some point. Also move away from .asp pages to "friendly" URLs without breaking existing links/losing Google juice.
Toan Nguyen
AuthorHi,
Thanks for your posts and they are really helpful. However, the fore color of the text are really hard to read. Could you pleas upgrade the text to black instead of black-grayish. Which would make your posts easier to read.
Once again, Thank you
Christian Metz
AuthorThank you for this complete details.
I would like to go one more step as I still do not like the table names that are created by EF.
For example instead of using AspNetUsers I just want my tablename to be Users.
I thought this must be easy and added the OnModelCreating to your ApplicationDbContext and added the ToTable mappings for the IdentityUser class and my custom ApplicationUser class.
First I am faced with the mapping error that there is no Key defined and then there where no references to the related tables. After some hours I was completly lost and still get a lot of errors.
Do you have a tip how I could combine Integer Key And custom tablename together?
John Atten
Author@Carlos –
Have you tried setting a break point and stepping through the code? I suspect you are running into a situation where something is not setting the proper type for Id.
Did you try cloning the project from my github? It SHOULD work fine.
John Atten
Author@Bart Calixto –
Perhaps some of us like to look a little deeper, and really understand the frameworks we work with? Or maybe we don't want a weird mix of strings and ints as keys on our objects?
The answer from Hao Kung is one approach, but to do this for all the entity models would become no less a chore than the way I do it here. The minute you re-implement any of the other model classes you will still have to make sure you pass your re-implemented types down the stack.
Also, do you really want to have two UserId properties, one of string type, and one if int type on the same class? If you merely wanted ints for your user id, and didn;t care about the rest of the model classes, this might make sense to save a bunch of work.
Lastly, try implementing your own MyUser class in teh Identity Samples project. See what happens. Now try doing the same thing for both Roles and users.
Also, there were two points to the article – one was getting a consistent implementation of integer keys ALL THE WAY down. The other was to get familiar with the underlying structure of the Identity models and how they interact.
Bart Calixto
AuthorWhy would someone point to that SO answer and post a blog like this huge when the answer from Hao Kung is to only implement this :
public class MyUser : IUser {
public int Id { get; set; }
string IUser.Id { get { return Id.ToString(); } }
}
what am I missing ?
Bart Calixto
AuthorWhat about :
Uninstall-Package Identity.EntityFramework
Install-Package Identity.EntityFrameworkInt
is that viable ? can someone make a package like this ?
can't MICROSOFT create a package like this ?
Carlos Gaviria
AuthorGetting an error in this line:
// Need to add THIS line because we added the third type argument (int) above:
getUserIdCallback: (claim) => int.Parse(claim.GetUserId()))
Error:
"Input string was not in a correct format"
Can you help me?
Dariel Marlow
AuthorAppreciate the walkthrough. I too had to go through this not too long ago and found it to be more work than I thought it needed to be.
John Atten
Author@Japarradog –
I think you want to keep the email as is, but you can simply add your identification code as a new property on ApplicationUser.
If you really want to do away with the Email field, you probably can, but you will likely need to deal with all the points in the application where an email field is expected by either the Samples project template, and/or Identity Framework itself.
I would just add the property.
Japarradog
AuthorHow easy it is to replace the Email for other data, such as an identification code or something, if you can do?
John Atten
Author@Aliosat –
You are entirely correct. Silliness on my part, shuffling things around while writing the article.
Dustin
AuthorHowdy! I just wanted to let you know that I really appreciate your work on this series of articles. I'm in the process of re-familiarizing myself with ASP.NET, after having been away from it for a few years, and this article series is proving invaluable in getting up and running quickly. Thanks again!
Dustin
Aliosat Mostafavi
AuthorHi
Thanks for helpful tutorial.
But i have a question.
what's different between :
[code]
public ApplicationRole() : base() { }
[/code]
and
[code]
public ApplicationRole(){}
[/code]
because parameterless base constructor() must implicit runs when created child class. is not it?