Setting up authentication in asp.net MVC

I wanted to setup user authentication in asp.net MVC, and I wanted it to tie in with the built-in IIdentity and IPrincipal support that is already in asp.net, but I did not want to have to setup asp.net membership services. I wanted to have my own user objects, and I wanted to save/retrieve them through my user repository just like my other domain objects. All of this was done for a demo, so it is certainly not production quality code, but I hope it helps you.

First off I setup my User table in my database, it looks like this:

User Table

This is obviously just a start, but it is enough to get the job going. I have dragged this database table onto the Linq To Sql designer and it created my User object like so:

UserDb

Next in my UserRepository class I have a method that looks like this:

public User GetUserByUsername(string username)
{
    RecipeZoneDataContext context = ContextFactory.GetRecipeZoneContext();
    return (from u in context.Users where u.UserName == username select u).SingleOrDefault();
}

This way when someone tries to login, I can pass their username into this method and it tries to look that user up. You will notice that we use a "SingleOrDefault" method in order to return a single user or "null" in case there is no user by that name. Next I have created a method on my UserController named "Login" that looks like this:

public void Login()
{
    _viewData.Redirect = Request.QueryString["redirect"];
    RenderView("Login", _viewData);
}

Here we are looking for a query string parameter named "redirect" that is going to contain a url that we will redirect to upon successful login. We assign this to our UserControllerViewData class and then pass that to RenderView. The UserControllerViewData is just a class that I use to hold view data for most of the actions on my UserController class.

My Login view is then rendered:

<asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server">  
    <%= Html.RenderUserControl("~/Views/Shared/ErrorMessages.ascx") %>
    <%using (Html.Form<UserController>(c => c.LoginValidate(RedirectUrl)))
      { %>
    <table>
        <tr>
            <td>
                Username:
            </td>
            <td>
                <%=Html.TextBox("User.UserName")%>
            </td>
        </tr>
        <tr>
            <td>
                Password:
            </td>
            <td>
                <%=Html.Password("User.Password")%>
            </td>
        </tr>
        <tr>
            <td>
            </td>
            <td>
                <%=Html.SubmitButton("login", "Login")%>
            </td>
        </tr>
    </table>
    <% } %>
</asp:Content>

Here we are using a few of our Html helper methods. We use the Html.Form helper method in order to create our html form as well as to render our url that the form will post back to. I am using the overload that takes an Expression and then renders the url based on the controller type and method that you call on it in the expression. You can see that we are posting back to a different action called "LoginValidate" and we are passing the "RedirectUrl" as a parameter to it.

The LoginValidate controller action looks like this:

public void LoginValidate(string redirect)
{
    if (Models.User.ValidateUser(Request.Form["User.UserName"], Request.Form["User.Password"]))
    {
        if (!String.IsNullOrEmpty(redirect) || String.Compare(redirect, "none", true) == 0)
        {
            Response.Redirect(redirect, true);
        }
        else
        {
            RedirectToAction("", "");
        }
    }
 
    _viewData.ErrorMessages.Add("Invalid username or password.");
    RenderView("Login", _viewData);
}

First we pass our UserName and Password to our User.ValidateUser method. If the user is validated then we check for a redirect and if one exists then we redirect. Otherwise we redirect to a default controller and action. Here we have entered no defaults, you would want to fill them in. If we don't succeed then we add an error message to the view data and redirect back to our Login action. There is a bit of data we have to put into the web.config first though:

<authentication mode="Forms">
  <forms name=".APPNAME" protection="All" cookieless="UseCookies" />
</authentication >

This just sets up our application to use forms authentication, and then sets a few parameters on it. After we have this setup we can now call the ValidateUser method. The ValidateUser method looks like this:

public static bool ValidateUser(string username, string password)
{
    var userRepository = new UserRepository();
 
    User user = userRepository.GetUserByUsername(username);
    if (user == null)
    {
        return false;
    }
 
    if (String.Compare(user.Password, password, false) == 0)
    {
        var authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now,
                                                       DateTime.Now.AddMinutes(30), true, "");
 
        string cookieContents = FormsAuthentication.Encrypt(authTicket);
        var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, cookieContents)
                        {
                            Expires = authTicket.Expiration,
                            Path = FormsAuthentication.FormsCookiePath
                        };
        if (HttpContext.Current != null)
        {
            HttpContext.Current.Response.Cookies.Add(cookie);
        }
        return true;
    }
    return false;
}

We pull the user and if it doesn't exist then we immediately return. Then we compare passwords (yes, there is no encryption at this point, like I said, demo) and if they match then we create our authentication ticket. This is part of the forms authentication system and it lets us specify which user is currently logged in. We create a cookie with our data and write it out, then return true. Pretty simple! This saves our username that is logged in, but what about our roles? We need to be able to tell if someone is a normal user or an administrator. In order to do this we have to implement the Application_AuthenticateRequest method in the global.asax file.

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{            
    HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {                
        FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
        var identity = new GenericIdentity(authTicket.Name, "Forms");
        var principal = new CustomPrincipal(identity);                                
        Context.User = principal;
    }
}

