The first step in authorizing a user is authenticating them. There are any number of ways this could be accomplished, from basic Username/Password authentication against a database, to LDAP authentication, to biometrics, to the lowly Yubikey (which desperately needs more love). However, Microsoft only provides a small handful of authentication mechanisms, primarily based around Active Directory. For the RONet, we use a hybrid approach. We authenticate our users against Active Directory, but then verify that the AD user is authorized for our system. Admittedly, we could work this into the authorization step (discussed next week), but we don't want to let un-authorized users do anything, so we simply deny them at authentication.
Unfortunately, this means we needed to write a custom MembershipProvider, which can be found in the System.Web.Security namespace. .NET does do quite a bit of the work for Membership for us, but interface demanded by the MembershipProvider is pretty heavy. For our purposes this was a fairly easy step, we manage very little of the user's directory information and we can't do anything with their password, so for us this class is pretty light-weight. Unfortunately, this does mean that we have a lot of methods that do nothing.
First, ther are a series of required Properties:
- ApplicationName - The name of the Application you're authenticating for. We don't use this, but it's set in the Web.config
- EnablePasswordReset - Notes if the user can reset their password through our system. They can't
- EnablePasswordRetrieval - Notes if hte user can retrieve their password through our system. If this is ever anything other than false, you're doing something wrong. Passwords should always be hashed, and preferably salted before hashing. Never, ever store plain-text passwords.
- MaxInvalidPasswordAttempts - The number of times that a user can mess up their password before locking out their account. We don't use this.
- MinRequiredNonAlphanumericCharacters - Pretty straight forward. Only used when accepting new passwords. Not useful for us.
- MinRequiredPasswordLength - Same as above.
- PasswordAttemptWindow - How long, in minutes, to lock out a users account for when they mess up their password more than MaxInvalidPasswordAttempts. We don't actually lock out user accounts, so we don't really use this. We do use logic in the login page to prevent brute-forcing through our login system.
- PasswordFormat - Returns a MembershipPasswordFormat value noting how the password is stored. Again, never use the value Clear.
- PasswordStrengthRegularExpression - A regular expression (stored as a string) to match a password against to make sure it's acceptably strong. In my opinion any password fitness methods used should probably be stronger than a simple RE, but this is probably better than nothing.
- RequiresQuestionAndAnswer - Requires that you post a question and get an answer from the user before doing things like resetting their password.
- RequiresUniqueEmail - Tells the provider to ensure that no duplicate e-mails addresses exist for clients.
I'm not going to go through all the functions required in this class (go here for that) because based on the Properties, the functionality of the class is pretty straightforward. This class provides functions to do a fair amount of user management, from locking/unlocking accounts, to making new accounts or removing old ones. It allows Passwords to be recovered, or reset, and provides information necessary to facilitate the kind of security questions that we see all over the web. Most importantly, this class provides the mechanism for you to define how the data store is checked for user's information.
For our purposes, we don't care about User Management at this time (eventually, I will likely implement the functions to place user records in our local data store, but for the time being, that data is managed by a Classic ASP app). We will never likely care about Password Management, as that is a requirement for Central IT and the University Active Directory. Because of this, the functions which handle that functionality either return false (where appropriate), or throw exceptions (where appropriate). Sometimes the exception is a ProviderException, other times, it's an InvalidOperationException, though I'm on the fence about whether or not the IOE's should be replaced by ProviderExceptions.
It is important to note, that the only NotImplementedExceptions that I've left in the code are methods that I fully intend to implement. Like the Mono project, I believe that a NotImplementedException is the same as a TODO item. That's just kind of an aside, but it makes the most sense to me.
Moving on, we've got methods to get an individual users' MembershipUser.aspx) record. These are fairly straightforward, contaning the MembershipProvider Name, the user's username, unique key, email, Password restore question, and description, if their account is locked out, creation/logon dates etc. Lot's of information, but nothing too sensitive. You can search for a MembershipUser based on username or unique key. If you want their username based on their e-mail, you can either search for their username based on their e-mail, or get back a MembershipUserCollection (which only makes sense if RequiresUniqueEmail is false, obviously).
And the MembershipUserCollection comes up often. Internally, it appears to be a List which can only contain MembershipUsers, though it wasn't implemented with Generics. These methods are harder to implement as they require the idea of 'paging', where a request is made for a certain page of records, or a certain pageSize. You return to the user the MembershipUserCollection based on the search for their their e-mail, or username, or a complete list of users, and an out parameter which contains the total number of records in the entire data set.
One really nice thing about this MembershipProvider system is that you can allow certain options to be set by the User if that makes sense. There is an Initialize() method for the provider class which takes a Dictionary as an argument. With that, the user can specify the values they want for every property you provide. Now, for us, we're the only ones who will be using this class, and the options aren't really negotiable, so I've hard-coded them. But, this is a level of flexibility that is nice to have if you aren't implementing such a special purpose (albeit reusable within our unique problem space) MembershipProvider.
There is one thing I'd like to mention before I wrap this up, and that is some caveats I ran across in my authentication step. The method which validates a user's credentials is the ValidateUser method, which takes in a Username and Password and returns a Boolean value to indicate if the user should be allowed in to the system. My first draft of the method looked like this:
The code is fairly straight-forward. We try to log-in to the Active Directory server as the user, and recover that user's specific DirectoryEntry. We then check our local database (using LINQ) by finding a user with the given EmployeeID (I'd do an integer comparison, but the code wasn't originally written to do that, and the refactoring project hasn't begun), and then make sure that the user is Enabled, and not Cancelled. The call the SingleOrDefault will cause the statement to return false if no matching records are found.
Of course, I did say this was my first draft, so that fact that it didn't work is likely apparent. I already knew that LINQ was a lazy-loading API. For instance, if I were to drop the call to SingleOrDefault and save the query into a variable, I would get an IQueryable object, which I could choose to apply more constraints to. No call to SQL would be made until SingleOrDefault (or any method that returns an actualy value, or set of values) is called. It's fairly simple. It turns out, however, that DirectoryEntries are also lazy-loaders. If the username or password was invalid, this particular method would fail would throw an exception at the point I tried to access the Properties, not at the initialization of the DirectoryEntry object.
The revised code I settled on simply moved the LINQ into the try-block, since even a Database Query should result in a false return value. While I'm not doing any logging in the Catch block, certainly, I am not precluded from doing so.
That leaves only one final step to configuring a custom MembershipProvider, and that is defining that provider in the Web.config file for your site.
And that's it. Just refer to the Membership object in the HttpContext, and you'll now be using your custom MembershipProvider. The userIsOnlineTimeWindow is used to set the timeout on the Cookie that the user is given when they login (Yes, ASP.NET uses separate Session and Authentication cookies), but this provides seamless integration with existing ASP.NET controls with your custom User store. Logging a user in via FormsAuthentication looks exactly the same as if you were using any other authentication mechanism, allowing you to easily swap out MembershipProviders with little to no impact on the rest of your application, which is probably my favorite part.
Next time I'll talk about the next step of validating the user, Authorization. Including means to ensure that you can do more fine-grained control of a user's access than simply checking their broad role definitions.