This release of Visual Studio 2013 and ASP.NET features YAMF - yet another membership framework. The new features build on top of an assembly and NuGet package named Microsoft.AspNet.Identity.Core.
The core identity abstractions are interface based and revolve around the concepts of users and stores. Users are the people who are happy to authenticate themselves to your web application, while stores are the components happy to persist and retrieve user information from a data source.
These abstractions are more flexible and composable than the membership providers from previous releases of ASP.NET and don’t rely on the “membership provider” base classes that have been around since ASP.NET 2.0.
In the next few blog posts we’ll look at how the components work, how to customize the identity features, how to work with 3rd party claims, and how to build a custom store using a non-relational database (specifically MongoDB).
This first post in the series looks at the core abstractions. It’s important to note that the Identity Core assembly defines mostly abstractions with no implementations. A later post will look at components provided by Microsoft to implement these interfaces and provide membership features backed by SQL Server.
A user object must implement the IUser interface, which requires every user to have at least an ID and a name. The most notable aspect of IUser is the Id property of type string. Strings work well for GUIDs but can be a bit tricky when using integers for keys as is common with SQL Server. More details in a future post.
The I*Store interfaces in the Core Identity assembly are factored to offer various levels of functionality.
IUserStore defines the bare minimum functionality you’d want for users – create, retrieve, update, and delete (CRUD) functionality. If your website wants to allow users to create local accounts with passwords, you’ll want a component implementing IUserPasswordStore (which is an IUserStore). For third party logins (Twitter and Facebook, for example), add in IUserLoginStore, and for claim storage there is an IUserClaimStore. Also, not shown in the class diagram above, is a collection of interfaces for roles (IRole, IUserRoleStore, etc). Let the debate on roles versus claims begin.
One of the curious aspects of IUserStore is its generic constraint.
public interface IUserStore<TUser> : IDisposable where TUser : IUser { // ... }
The implication is that any user object you want to use with the Identity features will need to implement IUser, which requires a dependency on the core identity assembly. It’s a bit odd to place a constraint on an interface definition since an interface doesn’t provide an implementation, but presumably the constraint helps to enforce the concept of all users having username and IDs of type string. The method signatures for IUserStore make this assumption, as does the UserManager.
The UserManager is a concrete class and it is the UserManager that provides the domain logic for working with user information. The UserManager knows when to hash a password, when to validate a user, and how to manage claims.
There are a few extensibility points with the UserManager, for example, the manager has a number of properties you can use to set a custom user validator (any object implementing IIdentityValidator), a custom password validator, and a custom password hasher. The primary extensibility point is via the UserManager constructor, which allows you to pass in any object implementing IUserStore. If you want to use a UserManager to manage user information stored in SQL Server, we’ll look at pre-built classes to do this in a later post, but it is also easy to create an IUserStore to work with a document database or other forms of storage.
Remember an IUserStore doesn’t know how how to work with user passwords, only an IUserPasswordStore knows about passwords. The UserManager is aware of the different core interfaces and will try to work with the capabilities of the store object given in the constructor. For example, if you use FindByIdAsync the UserManager can use the user store to query for a user by ID, but if you invoke FindAsync (which takes a username and a password), and the underlying store doesn’t implement the IUserPassword store interface, the UserManager will be forced to throw an exception.
It’s tricky to use a single concrete class for everything from users to passwords to claims, and tradeoffs must be made. We’ll also look at some of the tradeoffs in upcoming posts.
Something you might have noticed in the class diagrams is that every nearly every method on the store and manager classes is an async method. The return types for these methods are all Task<T>, where the Task represents the outstanding work of querying or updating user information.
These async methods work well with the Entity Framework version 6, which now includes an async API. We’ll also look at implementing these methods using a non-async data source in a future post.
The new Identity features are a welcome break from the dated model of the ASP.NET membership providers and more flexible than the the SimpleMembershipProvider in the last ASP.NET release.
Some developers will notice a lack of features compared to the membership providers of old (like the ability to track the last login and bad password attempts). Some of these features are easy to add, but it will be interesting to see the challenges that result if Microsoft attempts to add these features to the Identity bits in a future release (expect more interface definitions to appear).
Something I learned when building Memflex is that a membership framework has to make some assumptions and build itself around some core constraints which can limit the usefulness of a membership component in some applications – the needs for membership are just so varied in this big world. Still, the Identity bits have done a good job of abstracting away persistence concerns from the logic of managing users, logins, clams, and passwords, and as we’ll see in a future post (lots of future promises in this post), the implementation of the persistence APIs is mostly formulaic.