Securing ASP.NET MVC

Beyond the basics of the AccountController class

ASP.NET MVC introduces many significant changes to the Web programming paradigm, but doesn’t change much of the runtime environment that backs up the application. To a large extent, Web Forms applications and ASP.NET MVC applications share the same runtime environment—only configured in a slightly different way. This is to say that security and account control in ASP.NET MVC cannot be something significantly different from ASP.NET Web Forms. Many developers—quite reasonably from my perspective—begin their development from the standard template that the ASP.NET MVC Visual Studio tooling provides. The standard ASP.NET MVC template supplies a number of classes that altogether provide effective forms-based authentication. The ASP.NET MVC hello-world application is already capable of distinguishing between anonymous and logged users and provides the scaffolding for registering new users and for basic account functionalities such as change of password. Can you ask for more?

If you look at it from a functional and highly pragmatic perspective, 80 percent of the work you need 80 percent of the time is pretty much done. However, the code you get from the Visual Studio template, although effective, should be merged with the rest of your code. As a result, you might need to take some classes and interfaces out of the native classes and move them to distinct assemblies. In addition, you might need to extend the default IPrincipal with custom data and manage roles and membership in a personalized way, while providing users with a great authentication experience and preserving testability.

In this article, I’ll discuss how I would rewrite the AccountController class, and related view model classes, to make them fit comfortably in a multi-layer solution that provides extra capabilities. I’ll focus on log-in and log-out only.

Dissecting the Original Classes

The AccountController class you get from the template includes a few actions such as Logon, Logoff, Register, and ChangePassword. Most of the time, you need all of them if you're building an authentication layer on top of your pages. You might consider splitting the original account controller into two distinct controllers to separate logon/logoff operations from password management. The approach you take is up to you and depends on your definition of granularity in software.

The controller class(es) you end up with need some injection from the outside world. Specifically, you must implement a mechanism for injecting dependencies on membership and authentication services. The membership service will offer your controller methods a way to check whether a given user is bound to a registered account and plays a known role. The authentication service is expected to perform any actions required to create and store a security token that carries credentials.

The default template offers two built-in interfaces for this purpose: IMembershipService and IFormsAuthenticationService. They work most of the time, but are open to extensions. Figure 1 shows a slightly customized version where the return value of ValidateUser is an application-specific type instead of a plain Boolean value.

This code lives in the AccountModels.cs class placed under the Models folder in the default template. I suggest you create an ad hoc YourApp.Contracts assembly and isolate there all public abstractions of services. Next, you might want to create another assembly with some concrete implementation of these services.

The default implementation of membership and authentication services is fairly good. In a real-world application, though, you probably have your own membership and role providers to validate supplied credentials. You register your providers in the web.config file and derive them from system-provided base classes such as MembershipProvider and RoleProvider. This is in no way different from what you would do in a similar situation in Web Forms. Figure 2 shows how to register custom membership and role providers.

Once you’ve registered a valid membership provider (plus optionally a role provider), the code from the template works fine; only registered users are enabled to join the application's protected areas. By using the IsInRole method on the Principal object in the HTTP context you can also check roles and decide about the user interface to display and functions to enable.

In addition to interfaces, the AccountModels.cs file contains a few view model classes, custom data annotation attributes, and utilities. Personally, I would move utilities and attributes to a separate, application-wide assembly and move view model classes to distinct files. In this article, I focus on logon/logoff so I expect to have a LogonViewModel class, some quick validation utilities to check empty strings, and perhaps data annotation attributes. After this split-up is done, you no longer need an AccountModels.cs file in your project.

Personalized Login Response

In ASP.NET MVC, the canonical login form is an editor bound to a view model class. Data annotations and client script code will contribute to make it easy to maintain and effective to end users. How many reasons do you have in the application for a login to fail? Certainly, a login attempt might fail if a user provides incorrect user name and/or password. Perhaps you have other reasons for a login to fail. For example, the account might have been locked down by the administrator for some reason or the system might detect business-specific reasons to deny a login.

The method in the membership provider that decides the success or failure of the login attempt is ValidateUser. Here’s a typical implementation of the method:

public override Boolean ValidateUser(String username, String password)
\\{
   // Access your Users database table and return a Boolean answer
   return _userRepository.ValidateCredentials(username, password);
\\}

Based on the Boolean response, you then arrange an error message for the user. Clearly, a True/False alternative doesn't help create a really helpful message. The message has to be generic and support a few possible causes—unknown user name, invalid password, or some business-specific message. To support more advanced scenarios, you need to add one more member to the custom MembershipProvider class, as shown in Figure 3.

