in·dom·i·ta·ble
adj.
Incapable of being overcome, subdued, or vanquished; unconquerable.
6th
FEB
Forms Authentication in Asp.Net MVC, Part II
Posted by indomitablehef | Filed under Asp.Net MVC
This is part 2 of 4
Forms Authentication in Asp.Net MVC
Continuing on from where we left off in part I, we now need to implement the code that actually does the Authentication against whatever user store we have in place. The default MVC project uses the Membership provider for Asp.Net, and that works just fine. I need more control than that, though, so let’s take a look at the next steps.
Last time, I mentioned the MembershipController and the Login action. Once the Login view has been rendered, it submits to the Authenticate action, also on the MembershipController.
The Login View:
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %> <%@ Import Namespace="MyProject.Web.Controllers" %> <asp:Content ID="content" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <h1> Login</h1> <% if( ViewData["ErrorMessage"] != null ){ %> <p> <% =ViewData["ErrorMessage"] %></p> <% } %> <% using( Html.BeginForm("Authenticate", "Membership" )){ %> <fieldset> <legend>Login</legend> <div> <label for="userName"> User Name:</label> <% =Html.TextBox( "userName" ) %></div> <div> <label for="password"> Password:</label> <% =Html.Password( "password" ) %></div> <div> <label for="rememberMe"> Remember Me:</label> <input type="checkbox" id="rememberMe" name="rememberMe" checked="checked" value="checked" /></div> <div> <% =Html.SubmitButton() %></div> <% =Html.Hidden( "returnUrl", "/" ) %> </fieldset> <% } %> </asp:Content>
And the login action on the MembershipController
1 2 3 4 5 6 7 8 | public class MembershipController: Controller { public ActionResult Login() { return View("Login"); } } |
Now we need an Authenticate action, for when the Login form is submitted
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public ActionResult Authenticate(string userName, string password, string rememberMe, string returnUrl) { string validationMessage = ""; if (membershipProvider.ValidateUser(userName, password, out validationMessage)) { authenticationProvider.SetAuthCookie(userName, (rememberMe != null)); return new RedirectResult(returnUrl); } else { ViewData["ErrorMessage"] = validationMessage; return View("Login"); } } |
Notice some things here. We’re using the authenticationProvider we started with in part I, and a new membershipProvider. We’ve also added another method to our AuthenticationProvider, SetAuthCookie. This simply wraps the FormsAuthentication.SetAuthCookie method.
So, now our IAuthenticationProvider looks like this:
1 2 3 4 5 6 | public interface IAuthenticationProvider { bool IsAuthenticated(ControllerContext context); void RedirectToLogin(ControllerContext context); void SetAuthCookie(string userName, bool createPersistentCookie); } |
Our FormsAuthenticationProvider implementation looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class FormsAuthenticationProvider: IAuthenticationProvider { public virtual bool IsAuthenticated(ControllerContext context) { return context.HttpContext.User.Identity.IsAuthenticated; } public virtual void RedirectToLogin(ControllerContext context) { context.HttpContext.Response.Redirect(FormsAuthentication.LoginUrl + string.Format("?ReturnUrl={0}", context.HttpContext.Request.Url.AbsolutePath), true); } public virtual void SetAuthCookie(string userName, bool createPersistentCookie) { FormsAuthentication.SetAuthCookie(userName, createPersistentCookie); } } |
And we need to provide the Authentication and Membership provider to our MembershipController, so we add them in the constructors. The whole thing, with the constructors, Login Action and Authenticate action looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class MembershipController: Controller { private readonly IAuthenticationProvider authenticationProvider; private readonly IMembershipProvider membershipProvider; public MembershipController(IAuthenticationProvider authenticationProvider, IMembershipProvider membershipProvider) { this.authenticationProvider = authenticationProvider; this.membershipProvider = membershipProvider; } public MembershipController():this(ServiceLocator.Current.GetInstance<IAuthenticationProvider>(), ServiceLocator.Current.GetInstance<IMembershipProvider>()) {} public ActionResult Login() { return View("Login"); } public ActionResult Authenticate(string userName, string password, string rememberMe, string returnUrl) { string validationMessage = ""; if (membershipProvider.ValidateUser(userName, password, out validationMessage)) { authenticationProvider.SetAuthCookie(userName, (rememberMe != null)); return new RedirectResult(returnUrl); } else { ViewData["ErrorMessage"] = validationMessage; return View("Login"); } } } |
Now we can write some good tests for our MembershipController, mocking the MembershipProvider and the AuthenticationProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | [TestFixture] public class MembershipControllerTests { private MembershipController controller; private IMembershipProvider mockedMembershipProvider; private IAuthenticationProvider mockedAuthenticationProvider; [SetUp] public void SetUp() { mockedMembershipProvider = MockRepository.GenerateMock<IMembershipProvider>(); mockedAuthenticationProvider = MockRepository.GenerateMock<IAuthenticationProvider>(); controller = new MembershipController(mockedAuthenticationProvider, mockedMembershipProvider); } [Test] public void CanInitUserLogin() { ViewResult result = controller.Login().AssertViewRendered(); } [Test] public void CanAuthenticate() { //arrange string validationMessage = ""; mockedMembershipProvider.Expect(x => x.ValidateUser("", "", out validationMessage)).IgnoreArguments().Return(true); mockedAuthenticationProvider.Expect(x => x.SetAuthCookie("", false)).IgnoreArguments(); //act, assert RedirectResult redirectResult = controller.Authenticate("username", "password", "rememberme", "/").AssertHttpRedirect(); mockedMembershipProvider.VerifyAllExpectations(); mockedAuthenticationProvider.VerifyAllExpectations(); } [Test] public void CanRedirectToLoginWhenNotAuthenticated() { //arrange var controller = new MembershipController(mockedAuthenticationProvider, mockedMembershipProvider); string validationMessage = ""; //when validate user returns false mockedMembershipProvider.Expect(x => x.ValidateUser("", "", out validationMessage)).IgnoreArguments().Return(false); //act, assert ViewResult viewResult = controller.Authenticate("username", "password", "rememberme", "/").AssertViewRendered(); Assert.IsNotNull(controller.ViewData["ErrorMessage"]); mockedAuthenticationProvider.AssertWasNotCalled(x => x.SetAuthCookie("username", true)); mockedMembershipProvider.VerifyAllExpectations(); } } |
Notice the AssertViewRendered and AssertHttpRedirect extension methods. These are part of the MvcContrib TestHelper library.
Finally, we’ll take a quick look at our IMembershipProvider interface
1 2 3 4 | public interface IMembershipProvider { bool ValidateUser(string userName, string password, out string validationMessage); } |
…but I’ll need to leave the implementation of that for part III, coming soon, which can be found here.
Reader's Comments
Leave a Reply
Post Meta
-
February 6, 2009 -
Asp.Net MVC -
2 Comments
-
Comments Feed -
Del.ico.us
-
Digg This
Great series - this has been a huge help.
At this point, do the MembershipController tests pass? The previous RequiresAuthenticationAttribute tests worked fine for me, but I couldn’t get this set to run until I altered the setup to create the controller using the mocked providers. If I leave it with the default constructor, it fails, with complaints that
Microsoft.Practices.ServiceLocation.ActivationException : Activation error occured while trying to get instance of type IAuthenticationProvider, key “”
—-> Castle.MicroKernel.ComponentNotFoundException : No component for supporting the service IAuthenticationProvider was found.
I didn’t really follow why IAuthenticationProvider would cause problems, as that just worked fine with RequiresAuthenticationAttribute.
‘d appreciate any insight you can offer. Again, thanks for this series!
Best,
Dug
Thanks! It’s definitely been the most popular set of posts I’ve ever written.
You’re right, there’s something missing at this point. In order for the default constructor for MembershipController to get the needed components from
You would have to have added those types to the Windsor container and set the CommonServiceLocator’s provider to that container, from part IV:
As you’ll see in part IV, you can then use the ASP.Net MVC ControllerFactory and pass it your Windsor container. Once you’ve done that, you can completely remove the default constructor for MemberController, and all calls to ServiceLocator.Current.GetInstance, and let ASP.Net MVC and Windsor just wire up all the dependencies you need when the controller is created.
So, at this point the code wouldn’t pass, without the Windsor container/ServiceLocator setup found in part IV. I’ve updated the code in this post’s MembershipControllerTest class to always use the mocked IMembershipProvider and IAuthenticationProvider.
Hope this helps!