Thursday, March 06, 2008

Forms Authentication with the MVC Framework

I'm just wrapping up writing my first commercial application with the new MVC Framework. Because it sits on top of ASP.NET, simply replacing the Web Forms model, you can use all of the ASP.NET infrastructure. This includes Forms Authentication. I want to show how I used Forms Authentication with my own database rather than using the membership API, but you can plug in membership quite easily too.

The first thing you need to do is set up your Web.config file to use Forms authentication:

<!--Using forms authentication-->
<authentication mode="Forms">
  <forms loginUrl="/login.mvc/index" defaultUrl="/home.mvc/index" />
</authentication>

Just set mode to Forms and the login URL to your login page. This means that any unauthenticated users are redirected to the login page. I've also set the default URL to my home page. The other change you'll need to make to your web.config file is to allow unauthenticated users to see your login page and any CSS and image files (although this depends on how you configure IIS). If you've got public areas of your web site you will need to add these as well.

  <!-- we don't want to stop anyone seeing the css and images -->
  <location path="Content">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>
  <!-- allow any user to see the login controller -->
  <location path="login.mvc">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
  </location>

Next we need to set up the login controller. Since I'm using my own data access I need to pass in my user repository; that's the class that wraps my user data access.

public class LoginController : Controller
{
    readonly IUserRepository userRepository;

    public LoginController(IUserRepository userRepository)
    {
        this.userRepository = userRepository;
    }
}

We need a controller action method to render the login form. I've called mine Index. The LoginViewData simply allows me to include an error message, we'll see how that works later on.

[ControllerAction]
public void Index()
{
    RenderView("index", new LoginViewData());
}

Next we need to create the view for the login form. I've just rendered a simple form.

<%= Html.ErrorBox(ViewData.ErrorMessage) %>

<% using(Html.Form("Authenticate", "Login")) { %>
<div id="login_form">

    <p>Enter your details</p>
    
    <label for="username">User name</label>
    <%= Html.TextBox("username") %>
    
    <label for="password">Password</label>
    <%= Html.Password("password") %>

    <%= Html.SubmitButton() %>
</div>
<% } %>

Note that the form posts to the Authenticate action. This action is where we do all the work of checking the user's credentials and logging them in.

[ControllerAction]
public void Authenticate(string username, string password)
{
    User user = userRepository.GetUserByName(username);

    if (user != null && user.Password == password)
    {
        SetAuthenticationCookie(username);
        RedirectToAction("Index", "Home");
    }
    else
    {
        // If we got here then something is wrong with the supplied username/password
        RenderView("Index", new LoginViewData
            {
                ErrorMessage = "Invalid User Name or Password."
            });
    }
}

public virtual void SetAuthenticationCookie(string username)
{
    FormsAuthentication.SetAuthCookie(username, false);
}

Yes, I know, it's very naughty storing passwords unencrypted in the database, but I wanted to keep this demonstration simple. That's my excuse anyway :) You can see that we simply get the user with the given username from the database (my user repository) , make sure the passwords match and call the public virtual method SetAuthenticationCookie which calls the forms authentication API's SetAuthCookie method. This effectively logs the user in. Why is SetAuthenticationCookie a separate method, why not just call SetAuthCookie in the Authenticate action? This is so that we can unit test the Login controller without invoking the forms authentication API. We can create a partial mock of the Login controller and set up an expectation for SetAuthenticationCookie because it's marked public and virtual.

This is all you need to do to make forms authentication work with the MVC Framework. However, there was one additional thing I wanted to achieve. I have my own User class that implements IPrinciple:

public partial class User : IPrincipal
{
    public static User Guest
    {
        get
        {
            return new User() { Username = "Guest", Role = Role.Guest };
        }
    }

    #region IPrincipal Members

    public IIdentity Identity
    {
        get 
        {
            bool isAuthenticated = !(Role.Name == Role.Guest.Name);
            return new Identity(isAuthenticated, this.Username);
        }
    }

    public bool IsInRole(string role)
    {
        return this.Role.Name == role;
    }

    #endregion
}

This User class was generated by the LINQ to SQL designer and the above code is simply a partial extension of it. My data model also includes Roles so I can supply an implementation of IsInRole method. The identity property is handled by returning a simple implementation of IIdentity. To be able to make use of my IPrinciple implementation I need to supply my User to the HttpContext on each request and the best way of doing this is to handle the OnAuthenticateRequest event in Global.asax.

protected void Application_OnAuthenticateRequest(Object sender, EventArgs e)
{
    if (Context.User != null)
    {
        if (Context.User.Identity.IsAuthenticated)
        {
            User user = userRepository.GetUserByName(Context.User.Identity.Name);

            if (user == null)
            {
                throw new ApplicationException("Context.User.Identity.Name is not a recognised user.");
            }

            System.Threading.Thread.CurrentPrincipal = Context.User = user;
            return;
        }
    }
    System.Threading.Thread.CurrentPrincipal = Context.User = CreateGuestUser();
}

We get HttpContext's user, check if it's been authenticated. If it has, it means this user has already logged in and is one of the users in our database. We retrieve the user from our user repository and set the HttpContext's User to our user. We also set the current thread's currentPrinciple to our user. If the user is not authenticated we create a guest user and use that instead.

Now that the current thread's currentPrinciple is one of our users, we can do role based checks on any bit of code we want. Here, for example we're making sure that only administrators can execute this DeleteSomethingImportant action:

[ControllerAction]
[PrincipalPermission(SecurityAction.Demand, Role = "Administrator")]
public void DeleteSomethingImportant(int id)
{
 ....
}

You can access the current user at any time in the application simply by casting the HttpContext's User to your User. This makes role based menu's or features trivial to write.

Update

