ASP.NET MVC Authentication - Customizing Authentication and Authorization The Right Way
We're continuing the ASP.NET Authentication series (yes, I'm doing a few overlapping series, and yes, it's making me dizzy). The previous post covered Global Authentication and Allow Anonymous. This one continues with a simple tip that can be summed up as follows: keep it simple by extending rather than rewriting.
I see a lot of questions that involve unnecessary complications, and very often it's due to customizing authentication and authorization. For example, developers see that the AuthorizeAttribute won't work for their case, so they start to write a lot of code - using HttpModules, custom view engines, injecting authentication services and sprinkling authorization service calls throughout their controllers, etc.
Sometimes that's necessary, but it's rare. Most of the time you can handle things with either a custom membership provider, a subclassed AuthorizeAttribute, or both. Craig Stuntz summed this up well in a blog post back in 2009:
If you are developing a web application which requires authentication or security features not included in the regular ASP.NET membership feature, you might decide to implement these features yourself. But it seems as if the first instinct of many ASP.NET MVC developers is to do this by customizing their Controllers, because they’ve decided that AuthorizeAttribute can’t possibly serve their needs. They will decide that the way to do this is to write some sort of parent Controller type which examines the currently logged-in user when an action executes and changes the result of the action based on who they are. Others will try to re-implement AuthorizeAttribute without ever examining the source code for the original.
These are very bad approaches, for two important reasons:
- They don’t work. If your action result is cached by ASP.NET, then the action will not even run the next time it is requested. AuthorizeAttribute handles this correctly. Code you write in a Controller cannot handle this. Code you write in a custom action filter could work, if you cut and paste the implementation from AuthorizeAttribute. But AuthorizeAttribute is unsealed, so why cut and paste, when you can subtype?
- The modularity is wrong. You should aim to develop MVC sites which can be used with any authentication (or role) provider, whether it is ASP.NET membership, domain authentication, OpenId, or a custom membership provider. Wiring authentication concerns into a Controller makes this extremely difficult.
Craig recommends the same thing I'll be recommending - leverage the existing security systems in ASP.NET and ASP.NET MVC.
ASP.NET MVC's authorization system runs directly on top of the existing ASP.NET security system, and both have well established and tested extensibility points.
- If you need to customize the way ASP.NET MVC integrates with the underlying ASP.NET security system, subclass the AuthorizeAttribute
- If you need to customize the way the underlying ASP.NET membership system works, leverage the existing ASP.NET security provider system
I'll throw in one more - make sure you really need to customize anything at all.
Step Zero: Do you need to customize anything?
I've seen some examples that use the techniques below to implement authorization features... which didn't need implementing because they were already built in. AuthorizeAttribute, for example, already includes support for role-based authorization, but I've seen sample code that "adds in" role checking.
I've also seen examples which were built because the author assumed that AuthorizeAttribute only worked with Forms Authentication. That's not true - it just verifies if the user (1) is authenticated (2) is in the listed users and/or roles (if any are set). The same AuthorizeAttribute works with other authentication methods - the same attribute is also used with Windows Forms in the Intranet Application template, as well.
The AuthorizeAttribute has a pretty narrowly defined job, so it doesn't take much work to verify whether it already does what you need - check first.
Quick Note: Authentication and Authorization
Any sufficiently long article on web security must eventually devolve into distinguishing between authentication and authorization, so here goes:
- Authentication is the act of establishing who the user is.
- Authorization is the act of determining if that user should have access to a resource.
A user may be authenticated but not authorized to access a resource - e.g. a simple user isn't authorized to access site administration pages. A user may be authorized, but not authenticated - e.g. a site which allows anonymous access, a site which controls access using an API / access key, etc.
Customizing ASP.NET MVC's Interaction with ASP.NET Authorization by subclassing AuthorizeAttribute
Subclassing an AuthorizeAttribute is pretty straightforward. In most cases you just create a class that inherits form AuthorizeAttribute and override AuthorizeCore. Here's a very simple example: a key based login.
A simple key based AuthorizeAttribute
In this example, we'll be setting up a custom authorization scheme based on a key which will be validated using a very simple algorithm. This isn't secure for any number of reasons, but with some minor modifications (e.g. expiring a key once it is used) it would be sufficient for things like simple beta program for a pre-release website.
We'll accept a parameter called X-Key and validate that it's a number that passes a simple check.
To start with, we'll create a new class called KeyAuthorizeAttribute that inherits from AuthorizeAttribute:
public class KeyAuthorizeAttribute : AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { string key = httpContext.Request["X-Key"]; return ApiValidatorService.IsValid(key); } } public static class ApiValidatorService { public static bool IsValid(string key) { int keyvalue; if (int.TryParse(key, out keyvalue)) { return keyvalue % 2137 == 7; } return false; } }
This AuthorizeCore method checks a value (via header, querystring, form post, etc.) and calls into a service to validate it. In this case, validation is a simple static method that runs our validation algorithm. In your case, you'd probably want to check against a list of pre-issued keys in a database, call out to an external service, etc. AuthorizeCore returns a boolean value - pass or fail.
We can then slap that [KeyAuthorize] attribute on any action or controller in the site, or register it globally (as shown in my previous post).
This request would be allowed: http://localhost:8515/?X-Key=26381272 (because 26381272 mod 2137 equals 7)
This request would be denied: http://localhost:8515/?X-Key=12345
Handling Authorization Failures
AuthorizeAttribute is based on Forms Authentication, so when a request fails a call to the AuthorizeCore method of an applicable AuthorizeAttribute, it will by default redirect to the login page so that, hopefully, the user can get authorized. I walked through the mechanics of this redirection process in a previous post.
The default login page doesn't make any sense in a lot of scenarios, including the example above. If someone comes to my site with a missing or incorrect API key, the login page isn't going to help them. For that specific case, I'd perhaps want to redirect them to a page that tells them how to apply for an access key.
If you need to change what happens when users fail authentication, you've got a few options:
- If you want to change the site-wide redirection URL for when a request fails authorization, you can change the authentication/forms/loginUrl setting in web.config. Keep in mind, though, that this affects all authentication redirects for your entire application.
- If you want to run custom logic - including but not limited to redirecting to a URL - you can override the AuthorizeAttribute's HandleUnauthorizedRequest method.
Many more examples
This is a very simple example. You can find a lot more by searching on the override code above, e.g. "override authorizecore httpcontextbase". Some examples:
- Custom ASP.NET MVC Authorization with Facebook Connect
- ASP.NET MVC Forms authentication against an external web service
- Custom authorization to restrict editing based on the owner for a resource
- Twitter @Anywhere Authorization
- ASP.NET MVC Authentication based on Route Params
- Customizing ASP.NET MVC Basic Authentication
- Federated Identity with Multiple Partners
Note: The last one on the list implements the base AuthorizeAttribute interfaces rather than subclassing AuthorizeAttribute, which bears some discussion.
Subclass AuthorizeAttribute or Implement FilterAttribute, IAuthorizationFilter?
If you just look at the AuthorizeCore code in the AuthorizeAttribute, you may think it's so trivial you might as well just build your own IAuthorizationFilter from scratch. The problem with that idea is that it's very easy to do that incorrectly. A large amount of the code in the AuthorizeAttribute is there to make sure it works correctly with caching. If you get this wrong, you can very easily open yourself up to this scenario:
- Authorized user accesses an action and is correctly granted access.
- The action uses output caching, so the output is cached for future views.
- Unauthorized user accesses the action. Since it was cached, the restricted (or user-specific) content is served to the unauthorized user.
The AuthorizeAttribute has ensured that the code in the CacheValidateHandler and OnCacheAuthorization methods interact correctly with ASP.NET's caching system. Unless you really know what you're doing, it's a much better idea to start by subclassing AuthorizeAttribute.
Using AuthorizeAttribute with ASP.NET Web API
ASP.NET Web API uses the same AuthorizeAttribute scheme. It works the same way - drop an attribute on actions, API controllers, or globally, and you're set. All good so far.
But, if you use System.Web.Mvc.AuthorizeAttribute (or a subclassed attribute) on an Action Controller, nothing will happen. ASP.NET Web API uses a very similar, but different AuthorizeAttribute, found in the System.Web.Http namespace. There are some important differences (beyond the scope of this post), but a good place to look is at how the two AuthorizeAttributes handle unauthorized requests:
- System.Web.Mvc.AuthorizeAttribute.HandleUnauthorizedRequest() sets the response to an HttpUnauthorizedResult
- System.Web.Http.AuthorizeAttribute.HandleUnauthorizedRequest() sets the response to an HttpResponseMessage(HttpStatusCode.Unauthorized)
This reflects the difference between the target focus of both systems - ASP.NET MVC primarily focuses on HTML code that's viewed by people in a browser, and ASP.NET Web API primarily focuses on HTTP traffic that's handled by code. In ASP.NET Web API, you don't tell someone they're not authorized with a login page, you return the appropriate HTTP status code.
Fluent Security
If you have complex authorization requirements - particularly around configuration - you might want to look at Fluent Security. It provides a fluent, code-based configuration system, which lets you do define your application's authentication requirements in one place, like this:
SecurityConfigurator.Configure(configuration => { // Let Fluent Security know how to get the authentication status of the current user configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated); // Let Fluent Security know how to get the roles for the current user configuration.GetRolesFrom(() => MySecurityHelper.GetCurrentUserRoles()); // This is where you set up the policies you want Fluent Security to enforce configuration.For<HomeController>().Ignore(); configuration.For<AccountController>().DenyAuthenticatedAccess(); configuration.For<AccountController>(x => x.ChangePassword()).DenyAnonymousAccess(); configuration.For<AccountController>(x => x.LogOff()).DenyAnonymousAccess(); configuration.For<BlogController>(x => x.Index()).Ignore(); configuration.For<BlogController>(x => x.AddPost()).RequireRole(BlogRole.Writer); configuration.For<BlogController>(x => x.AddComment()).DenyAnonymousAccess(); configuration.For<BlogController>(x => x.DeleteComments()).RequireRole(BlogRole.Writer); configuration.For<BlogController>(x => x.PublishPosts()).RequireRole(BlogRole.Owner); }); GlobalFilters.Filters.Add(new HandleSecurityAttribute(), 0);
Customizing ASP.NET MVC Authorization using the existing ASP.NET Security systems
ASP.NET has been around for a long time. It's been beaten on pretty hard, and the existing systems have undergone a huge amount of real-world testing. When you run into a constraint that pushes you towards writing some custom code, the best approach is to make sure you understand how the existing systems work and integrate with them as closely as possible.
Since ASP.NET has been around for a long time, there is a good amount of existing information on the security system. It's a big topic. I'm just going to (try to) summarize, pointing out the best extension points from an ASP.NET MVC point of view.
Extending the Forms Authentication provider
As mentioned earlier, authentication is the process of establishing who the user is. It doesn't say anything about what access you've got, it just verifies that you are who you claim to be. ASP.NET has some built in systems to handle that - the Forms Authentication provider handles browser based login and account management, and there's a Windows Authentication provider which integrates with the Windows authentication. If you need to modify how authentication works, you'll almost certainly be working with the Forms Authentication provider.
You might assume that you extend authentication by plugging in a new Authentication Provider, but that's not the case. There are two in the box Authentication Providers, and you can't add new ones. That's pretty much never an issue, though, because the Forms Authentication provider gives you plenty of hooks for extension.
Warning: The next paragraph is exceptionally nerdy, but it sets some important background for interfacing with Forms Authentication.
Forms Authentication uses a Forms Authentication Ticket to track your identity - essentially your authenticated username. The ticket is stored in an encrypted Forms Authentication Cookie. There's support for cookieless authentication, which automatically appends cookie information via an encrypted querystring value. There are two main components that make Forms Authentication work - the FormsAuthetication class which sets the authentication cookie for authenticated users, and a FormsAuthenticationModule which checks for the cookie, authenticates you and sets your identity in the HttpContext. Since FormsAuthenticationModule is an HTTP module, it runs for every request, way at the beginning of the pipeline. That's an important part - a secure authentication system needs to check authentication at the beginning of the request.
This is all underlying machinery - the important part is that when someone logs in, something in your application calls FormsAuthentication.SetAuthCookie(). That sets the Authentication Cookie, which is then checked by the FormsAuthenticationModule on each request. You can see an example of how the default ASP.NET MVC AccountController Login method uses it:
[AllowAnonymous] [HttpPost] public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid) { if (Membership.ValidateUser(model.UserName, model.Password)) { FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } // If we got this far, something failed, redisplay form return View(model); }
A good example is DotNetOpenAuth, which adds OpenID, OAuth, and InfoCard authentication to ASP.NET by integrating with the ASP.NET Forms Authentication ticket. You can see how this works in the DotNetOpenAuth OpenAuthAuthenticationTicketHelper.cs class, and in the OpenIdRelyingPartyMvc sample's UserController.
The important points here:
- While it's tempting to look at your custom authentication needs as completely unique, most can be handled by determining who a user is (your code) and then telling ASP.NET Forms Authentication to track them.
- If you decide that you'd rather just handle authentication outside of ASP.NET, you've got a lot to think about. You'll be writing a lot of new code which hasn't gone through near the security testing, beating, improvements, etc., that Forms Authentication has.
The ASP.NET Roles and Membership System
The above authentication discussion was completely independent of where and how the actual user information was stored, and that's important. Forms Authentication doesn't handle passwords, logins, user roles, etc., it just tracks users once it's been told about them.
ASP.NET Membership
The default system for managing user and is ASP.NET Membership. There's a SqlMembershipProvider that runs against a SQL Server database, but the provider system allows you to plug in your own custom Membership Provider for managing users. Your login process can use your membership provider to validate the user's credentials, and you can use the membership system to store user information if you want. More on that in a bit.
ASP.NET Role Management
For some sites, authorization and authentication are nearly synonymous - the only goal of authorization is to prevent anonymous access. But for many sites, you've got different roles - users, administrators, superadministrators, and omegasupremeadministrators. For those cases, you need something that maps users to roles, and that's what Role Management Providers do. In most cases, roles and membership are managed together - the same system that tracks who your users are controls what rights they have. You'll often see role and membership providers in one big package, but they're two separate things.
Customizing Membership in ASP.NET
As before, the zeroth rule of customizing membership is to reconsider if you need to. There are plenty of hooks into the existing flow, such as overriding OnAuthorization and just adding some information to HttpContext.Items, Session, etc.
If you really do need a custom user management system, the first thing to do is look to see if someone's already written it. There are membership providers for a lot of backing systems. A quick search shows tons of them:
- The Universal Providers (System.Web.Providers) work with all editions of SQL Server 2005 and later as well as SQL Azure. Scott Hanselman covered them in detail: Introducing System.Web.Providers - ASP.NET Universal Providers for Session, Membership, Roles and User Profile on SQL Compact and SQL Azure
- ASP.Net MVC 3 Custom Membership Provider with Repository Injection
- MongoDB Membership
- Amazon SimpleDB Membership
- RavenDB Membership
- Piles of Membership Providers on GitHub and CodePlex
Implementing your own Membership Provider
If you don't find a Membership Provider that works for you, it's not difficult to write your own. Remember that you're taking more responsibility for your system's security. Matt Wrock wrote a great overview: Implementing custom Membership Provider and Role Provider for Authenticating ASP.NET MVC Applications.
There's plenty of information on MSDN as well, for example: Implementing a Membership Provider
Simple Membership
You'll notice that a lot of Matt's overview of creating Membership and Role Providers included a lot of System.NotImplementedException, because the ASP.NET Membership Provider system shows some underlying assumptions around user data going in relational databases. You can just ignore those parts and use what you want, but if you find the whole thing a little too complex and are tempted to throw it all out, I'd recommend looking at SimpleMembership from ASP.NET Web Pages.
Matthew Osborn wrote an overview of what SimpleMembership is and how it works in ASP.NET Web Pages. Although it was originally written for ASP.NET Web Pages, it can be readily adapted to ASP.NET MVC using the SimpleMembership.Mvc NuGet package.
Wrapping Up
ASP.NET MVC gives you a huge amount of flexibility, and it's tempting to want to write a lot of custom code. If you understand the underlying security systems that ASP.NET MVC uses, though, you can usually integrate with what's in place. You'll be saving yourself a lot of unnecessary code, along with the added costs of testing, debugging and maintenance. More important, though, you'll be using a system that's undergone a huge amount of security testing over the past decade.