Here you see how we are pulling the authentication ticket out of the cookie (if it exists) and we are creating a GenericIdentity object (this is because we don't need any additional functionality for our IIdentity class) and then we create an instance of our CustomPrincipal class which is just a class we have created which implements IPrincipal. This interface has the methods for checking roles. In this application I have just put in some simple code to test for roles since I wanted to keep it simple. This class looks like this:

public class CustomPrincipal : IPrincipal
{        
    public CustomPrincipal(IIdentity identity)
    {
        Identity = identity;
    }
 
    public bool IsInRole(string role)
    {
        if (String.Compare(role, "admin", true) == 0)
        {
            return (Identity.Name == "JustinEtheredge");
        }
        else
        {
            return false;
        }
    }
 
    public IIdentity Identity
    {
        get; private set;
    }
}

As you can see we just check for one role, "admin", and I hardcoded it against my username. I could obviously create a table to manage this, but I have not done so. So, now that we all of this setup, how are we going to use these roles? Well, we are going to create custom ActionFilterAttributes! Of course! I want to be able to protect my controller actions like this:

[UserInRole("Admin")]
public void List(int? page)
{
    _viewData.Users = _userRepository.GetUsers();
    RenderView("List", _viewData);
}

This UserInRoleAttribute will now make sure that you are authenticated and that you are in the "admin" role before it will let you access this controller action. If you aren't authenticated then it will forward you to the login page with a redirect to send you back to where you came from. This attribute looks like this:

public class UserInRoleAttribute : ActionFilterAttribute
{
    private readonly string roleName;
 
    public UserInRoleAttribute(string roleName)
    {
        this.roleName = roleName;
    }
 
    public override void OnActionExecuting(FilterExecutingContext filterContext)
    {
        if (filterContext.HttpContext.User.IsInRole(roleName)) return;
 
        //use reflection until they expose this method
        MethodInfo methodInfo = filterContext.Controller.GetType()
            .GetMethod("RedirectToAction",
                       BindingFlags.ExactBinding |
                       BindingFlags.NonPublic |
                       BindingFlags.Instance, null,
                       new[]
                           {
                               typeof (RouteValueDictionary)
                           }, null);
        methodInfo.Invoke(filterContext.Controller,
                          new object[]
                              {
                                  new RouteValueDictionary(
                                      new
                                          {
                                              controller = "User",
                                              action = "Login",
                                              redirect = filterContext.HttpContext.Request.Url.AbsolutePath
                                          })
                              });
    }
}

This class is actually very simple. The constructor simply takes a roleName and saves it. When the action is called the OnActionExecuting method is called and we simply test the current user to see if they are in the saved role. Otherwise we use some reflection in order to call "RedirectToAction" on the controller class since it is private. Hopefully they will implement this method in the ActionFilterAttribute base class, or they will make it public on the Controller class. But for right now this is the easiest way I could find to call it.

So, what else do we need? Well, we need some way for a user to logout. For this I simply added a Logout action that looks like this:

public void Logout()
{
    FormsAuthentication.SignOut();
    RedirectToAction("List");
}

That is all. You just call SignOut and then redirect to whatever action you want. You could also add a redirect parameter here if you wanted, so you could return to whatever page you clicked "logout" on.

So, there you have it. I'm sure that there is something that I am forgetting, but this post is pretty long and I am tired. :) Please let me know if you see anything that I did wrong or could have done differently! I hope you enjoyed it, and if you liked it, please kick it!

Be Sociable, Share!

14 comments

  1. man, after reading this, i’m glad i’m not using the asp.net mvc.

  2. Thanks for explaining. Neat implementation.

  3. Justin,
    Would it be possible to post the code for your sample?
    Thanks

  4. @Matt I have posted most of the code for this article in its entirety. My sample application is still quite unfinished, but once I am done (or at least closer to being done), I will post my example application to the site for feedback.

  5. any reason why you didn’t use membership services?

    nowadays you can find providers for almost any database out there, and membership as a "platform" is really robust…

    there’s even a asp.net MVC membership starter kit avaiable…

  6. @steve I just felt like writing my own. I could have just as easily used the built in membership stuff, but if I don’t need all of the functionality then I prefer not to use black boxes.

  7. Justin,

    I’ve searched Google for several days and came across your post on asp.net mvc forms authentication. Is the sample app available as mentioned in your earlier comment or has it been
    put on the back-burner ? Are you aware of other
    related posts using an approach similar to yours ? thanks

  8. @Danny That sample application was for something that never really panned out. It was never finished. Virtually all of the source is in this post though, did you have any specific issues?

  9. Justin,

    I may have misunderstood,I thought you were
    developing an entire sample application. With
    ASP.NET MVC not released yet I’ve been looking for complete apps as a means of self-study.

    I looked at the Manning Book but the table of contents didn’t appear it was for a newbie.
    The Apress MVC Preview app was too simple and
    focused more on concepts vs code. The BeerHouse MVC app on CodePlex is based on Preview 2. I suppose I could attempt to migrate it to Preview 5 and attempt to fix what’s broken.

    Thanks, any suggestions are appreciated.

  10. @Danny Yeah, I was originally writing a demo application, but it ended up not being needed. I never found the time to actually complete it. Sorry!

  11. BTW, Danny: there is a "ASP.Net MVC Membership Starter Kit" project on http://www.codeplex.com/MvcMembership. While it is based on ASP.NET membership, it tries to "to provide you with a basic MVC website from which you can build up your own MVC application without having to implement Login/Logout, Registration, and User Administration manually." Speaking of books, I found Maarten Balliauw’s "ASP.NET MVC 1.0 Quickly" attractive. http://blog.maartenballiauw.be/

  12. Hi Justin,

    I am getting null for methodInfo in the line:

    MethodInfo methodInfo = filterContext.Controller.GetType()
    .GetMethod("RedirectToAction", BindingFlags.ExactBinding | BindingFlags.NonPublic |
    BindingFlags.Instance, null,
    new[]
    {
    typeof (RouteValueDictionary)
    }, null);

    Do i need to check anything else for this?

  13. Vlad,

    Thanks for the info about security for MVC. For now , the out-of-the box membership is good for my MVC prototype. As for MVC books both Scott Gu’s
    and Maarten’s provide an excellent basic foundation. Mr. Sanderon’s book is next for the deep DIVE !

  14. Thanks for such an explanation.

Leave a comment