I had a question recently (hi Jasper) about the Identity class returned by the Identity property of my User class. This is a simple implementation of the System.Security.Principle.IIdentity interface that just returns the Username of my User class. Here it is in full:

public class Identity : IIdentity
{
    bool isAuthenticated;
    string name;

    public Identity(bool isAuthenticated, string name)
    {
        this.isAuthenticated = isAuthenticated;
        this.name = name;
    }

    #region IIdentity Members

    public string AuthenticationType
    {
        get { return "Forms"; }
    }

    public bool IsAuthenticated
    {
        get { return isAuthenticated; }
    }

    public string Name
    {
        get { return name; }
    }

    #endregion
}

Hopefully this makes things a little clearer.

23 comments:

Unknown said...

Nice blog regarding form authentication. Thanks.

Anonymous said...

Great stuff. I am trying to use unit test with my Controllers. The tests have no HTTPContext (I'd have to use System.Threading.Thread.CurrentPrincipal to get user info) - how would your code have to be modified to take that into account?

Thanks.

Mike Hadlow said...

Hi Anonymous, I have a controller base class 'ControllerBase' with a public virtual property 'LoggedInUser', which simply returns the HttpContext.User. I use Rhino Mocks for my tests and always create the controller as a PartialMock. In the expectations I set up an expectation for a call to LoggedInUser and get it to return whatever user I need in my tests.

I hope this is useful. Let me know if it makes no sense and I'll try and dig out some code.

Anonymous said...

That does make sense. I believe (please correct me if wrong) that HttpContext.User = Thread.CurrentPrincipal. I also believe that HttpContext.User is always null in the case of unit tests because the web server isn't actually fired.

Can you throw up some sample code showing your ControllerBase as well as how you have implemented a user logon framework for MVC?

Thanks.

Anonymous said...

Cool. Just in time.
Maybe you can write a custom HTMLHelper method (e.g. LoginForm) and spread it in the Internet?

Mike Hadlow said...

Hi Godofcsharp,

Great name! I guess one could create an HtmlHelper extension for a login form, but you'd have to provide the controller too. It kinda goes against the 'you have control over your HTML' philosophy of MVC rather than the drag-and-drop theme of Web Forms.

What do you think?

Anonymous said...

Omg thank you for this, u saved me a heap of time.

Anonymous said...

Is there any security concern by only using the username from the authentication cookie to set the active user?

Has it been hacked?

Forgive my ignorance, but your code looks great and I want to make sure everything is thought out 100%.

Thanks.

Mike Hadlow said...

Hi Anonymous, Setting the authentication cookie based on the username isn't my idea, that's part of the ASP.NET forms authentication infrastructure. I believe it to be secure, but I'd be very interested if you know otherwise.

You also might want to check out the most recent MVC Framework code from Preview 5. When you create a new MVC Framework project, much of the authorisation code is created for you.

Anonymous said...

Thanks Mike,

I am working on an active project from an earlier preview. The assemblies have been upgraded, but i never tried a creating new project.

I appreciate it.

Unknown said...

This looks great, but how do you get the reference to userRepository into the MvcApplication class where Application_OnAuthenticateRequest lives? I tried to pass it in as a constructor argument and hoped that my IoC container would do all the work, but it wouldn't compile without an argumentless constructor. Any ideas?

Mike Hadlow said...

Hi Brooks,

I usually set my Application class up as an IContainerAccessor, so I can always get a reference to the container. Check out the suteki shop code:

http://code.google.com/p/sutekishop/source/browse/trunk/Suteki.Shop/Suteki.Shop/Global.asax.cs

Unknown said...

Hey Mike - Thanks for the quick response!

I noticed that you decided to move this code to an action filter for SutekiShop, which seems like a better idea since it won't get executed for unnecessary requests (images, CSS, and controllers that don't require any kind of authorization). Is that the main reason you did that or is there another advantage to doing it that way?

PS: Thanks for linking me to the SutekiShop source. I'm an MVC beginner, and it looks like a great resource.

Mike Hadlow said...

Hi Brooks, Actually is was Jeremy Skinner who came up with the Action Filter idea. I really like it.

Glad you like Suteki Shop. Let me know if you have any comments/feedback.

Patrick said...

I have spent a few hours searching for a good resource and I finally found it:) You are an ace mate thanks, great work

Gaz said...

Just found this article and its exactly what I've been searching for. I'm looking at the best way to handle my authentication and have seen various ways including the use of base controller classes overriding either initialize or OnAuthorization or by creating a custom attribute filter. The final option I'd seen was what you have above on the AuthenticateRequest in the Global.asax but my worry was that this code would be executed too much (every image, css file). I notice from your replies that you've moved this into an attribute. Do you think this is the best place for implementing custom authentication as I've seen a number of posts questing this due to output caching problems?

Thanks
Gaz

emzero said...

Great article!
Could you provide a demo project with the source code showing how it works?
It would be great

Mike Hadlow said...

Hi Emo,

Check out Suteki Shop:
http://code.google.com/p/sutekishop/

Anonymous said...

For some reason it will not work with Visual Studio development server! See this post http://forums.asp.net/t/1469217.aspx
I have the same problem.

Anonymous said...

Just an FYI, every time you do an authenticate request, you are hitting your repository for user info. That is every image, css, and javascript being requested makes a call to the DB.

Denis Bednov said...

Hi, Mike. I did all you wrote, but I have an error:

Type is not resolved for member 'eShop.Models.User,eShop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.

What's wrong?

I'am using ASP.NET Development server, which distributed with VS2010

Sathish said...

Hi, I have requirement to authenticate actions based on roles from database. How we can achieve this.

Assil said...

Hi Mike:
You said
System.Threading.Thread.CurrentPrincipal = Context.User = user;


How do you guarantee that all the resquests from that user are using the same thread??

Thanks
A.A.