I added the GrantAccess method that receives credentials and returns a custom data structure, incorporating detailed information about the login attempt. Here’s a possible implementation of the login response class:

public class YourLoginResponse
\\{
    public Boolean Success \\{ get; set; \\}
    public String ErrorMessage \\{ get; set; \\}
\\}

As Figure 1 shows, the membership service interface directly exposed to ASP.NET MVC controllers already knows about the custom login response object. Let’s look at how to arrange a login form.

The Login Form

The login form is essentially an HTML form that posts to the LogOn method on the Account controller. At a minimum, the user interface includes a couple of text boxes, an optional checkbox for remembering users on next access, and a submit button. Figure 4 summarizes the markup you need.

All plain strings have been replaced with constants and resources. In addition, the view is strongly typed in accordance with common best practices of ASP.NET MVC development. The view model is the class shown below with members decorated with data annotation attributes:

public class LoginViewModel
\\{
    \\[Required(ErrorMessageResourceName = "UserNameRequired", 
              ErrorMessageResourceType = typeof(AppResources))\\]
    public String UserName \\{ get; set; \\}

    \\[Required(ErrorMessageResourceName = "PasswordRequired",
              ErrorMessageResourceType = typeof(AppResources))\\]
    public String Password \\{ get; set; \\}

    public Boolean RememberMe \\{ get; set; \\}
\\}

The EnableClientValidation call on top of the form enables client-side scripting against data annotations typical actions. In the most common situations, data annotations are field specific and limited to intercept most common mistake,s such as leaving a required field blank or entering patently wrong and incorrect data. The two validation messages at the bottom of Figure 4 exist only to display client-side messages. Instead, the validation message that refers to LoginError captures the response of server-side validation as performed by the controller via the membership service. Figure 5 shows the code used to post a login request.

The server-side validation layer analyzes the request and determines if it's OK to process it. If not, it stored meaningful information about what was wrong in the response. If the response is successful, the authentication service is invoked to create the authentication token; if not, the error message is added to the TempData collection and the user is redirected to the login page—in the example, the login form is hosted in the home page.

The controller method enabled to process a GET request for the login page will check the TempData collection first; if any content is found, the controller method will merge the content into the ViewData collection:

public ActionResult Index()
\\{
   // Anything to take care of available in TempData?
   var temp = TempData\\[TempDataEntries.ModelState\\] as ModelStateDictionary;
   if (temp != null)
   \\{
      ViewData.ModelState.Merge(temp); 
   \\}

   // Create the view model object and render the view
   :
\\}

The Authentication Token

Once the credentials have been validated, the authentication service proceeds with the creation of a security token—typically the authentication cookie:

FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);

By default, the cookie contains the user name and is named after settings in the configuration file. The duration is subject to information in the configuration file and the Boolean parameter you pass in. No aspects of this code, though, refer to practices that should be new to ASP.NET Web Forms developers.

Most of the time you’ll leverage the services of the FormsAuthentication module, which offers a SetAuthCookie method to sign in and a SignOut method to log off. The same API also offers lower-level functions to create a custom ticket and store there any additional information you may need.

Once logged in, the user is represented by an object associated with the User property on the HttpContext class. The type of User is IPrincipal; if you use a role provider, though, it’ll be the type of the RoleProvider principal and automatically includes role information. You programmatically determine roles in the implementation of the custom role provider by overriding the following method:

public override String\\[\\] GetRolesForUser(String username) \\{...\\}

This is not the only method you have to override, but it's the key one for associating a list of roles with a user name.

Going Beyond the Default

Because ASP.NET MVC and ASP.NET Web Forms share the same runtime environment, any tasks that relate to implementing security features (i.e., authentication) are based on the same set of primitives. Typically, in ASP.NET MVC you don’t rely on the Login control, but you use an editor for a data-annotated type and the TempData collection to refresh the user interface in a context-sensitive manner. In ASP.NET MVC, you also might want to use abstractions for services providing membership and authentication capabilities. Having abstractions ready also positions you very well for future enhancements to your site's security layer. To upgrade, say, to Windows Identity Foundation (WIF) you don’t need to do much more than provide a class that implements the contract for the authentication service.

This example was based on the default template for ASP.NET MVC applications and, therefore, heavily oriented to forms authentication. It won’t take much, though, to make membership work with a more general authentication service. Or why not have membership checks incorporated in the authentication service? In summary, the default ASP.NET MVC template gives you a lot of what you need most of the time; but not everything you need.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish