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.
Nice blog regarding form authentication. Thanks.
ReplyDeleteGreat 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?
ReplyDeleteThanks.
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.
ReplyDeleteI hope this is useful. Let me know if it makes no sense and I'll try and dig out some code.
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.
ReplyDeleteCan you throw up some sample code showing your ControllerBase as well as how you have implemented a user logon framework for MVC?
Thanks.
Cool. Just in time.
ReplyDeleteMaybe you can write a custom HTMLHelper method (e.g. LoginForm) and spread it in the Internet?
Hi Godofcsharp,
ReplyDeleteGreat 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?
Omg thank you for this, u saved me a heap of time.
ReplyDeleteIs there any security concern by only using the username from the authentication cookie to set the active user?
ReplyDeleteHas it been hacked?
Forgive my ignorance, but your code looks great and I want to make sure everything is thought out 100%.
Thanks.
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.
ReplyDeleteYou 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.
Thanks Mike,
ReplyDeleteI 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.
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?
ReplyDeleteHi Brooks,
ReplyDeleteI 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
Hey Mike - Thanks for the quick response!
ReplyDeleteI 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.
Hi Brooks, Actually is was Jeremy Skinner who came up with the Action Filter idea. I really like it.
ReplyDeleteGlad you like Suteki Shop. Let me know if you have any comments/feedback.
I have spent a few hours searching for a good resource and I finally found it:) You are an ace mate thanks, great work
ReplyDeleteJust 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?
ReplyDeleteThanks
Gaz
Great article!
ReplyDeleteCould you provide a demo project with the source code showing how it works?
It would be great
Hi Emo,
ReplyDeleteCheck out Suteki Shop:
http://code.google.com/p/sutekishop/
For some reason it will not work with Visual Studio development server! See this post http://forums.asp.net/t/1469217.aspx
ReplyDeleteI have the same problem.
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.
ReplyDeleteHi, Mike. I did all you wrote, but I have an error:
ReplyDeleteType 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
Hi, I have requirement to authenticate actions based on roles from database. How we can achieve this.
ReplyDeleteHi Mike:
ReplyDeleteYou 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.