ASP.NET MVC Authentication - Global Authentication and Allow Anonymous
As I was recently updating the Wrox Professional ASP.NET book for ASP.NET MVC 4, I thought about some of the common questions, tip, and tricks I've seen over the past few years, and thought it was time for a... quick blog series! Let's start with Global Authentication.
ASP.NET MVC has had an Account Controller since the ASP.NET MVC 1.0 preview releases; it handles login and registration. That, by itself, just allows users to get logged in - it doesn't do anything to restrict access. That's where the AuthorizeAttribute comes into play. AuthorizeAttribute is a Filter attribute which can be placed on ASP.NET MVC actions or entire controllers to prevent unauthorized access. Attempting to access a restricted controller action when you're not authorized redirects you to login, as I previously described in quite gory detail in a previous post titled Looking at how the ASP.NET MVC Authorize interacts with ASP.NET Forms Authorization.
Reminder: Don't use web.config to restrict access, use [Authorize]
Note: This is old news to MVC veterans, but bears repeating because it continues to be a common question and is really important to get right.
In ASP.NET Web Forms, requests mapped to physical files. There's an <authorization> element in web.config which can be used to restrict file-based access. So this worked well for ASP.NET Web Forms and file-based authorization in general.
But it's a very bad idea to use web.config based authorization in ASP.NET MVC, because URL's map to actions via routing, which can change. You may have multiple routes that map to the same controller action, or you may change routes over time. AuthorizeAttribute was built specifically for this purpose, because you can place your security directly on the resource (the action or controller). Change the routes all you want, the authorization rules go along with the actions.
Another interesting note here is that, since ASP.NET Web Forms has supported routing since ASP.NET 4, you need to pay closer attention to securing routes there as well. K. Scott Allen discussed that in an MSDN Magazine article a few years ago, Routing with ASP.NET Web Forms.
The progression of global authentication in ASP.NET MVC
The AuthorizeAttribute works pretty well, but you have to put it on every controller (or action, if you need to be that granular) that needs to be secured. That's tedious and error prone, and if you forget it, you've opened your site up to anonymous access. In many cases, it's preferable to restrict access to the entire site except for the the Login and Register actions. That's become a little easier with each release, to the point where in ASP.NET MVC 4 I think it's finally about right.
Rick Anderson wrote two comprehensive posts on this - and authorization in ASP.NET MVC in general - in a pair of posts covering security in ASP.NET MVC:
- Securing your ASP.NET MVC 3 Application
- Securing your ASP.NET MVC 4 App and the new AllowAnonymous Attribute
ASP.NET MVC 1 and 2 - Custom Controller Base
In the first post, Rick explains that prior to ASP.NET MVC 3, the recommended approach was to create a custom Controller base class with an [Authorize] attribute applied. This probably sounds more complicated than it is, because it's really easy:
- Right-click the Controllers folder and add a new Controller. Call it something like AuthorizedController.
- Delete the Index action.
- Add the [Authorize] attribute to the class.
- Clean up the Using block at the top if you care about that kind of thing.
End result:
using System.Web.Mvc; namespace InstantMonkeysOnline.Controllers { [Authorize] public class AuthorizedController : Controller { } }
Now you can use this AuthorizedController base class for any other controllers in your application. Changing the HomeController base class to inherit from AuthorizedController will require authorization to view the site home:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace InstantMonkeysOnline.Controllers { public class HomeController : AuthorizedController { public ActionResult Index() { ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application."; return View(); } //More actions here } }
The downside is that you have to remember to do this, so unless you change the T4 templates, you're setting yourself up for an easy mistake down the road.
ASP.NET MVC 3 - Global Action Filters
ASP.NET MVC 3 made it easy to apply an action filter to all actions in your application. If you look in the Global.asax in an new ASP.NET MVC 3 (or later) application, you'll see a RegisterGlobalFilters method.
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); }
To require authorization throughout your application, you could just register AuthorizationAttribute, right?
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new AuthorizeAttribute()); }
Unfortunately, this works a little too well - it's global, so users can't login. But, really, really secure, right?
Rick proposed a custom LoginAuthorizeAttribute which inherited from AuthorizeAttribute but added an exception for the AccountController, but Levi Broderick (an ASP.NET team member who's a whiz with web security) recommended using a filter to whitelist actions which should be available for anonymous access.
using System.Web.Mvc; using MvcGlobalAuthorize.Controllers; namespace MvcGlobalAuthorize.Filters { public sealed class LogonAuthorize : AuthorizeAttribute { public override void OnAuthorization(AuthorizationContext filterContext) { bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true); if (!skipAuthorization) { base.OnAuthorization(filterContext); } } } }
So now you'd register LogonAuthorize as a global filter, and to allow access to an action, you'll need that AllowAnonymousAttribute. Since it's just a marker attribute, there's no actual code - just attribute usage settings.
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AllowAnonymousAttribute : Attribute { }
ASP.NET MVC 4 and the AllowAnonymous attribute
The good news is that this is even easier in ASP.NET MVC 4, because it's baked in. By baked in, I mean that:
- There's a built-in AllowAnonymousAttribute in the the System.Web.Mvc namespace which whitelists actions for anonymous access
- The AuthorizeAttribute filter has built-in logic that allows access to actions decorated with the AllowAnonymousAttribute
That means that you can just register the AuthorizeAttribute as a global filter and put the AllowAnonymousAttribute on any actions that should be public.
If you use the AllowAnonymousAttribute without registering AuthorizeAttribute there's no effect. For that reason, the default AccountController in a new ASP.NET MVC 4 project has [AllowAnonymous] on all the actions that should always be public:
[Authorize] public class AccountController : Controller { [AllowAnonymous] public ActionResult Login() [AllowAnonymous] [HttpPost] public JsonResult JsonLogin(LoginModel model, string returnUrl) [AllowAnonymous] [HttpPost] public ActionResult Login(LoginModel model, string returnUrl) public ActionResult LogOff() [AllowAnonymous] public ActionResult Register() [AllowAnonymous] [HttpPost] public ActionResult JsonRegister(RegisterModel model) [AllowAnonymous] [HttpPost] public ActionResult Register(RegisterModel model) public ActionResult ChangePassword() [HttpPost] public ActionResult ChangePassword(ChangePasswordModel model) public ActionResult ChangePasswordSuccess() }
That works because, as mentioned above, the AuthorizeAttribute has the following logic (as you can see from the code on CodePlex):
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined( typeof(AllowAnonymousAttribute), inherit: true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined( typeof(AllowAnonymousAttribute), inherit: true); if (skipAuthorization) { return; }
Note: I actually started looking into this in detail because I was wondering how the AllowAnonymousAttribute was able to bypass the Authorization check, and was surprised to see that it didn't have any code at all. I wasn't alone here, I noticed David Hayden looked into this as well.
If you're interested in more information on ASP.NET MVC security and the history of the AnonymousAttribute, I highly recommend Rick's posts listed above. He goes into a lot more detail - with some excellent input from Levi - on what not to do (e.g. put security logic into routing) and further considerations like requiring HTTPS globally.