Showing posts with label Forms Authentication. Show all posts
Showing posts with label Forms Authentication. Show all posts

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.