This is part three of my articles on writing Custom ASP.NET Authentication. The first article served merely as introduction, but the second delved into writing a Membership Provider and what that entails.
Now that you have a Membership Provider written, and you now have a method to authenticate and manage users. However, being able to authenticate the user is only half the problem. If you're had to customize the login process, odds are you also are going to need to customize the authorization process as well.
The most common means currently to do this is to separate the concerns out into "Roles". For instance, some users are Administrators, some users are Customers, some users are Salespeople, etc. Users can fit into multiple roles, but ultimately what they're allowed to access is based on the Roles they've been assigned. This works well for authenticating users against certain activities, but it doesn't really define what resources they have access to. We'll get to that later, besides, for many cases, basic Role-based Authentication will work fine.
In ASP.NET this process is driven primarily by two Interfaces: IPrincipal and IIdentity.
The Identity is the interior class, so we'll start there. The Interface is simple, including merely three Read-Only Properties. The first, a string representing the Name, the second a string representing AuthenticationType, and the final being a boolean IsAuthenticated. For us, this was not enough, as the Name field (which ASP.NET generally uses as the user name), is not what we use internally to identify the user. Luckily, we can freely add new Properties, since the rest of the API only requires it to be a IPrincipal.
So, what do these values mean? IsAuthenticated is generally set to true anytime a user has given a valid username and password. The reason for the property appears to act as sort of a gatekeeper into the rest of the class. In our Application, before I cast the IPrincipal into our application specific type, I test IsAuthenticated, because I know that if it's false, then I've not set up the user with the correct Principal.
As for the AuthenticationType, I typically leave it as whatever the base authentication mechanism was. For our purposes we're using Forms-based authentication, so we end up saving this as "Forms". I never use this variable for anything, and I'm unclear if the framework does, but I'm fairly sure it's important to leave this value as what it's defined as in the Web.config. If nothing else, consistency is king, particularly in a system as dense and opaque as ASP.NET.
The name, I set to the Username for the user, but only because that better matches the name (and datatype) of the property. Internally, I still use the numeric ID which we use to uniquely identify our users, and that is what the Identity takes in the constructor. The name is merely calculated on each access of the property.
The second interface, and arguably more important is the Principal. It's even simpler, as it only contains the Identity (again, Read-Only), as defined above, and an "IsInRole" method, which takes a string and returns a boolean. This makes using the class very easy. Need to know if the user is an admin?
if (User.IsInRole("administrators")) { // Do stuff for Admins }
That simple. How you determine if a user is in a role or not is up to you. Active Directory Authentication checks AD groups for membership, for our purposes, we look into a database to determine if a user fits a certain role or not. You can define as many roles as you'd like.
This interface can be extended as well (obviously), but that discussion will wait until next time, where we talk about Claims-based authentication. Right now, there is one very, very important caveat regarding using your custom Principal and custom Identity. Namely, ASP.NET will not remember them between page loads. The ASP.NET login cookie doesn't contain information about the principal for the user, so the next time the user hits your page, the system will view them as nothing more than a simple forms-authenticated user with a basic Principal and Identity that fails to give you access to the methods you've defined.
This is simple enough to work around, in your Global.asax file.
protected void OnPostAuthenticateRequest(object sender, EventArgs e) { if (User.Identity.IsAuthenticated && User.Identity.AuthenticationType == "Forms") { var ident = (FormsIdentity)User.Identity; var userInfo = Membership.GetUser(ident.Name); var principal = new CustomPrincipal( new CustomUserIdentity(int.Parse((string)userInfo.ProviderUserKey), User.Identity.IsAuthenticated, User.Identity.AuthenticationType)); HttpContext.Current.User = principal; Thread.CurrentPrincipal = principal; } }
Since ASP.NET so helpfully recasts your Identity into one of the base types, you're basically stuck doing this on every page load. First we take the current Identity, which has been helpfully made a FormsIdentity, then we get their UserId from the Membership (remember, we made that the providerUserKey), then we declare our new Principal, giving it a new Identity as an argument. Since we did use Forms authentication, we use the values defined in the current Identity object to fill in parts of our new Identity object. If I wanted to allow non-forms based authentication, that should be possible as well, just don't check the Authentication type, and leave the Identity an IIdentity, and you'll be fine. For my purposes, non-Forms based authentication could be a problem, so I simply disallow it.
Once you've declared the new Principal, replace the default Principal with your custom one and you're done. The only potential caveat is that any attempts to use your custom methods will require a type-cast into the Custom type, since the Interface doesn't define your custom stuff, but if all you want is the IsInRole method, the cast is basically pointless.
This system isn't horrid, my only real complaint with it is that ASP.NET fails to remember what I've done between pageloads. I know that the web is stateless, but with all the other work Microsoft did in trying to make ASP.NET seem state-full, this seems like quite the oversight.