On March 20, 2014, the ASP.NET team released the RTM version 2.0 of the new Identity framework. The new release brings with it some long-awaited new features, and marks a substantial expansion of the security and authorization capabilities available to ASP.NET applications of all types.
The ASP.NET Identity framework was originally introduced in 2013 as the follow-on to the ASP.NET Membership system, a staple of MVC applications for several years, but which was beginning to show its age. Originally, ASP.NET Identity presented a useful, if somewhat minimal API for managing security and authorization in the context of a public-facing web application built using ASP.NET. The Identity framework introduced modern features such as social network log-in integration, and easily extensible user model definitions.
Image by Ryan Somma | Some Rights Reserved
The new RTM release introduces the following features, among others:
- Extended User Account Definition, including Email and contact information
- Two-Factor Authentication via email or SMS messaging, functionally similar to that used by Google, Microsoft, and others
- Account Confirmation via email
- Administrative management of Users and Roles
- Account Lock-Out in response to invalid log-in attempts
- Security Token Provider to regenerate a user's security token in response to changes in security settings.
- Improved support for Social log-ins
- Easy Integration of Claims-Based Authorization
Identity 2.0 represents a substantial revision from the original version introduced last year. With the numerous new features, comes some added complexity. If, like myself, you had just recently found your way through the first iteration of the Identity framework, be ready. While you won't be starting over from scratch with version 2.0, there is a lot to learn.
In this article, we're going to take a look around, get familiar with the major components of the system, and in general familiarize ourselves with the new features, and where they fit in the overall scheme of things. We won't go into too much detail yet. Think of this as a familiarization tour.
If you are looking for more detailed how-to's, I will be adding posts over the next few weeks examining specific implementation concerns here:
If you are planning to use Identity 2.0 with Web Api, the landscape looks a little different. You may want to check out the following posts:
While we will be looking at a decent amount of code, it's not necessary yet to understand the details of what it all does - just get familiar with the general concepts, where the major components are located, and how things are structured.
Identity 2.0 does not slide smoothly into place for applications written using version 1. The additional capabilities appear to have required significant changes to the architecture, and the manner in which the Identity API is consumed from within the application. Upgrading an existing ASP.NET application from Identity 1.0 to the 2.0 version will require some new code, and is beyond the scope of this article. Be aware, though, that moving from Identity 1.0 to the 2.0 version is not a simple "plug-it-in-and-go" affair.
As of this writing, there is not a directly available ASP.NET MVC project template using Identity 2.0. In order to take Identity for a spin, you need to pull the example project libraries into an empty ASP.NET MVC project. First, Create a new ASP.NET project, and select the Empty Project template from the template options dialog:
Select the Empty ASP.NET Project Template:
Once you have created the new Empty project, you can get the Identity 2.0 sample project from Nuget by typing the following into the Package Manager Console:
Install the Sample Project from Nuget:
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
Once Nuget has done its thing, you should see a folder structure in the Solutions Explorer that looks quite like a standard MVC project. Nuget has basically added everything needed to compose a complete ASP.NET MVC project, including Models, Views, Controllers, and various components required for this basic application to run.
While at first glance the project components look fairly similar, a closer look will reveal some significant changes, and some added complexity.
In my mind, one of the strengths of the original Identity framework was also its primary weakness (strange how THAT works in software, isn't it?). The simplicity of the Identity version 1.0 made it extraordinarily easy to use, and relatively intuitive to figure out. On the other hand, the utter simplicity also meant that the feature set available "out-of-the-box" was limited, and to some, insufficient.
Just to get an idea, we'll take a quick look at some of the configuration that runs when our application starts, and compare it to the comparable code in an application which used Identity Version 1.0.
In both flavors of project, we find a class file named Startup.cs at the root level of the project. In this file a class named Startup
is defined, and makes a single call to the method ConfigureAuth()
. What we DON'T see anywhere in this file is an actual method named ConfigureAuth()
. This is because the rest of the code for the Startup
class is defined in a partial class tucked away in the App_Start folder. The code file is named Startup.Auth.cs, but if we open that, we find a standard partial class definition, which contains the ConfigureAuth()
method. In a project using the original version 1.0 of the Identity Framework, the standard code for ConfigureAuth()
looks like this:
Standard ConfigureAuth() Method Using Identity 1.0:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
In the above, we see some boilerplate code for configuring cookies, and some commented out code which can be uncommented, and then called to enable third-party logins from various social media providers.
In contrast, when we look at the ConfigureAuth()
method in our project using Identity 2.0, we see a bit more code has been added:
ConfigureAuth() Method from Project Using Identity 2.0:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);
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))
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(
DefaultAuthenticationTypes.TwoFactorCookie,
TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(
DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
}
}
Above, the first thing we notice are several calls to app.CreatePerOwinContext
, wherein we register callbacks to be invoked to create instances of the type specified by the type arguments. Type instances created are then available using the context.Get()
method.
What this tells us is that, at least for the purpose of the example project supplied by the Identity 2.0 team, Owin is now part of our application, and Identity 2.0 relies upon it to deliver the goods. I am not clear on whether Owin is REQUIRED in general for identity 2.0 to work, but for the purpose of our example project, it is.
UPDATE: Rick Anderson from the team at Microsoft mentions in a comment on my original blog post:
"OWIN is not required for all of identity 2.0, but specifically we use OWIN as a poor mans DI (CreatePerContext), and also we use owin to generate cookies/do authentication as part of the app. You can use UserManager/RoleManager stand alone without owin (i.e. if you wanted to still use FormsAuth with identity you could)"
We can also see some other new calls in the ConfigureAuth
method body setting up Two-Factor Authentication, and some additional cookie authentication configuration code not present in the previous version.
For our purposes here, we can assume that most of this stuff has been configured optimally for our basic use, and short of adding social media logins (which we're not going to look at in this article), we can leave this code alone. But bear in mind that this is where a lot of the Identity behavior in your application is set at runtime, based on the configuration components and helpers defined in another file in the App_Start folder, IdentityConfig.cs.
Before we go seeing what's up in IdentityConfig.cs, though, it will help us understand what's going on there is we first take a look at the ApplicationUser
class, defined for us in the Models folder.
If you have built out an application using the previous version of Identity framework, you may have run into the situation where you found the core IdentityUser
class rather limiting. Previously, Identity used a very simple IdentityUser
implementation which represented a very minimal user profile:
The Original IdentityUser Class From Identity Version 1.0:
public class IdentityUser : IUser
{
public IdentityUser();
public IdentityUser(string userName);
public virtual string Id { get; set; }
public virtual string UserName { get; set; }
public virtual ICollection<IdentityUserRole> Roles { get; }
public virtual ICollection<IdentityUserClaim> Claims { get; }
public virtual ICollection<IdentityUserLogin> Logins { get; }
public virtual string PasswordHash { get; set; }
public virtual string SecurityStamp { get; set; }
}
Of the properties available in the above, only the first three, Id
, UserName
, and Roles
were of much use from the business perspective of our application. The other items are mainly used by the security logic, which, while important, does not help us maintain useful information about users.
In a previous article, we examined how to extend Identity Accounts and implement Role-Based Authentication under Identity 1.0 to add more useful data, such as a user email address and/or other information which might be needed by our application.
With the advent of Identity 2.0, the need to create such work-arounds is diminished somewhat. While it is still possible to extend the Identity 2.0 ApplicationUser
class in a similar manner, the Identity team has taken care of some of the more common use-cases for us.
What we find is that the example project already contains a subclass ApplicationUser
, which is derived from a more complex default IdentityUser
implementation.
We find the definition for ApplicationUser
in the Models folder, in a file named IdentityModels.cs. We can see that the class definition itself is as simple as can be:
The ApplicationUser Class in Identity 2.0:
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
var userIdentity =
await manager.CreateIdentityAsync(this,
DefaultAuthenticationTypes.ApplicationCookie);
return userIdentity;
}
}
We see here that ApplicationUser
is, as stated previously, a sub-class of IdentityUser
. However, if we find the definition for IdentityUser
(using VS Go to definition context menu item) we see that IdentityUser
, as defined under the Identity 2.0 framework, is itself a sub-class of IdentityUser<TKey, TLogin, TRole, TClaim>
. When we look at the definition of THAT, we find a significantly different animal from the version 1.0 implementation:
IdentityUser Implementation from Identity 2.0:
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
{
public IdentityUser();
public virtual int AccessFailedCount { get; set; }
public virtual ICollection<TClaim> Claims { get; }
public virtual string Email { get; set; }
public virtual bool EmailConfirmed { get; set; }
public virtual TKey Id { get; set; }
public virtual bool LockoutEnabled { get; set; }
public virtual DateTime? LockoutEndDateUtc { get; set; }
public virtual ICollection<TLogin> Logins { get; }
public virtual string PasswordHash { get; set; }
public virtual string PhoneNumber { get; set; }
public virtual bool PhoneNumberConfirmed { get; set; }
public virtual ICollection<TRole> Roles { get; }
public virtual string SecurityStamp { get; set; }
public virtual bool TwoFactorEnabled { get; set; }
public virtual string UserName { get; set; }
}
Note in the above, a whole lot of those properties are again related to authorization and security, and not to our business needs for user data. However, the Email
and PhoneNumber
fields definitely go a long way towards minimizing the need for additional customization of the ApplicationUser
class.
But, what's with the weird generic type arguments in the class declaration?
The new version of IdentityUser
implements generic type arguments to allow for additional flexibility. As an example, recall that in Identity Version 1.0, the Id
property was a string. Here, the generic type argument TKey
allows us to specify the type of the Id
field. We can see in the above, the Id
property declaration returns the type specified by TKey
:
The Id Property Declaration:
public virtual TKey Id { get; set; }
Also of particular note, the Roles property, defined as follows:
The Roles Property:
public virtual ICollection<TRole> Roles { get; }
We can see the the Type TRole
is left open at compile time, and in fact is specified in the generic declaration of the IdentityUser
class. If we look at the type constraints in that declaration, we see the TRole
is constrained to the type IdentityUserRole<TKey>
which is not terribly different from the version 1.0 implementation. What IS different, and which represents a breaking change, is the definition of IdentityUserRole
itself.
Previously, in version 1.0 of the Identity Framework, IdentityUserRole
was defined as follows:
public class IdentityUserRole
{
public IdentityUserRole();
public virtual IdentityRole Role { get; set; }
public virtual string RoleId { get; set; }
public virtual IdentityUser User { get; set; }
public virtual string UserId { get; set; }
}
Compare with the Identity 2.0 Implementation:
public class IdentityUserRole<TKey>
{
public IdentityUserRole();
public virtual TKey RoleId { get; set; }
public virtual TKey UserId { get; set; }
}
See what happened there? The former contained references to an IdentityRole
object, and an IdentityUser
object. The version 2.0 implementation contains only Id
values. If you had done any customization under the previous version, such as we discussed in Implementing Group-Based Permissions, this will break things.
We will take a closer look at the new flexibility created with the new, extended IdentityUser
class in a subsequent post. For now, realize that while the basic user class definition has become more complex, it has also become significantly more flexible.
Since ApplicationUser
sub-classes IdentityUser
, all of the above properties are available to ApplicationUser
, which is the basic implementation used in the example application.
Now that we have taken a quick look at the new ApplicationUser
implementation, the configuration components and helpers we are about to look at will make more sense.
While the ConfigAuth()
method of the Startup
class is where the runtime configuration for Identity happens during startup, we actually use the components defined in the IdentityConfig.cs file to configure how most of the Identity 2.0 features behave in our application.
If we examine the content of the IdentityConfig.cs file, we find that there are a number of individual classes defined therein. We could split each one out into its own code file, but for now, we will just examine each class independently, despite the fact that they all share the same file location in our project. Not that all of these classes are enclosed in the ApplicationName.Models
namespace.
The first things we run into in the IdentityConfig.cs file are two helper classes, ApplicationUserManager
and ApplicationRoleManager
. Be ready - large blobs of code with generic types ahead!
The Identity 2.0 Application User Manager Class:
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>()));
manager.UserValidator =
new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;
manager.RegisterTwoFactorProvider("PhoneCode",
new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is: {0}"
});
manager.RegisterTwoFactorProvider("EmailCode",
new EmailTokenProvider<ApplicationUser>
{
Subject = "SecurityCode",
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 virtual async Task<IdentityResult> AddUserToRolesAsync(
string userId, IList<string> roles)
{
var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store;
var user = await FindByIdAsync(userId).ConfigureAwait(false);
if (user == null)
{
throw new InvalidOperationException("Invalid user Id");
}
var userRoles = await userRoleStore
.GetRolesAsync(user)
.ConfigureAwait(false);
foreach (var role in roles.Where(role => !userRoles.Contains(role)))
{
await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false);
}
return await UpdateAsync(user).ConfigureAwait(false);
}
public virtual async Task<IdentityResult> RemoveUserFromRolesAsync(
string userId, IList<string> roles)
{
var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;
var user = await FindByIdAsync(userId).ConfigureAwait(false);
if (user == null)
{
throw new InvalidOperationException("Invalid user Id");
}
var userRoles = await userRoleStore
.GetRolesAsync(user)
.ConfigureAwait(false);
foreach (var role in roles.Where(userRoles.Contains))
{
await userRoleStore
.RemoveFromRoleAsync(user, role)
.ConfigureAwait(false);
}
return await UpdateAsync(user).ConfigureAwait(false);
}
}
For what appears to be a large chunk of code, ApplicationUserManager
actually only provides a handful of very important functions - Adding new Users, Adding Users to Roles, and Removing Users from Roles. However, ApplicationUserManager is derived from the UserManager<ApplicationUser>
class, so all the functionality provided by UserManager
is also available to ApplicationUserManager
. Other than that, there is a static Create()
method defined which returns an instance of ApplicationUserManager
itself. It is in this method that much of your user configuration settings and default are set up.
Of particular note in the Create()
method is the call to context.Get<ApplicationDBContext>()
. Remember earlier, when we looked at the ConfigAuth()
method with those calls to CreatePerOwinContext
and we passed in a callback method? The call to context.Get<ApplicationDbContext>()
executes that call back, in this case, the static method ApplicationDbContext.Create()
. We'll see more of this shortly.
If you look closely, you can see that user authorization, authentication, and management settings and defaults are set up in the Create()
method, before returning a new ApplicationUserManager
instance to the caller. Also, this is where two-factor auth services are set up. We can see that most of the settings are fairly self-explanatory. However, the two services bear a closer look. We'll come back to that in a moment. First, a quick look at the ApplicationRoleManager
class:
The Application Role Manager Class:
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options,
IOwinContext context)
{
var manager = new ApplicationRoleManager(
new RoleStore<IdentityRole>(
context.Get<ApplicationDbContext>()));
return manager;
}
}
As with ApplicationUserManager
, we can see that ApplicationRoleManager
is derived from RoleManager<IdentityRole>
and thus brings with it all of the functionality offered by that class as well. Once again, we see a static Create()
method returning an instance of the class itself.
Also in the IdentityConfig.cs file are two service classes, EmailService
and SmsService
. Out of the box, these two classes are basically empty wrappers, providing an abstraction within which you can implement Email and/or SMS services required for two-factor authentication and account validation.
The ASP.NET Identity Email Service Class:
public class EmailService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
return Task.FromResult(0);
}
}
The ASP.NET Identity SmsService Class:
public class SmsService : IIdentityMessageService
{
public Task SendAsync(IdentityMessage message)
{
return Task.FromResult(0);
}
}
Note that both of these classes implement a common interface, IIdentityMessageService
. Also recall in the ApplicationUserManager.Create()
method, the following lines:
Setting Up the Email Service and the SMS Service in the ApplicationUserManager Create Method:
manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is: {0}"
});
manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
{
Subject = "SecurityCode",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
We can see that during the course of the Create()
method new instances of both EmailService
and SmsService
are initialized, and referenced by corresponding properties on the new ApplicationUserManager
instance.
In creating the Identity Sample project, the Identity team has added a handy helper class, also found in the IdentityConfig.cs file, which wraps commonly needed calls for sign-in and authentication into an efficient and easy to use API. We can see how these methods are consumed by examining the AccountController
in the Controllers folder. First, though, let's take a look at the SignInHelper
class itself.
As with the previous examples, we're not going to go into much detail here, other than familiarizing ourselves with the basic structure of the class, the methods available, and get a rough idea how we might use the methods in SignInHelper
from within our application.
The Sign-In Helper Class:
public class SignInHelper
{
public SignInHelper(ApplicationUserManager userManager, IAuthenticationManager authManager)
{
UserManager = userManager;
AuthenticationManager = authManager;
}
public ApplicationUserManager UserManager { get; private set; }
public IAuthenticationManager AuthenticationManager { get; private set; }
public async Task SignInAsync(ApplicationUser user, bool isPersistent, bool rememberBrowser)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie,
DefaultAuthenticationTypes.TwoFactorCookie);
var userIdentity = await user.GenerateUserIdentityAsync(UserManager);
if (rememberBrowser)
{
var rememberBrowserIdentity =
AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id);
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = isPersistent },
userIdentity,
rememberBrowserIdentity);
}
else
{
AuthenticationManager.SignIn(
new AuthenticationProperties { IsPersistent = isPersistent },
userIdentity);
}
}
public async Task<bool> SendTwoFactorCode(string provider)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return false;
}
var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);
return true;
}
public async Task<string> GetVerifiedUserIdAsync()
{
var result = await AuthenticationManager.AuthenticateAsync(
DefaultAuthenticationTypes.TwoFactorCookie);
if (result != null && result.Identity != null
&& !String.IsNullOrEmpty(result.Identity.GetUserId()))
{
return result.Identity.GetUserId();
}
return null;
}
public async Task<bool> HasBeenVerified()
{
return await GetVerifiedUserIdAsync() != null;
}
public async Task<SignInStatus> TwoFactorSignIn(
string provider,
string code,
bool isPersistent,
bool rememberBrowser)
{
var userId = await GetVerifiedUserIdAsync();
if (userId == null)
{
return SignInStatus.Failure;
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
{
await UserManager.ResetAccessFailedCountAsync(user.Id);
await SignInAsync(user, isPersistent, rememberBrowser);
return SignInStatus.Success;
}
await UserManager.AccessFailedAsync(user.Id);
return SignInStatus.Failure;
}
public async Task<SignInStatus> ExternalSignIn(
ExternalLoginInfo loginInfo,
bool isPersistent)
{
var user = await UserManager.FindAsync(loginInfo.Login);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
return await SignInOrTwoFactor(user, isPersistent);
}
private async Task<SignInStatus> SignInOrTwoFactor(ApplicationUser user, bool isPersistent)
{
if (await UserManager.GetTwoFactorEnabledAsync(user.Id) &&
!await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id))
{
var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));
AuthenticationManager.SignIn(identity);
return SignInStatus.RequiresTwoFactorAuthentication;
}
await SignInAsync(user, isPersistent, false);
return SignInStatus.Success;
}
public async Task<SignInStatus> PasswordSignIn(
string userName,
string password,
bool isPersistent,
bool shouldLockout)
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
return SignInStatus.Failure;
}
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
if (await UserManager.CheckPasswordAsync(user, password))
{
return await SignInOrTwoFactor(user, isPersistent);
}
if (shouldLockout)
{
await UserManager.AccessFailedAsync(user.Id);
if (await UserManager.IsLockedOutAsync(user.Id))
{
return SignInStatus.LockedOut;
}
}
return SignInStatus.Failure;
}
}
That's a lot of code to wade through, and like I said, we're not going to look too closely right now. We are mainly here to find our way around, get oriented. We can see that the methods in this class all appear to be related to sign-in and authorization responsibilities.
The methods available in the SignInHelper
class all represent some of the new features introduces in Identity 2.0. We see a familiar SignInAsync()
method, of course, but then we see a host of new methods related to two-factor authorization and external log-in. Further, there appears to be a lot more going on here in order to sign in than previously.
We can look at the Login method on AccountController
in the example project for an example of how authentication is handled in Identity 2.0:
The Login Method on Account Controller Using Identity 2.0:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await SignInHelper.PasswordSignIn(model.Email, model.Password,
model.RememberMe,
shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresTwoFactorAuthentication:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
If we take a quick look at how the log-in task was handled in an MVC project using Identity 1.0, we can go straight to the AccountController.Login
method, and we find the following bit of code:
The Login Method on Account Controller Using Identity 1.0:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindAsync(model.UserName, model.Password);
if (user != null)
{
await SignInAsync(user, model.RememberMe);
return RedirectToLocal(returnUrl);
}
else
{
ModelState.AddModelError("", "Invalid username or password.");
}
}
return View(model);
}
Within the method above, we call into a UserManager
class, similar to the code we saw for the SignInHelper
. We also call the SignInAsync
method, also defined directly in AccountController
:
The SignInAsync Method on Account Controller Using Identity 1.0:
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var identity = await UserManager.CreateIdentityAsync(
user, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(
new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
Obviously, with the added features related to security, authentication, and authorization comes additional complexity, even at the basic log-in level.
If you have spent any time at all working with ASP.NET MVC in general, and Identity in particular, you are likely familiar with the ApplicationDbContext
. This is the default Entity Framework implementation class by which your application accesses and stores Identity-related data.
In the example project, the team has set this up a little differently than in the standard ASP.NET project using Identity 1.0. First, if we take a look in the IdentityModels.cs file, we find the ApplicationDbContext
class defined thusly:
The ApplicationDbContext Class from Identity 2.0 Example Project:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false)
{
}
static ApplicationDbContext()
{
Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
The code above sets up two static methods, Create()
, and another, ApplicationDbContext(),
which sets a database initializer. This latter method is called during startup, and performs whatever database initialization is established in the ApplicationDbInitializer
class.
If we go back to the IdentityConfig.cs file, we find the ApplicationDbInitializer
defined like so:
The ApplicationDbInitializer Class from the IdentityConfig.cs File:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext>
{
protected override void Seed(ApplicationDbContext context)
{
InitializeIdentityForEF(context);
base.Seed(context);
}
public static void InitializeIdentityForEF(ApplicationDbContext db)
{
var userManager = HttpContext
.Current.GetOwinContext()
.GetUserManager<ApplicationUserManager>();
var roleManager = HttpContext.Current
.GetOwinContext()
.Get<ApplicationRoleManager>();
const string name = "admin@admin.com";
const string password = "Admin@123456";
const string roleName = "Admin";
var role = roleManager.FindByName(roleName);
if (role == null)
{
role = new IdentityRole(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);
}
var rolesForUser = userManager.GetRoles(user.Id);
if (!rolesForUser.Contains(role.Name))
{
var result = userManager.AddToRole(user.Id, role.Name);
}
}
}
As it is currently configured, this initializer will drop and re-create the database is the model schema (as defined by our code-first model objects) has changed. Otherwise, it will continue using the existing database.
If we want to drop and re-create the database every time our application is run we could change the base class from which it inherits to DropCreateDatabaseAlways<ApplicationDbContext>
. We might want to do this during development if we wanted to start with an empty (or nearly so) data set every time for testing, for example.
Also, take note of the InitializeIdentityForEF()
method. This method performs a function similar to that of the Seed()
method when we use EF Migrations, allowing us to initialize the database with some data. In this case, the example project is set up with a pre-defined admin user, password, and role.
In this article we have looked broadly at where some of the new features and configuration items live in an ASP.NET MVC project using the Identity 2.0 framework. There is a lot more to it, and we will look at specifics in several upcoming posts.
UPDATE: As of 5/13/2014, the Identity source code (a version of it, anyway) can be found in the ASP.NET vNext Github repo. Note that because this is essentially a beta release of ASP.NET, the code in this repo may or may not reflect the actual code that runs when you install Identity 2.0 in a project. But it is worthwhile to note that you can explore this code base to at least get an idea of what goes on "under the hood" with the Identity 2.0 framework.
For now, explore and run the example project, and get more familiar with how things work.
The Following Focus on Using the Identity 1.0 Framework:
CodeProjectJohn on Google