The first, simplest step in securing an application is requiring that a user be logged in to access specific URLs within the application. You can do that using the Authorize action filter on either a controller or on specific actions within a controller. The AuthorizeAttribute is the default authorization filter included with ASP.NET MVC. Use it to restrict access to an action method. Applying this attribute to a controller is shorthand for applying it to every action method within the controller.
Without any parameters, the Authorize attribute just requires that the user is logged in to the site in any capacity — in other words, it just forbids anonymous access. You look at that first, and then look at restricting access to specific roles.
Let's assume that you've naively started on your music store application with a very simple shopping scenario: a StoreController with two actions: Index (which displays the list of albums) and Buy:
using System.Collections.Generic; using System.Linq; using System.Web.Mvc; using Wrox.ProMvc4.Security.Authorize.Models; namespace Wrox.ProMvc4.Security.Authorize.Controllers { public class StoreController : Controller { public ActionResult Index() { var albums = GetAlbums(); return View(albums); } public ActionResult Buy(int id) { var album = GetAlbums().Single(a => a.AlbumId == id); //Charge the user and ship the album!!! return View(album); } // A simple music catalog private static List<Album> GetAlbums() { var albums = new List<Album>{ new Album { AlbumId = 1, Title = "The Fall of Math", Price = 8.99M}, new Album { AlbumId = 2, Title = "The Blue Notebooks", Price = 8.99M}, new Album { AlbumId = 3, Title = "Lost in Translation", Price = 9.99M }, new Album { AlbumId = 4, Title = "Permutation", Price = 10.99M }, }; return albums; } } }
However, you're obviously not done, because the current controller would allow a user to buy an album anonymously. You need to know who the users are when they buy the album. You can resolve this by adding the AuthorizeAttribute to the Buy action, like this:
[Authorize] public ActionResult Buy(int id) { var album = GetAlbums().Single(a => a.AlbumId == id); //Charge the user and ship the album!!! return View(album); }
To see this code, use NuGet to install the Wrox.ProMvc4.Security.Authorize package into a default ASP.NET MVC project, as follows:
Install-Package Wrox.ProMvc4.Security.Authorize
Run the application and browse to /Store. You'll see a list of albums, and you haven't had to log in or register at this point, as shown in Figure 7.1.
When you click the Buy link, however, you are required to log in (see Figure 7.2).
Since you don't have an account yet, you'll need to click the Register link, which displays a standard account signup page (see Figure 7.3).
When you click the Buy button after registering, the authorization check passes and you're shown the purchase confirmation page, as shown in Figure 7.4. (Of course, a real application would also collect some additional information during the checkout, as demonstrated in the MVC Music Store application.)
<location path=”Admin” allowOverride=”false”> <system.web> <authorization> <allow roles=”Administrator” /> <deny users=”?” /> </authorization> </system.web> </location>
What's going on behind the scenes here? Clearly, we didn't write any code (controllers or views) to handle the Log On and Register URLs, so where did it come from? The ASP.NET MVC Internet Application template includes an AccountController that implements support for accounts managed by both ASP.NET Membership and OAuth authentication.
The AuthorizeAttribute is a filter, which means that it can execute before the associated controller action. The AuthorizeAttribute performs its main work in the OnAuthorization method, which is a standard method defined in the IAuthorizationFilter interface. Checking the MVC source code, you can see that the underlying security check is looking at the underlying authentication information held by the ASP.NET context:
IPrincipal user = httpContext.User; if (!user.Identity.IsAuthenticated) { return false; }
If the user fails authentication, an HttpUnauthorizedResult action result is returned, which produces an HTTP 401 (Unauthorized) status code. This 401 status code is intercepted by the FormsAuthenticationModule OnLeave method, which instead redirects to the application login page defined in the application's web.config, as shown here:
<authentication mode="Forms"> <forms loginUrl="∼/Account/LogOn" timeout="2880" /> </authentication>
This redirection address includes a return URL, so after completing login successfully, the Account / LogOn action redirects to the originally requested page.
The Intranet Application template (available in ASP.NET MVC 3 Tools Update and later) is very similar to the Internet Application template, with one exception: It replaces Forms Authentication with Windows Authentication.
Because Registration and Log On with Windows Authentication are handled outside of the web application, this template doesn't require the AccountController or the associated models and views. To configure Windows Authentication, this template includes the following line in web.config:
<authentication mode="Windows" />
This template also includes a readme.txt file with the following instructions on how to configure Windows Authentication in both IIS and IIS Express. In order to use the Intranet template, you'll need to enable Windows authentication and disable Anonymous authentication.
The preceding scenario demonstrated a single controller with the AuthorizeAttribute applied to specific controller actions. After some time, you realize that the browsing, shopping cart, and checkout portions of your website each deserve separate controllers. Several actions are associated with both the anonymous Shopping Cart (view cart, add item to cart, remove from cart) and the authenticated Checkout (add address and payment information, complete checkout). Requiring Authorization on Checkout lets you transparently handle the transition from Shopping Cart (anonymous) to Checkout (registration required) in the Music Store scenario. You accomplish this by putting the AuthorizeAttribute on the CheckoutController, like this:
[Authorize] public class CheckoutController : Controller
This says that all actions in the CheckoutController will allow any registered user, but will not allow anonymous access.
For many sites, nearly the entire application should require authorization. In this case, it's simpler to require authorization by default and make exceptions in the few places where anonymous access is allowed — such as the site's home page and URLs required for the login process. For this case, it's a good idea to configure the AuthorizeAttribute as a global filter and allow anonymous access to specific controllers or methods using the AllowAnonymous attribute.
To register the AuthorizeAttribute as a global filter, add it to the global filters collection in RegisterGlobalFilters:
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new System.Web.Mvc.AuthorizeAttribute()); filters.Add(new HandleErrorAttribute()); }
This will apply the AuthorizeAttribute to all controller actions in the application. The obvious problem this presents is that it restricts access to the entire site, including the AccountController. Prior to MVC 4, if you wanted to use a global filter to require authorization, you had to do something special to allow anonymous access to the AccountController. A common technique was to subclass the AuthorizeAttribute and include some extra logic to selectively allow access to specific actions. MVC 4 adds a new AllowAnonymous attribute. You can place the AuthorizeAttribute on any methods (or entire controllers) to opt out of authorization as desired.
For an example, you can see the default AccountController in a new MVC 4 application using the Internet Application template. All methods that would require external access if the AuthorizeAttribute were registered as a global filter are decorated with the AllowAnonymous attribute. For example, the Login HTTP Get action appears, as follows:
// // GET: /Account/Login [AllowAnonymous] public ActionResult Login() { return ContextDependentView(); }
This way, even if you register the AuthorizeAttribute as a global filter, users will be able to access the login actions.