MVC Controllers and Forms Authentication

A couple of ActionResult implementations to abstract out FormsAuthentication specifics from Login/Logout controller actions... thoughts?

I've been re-implementing portions of my site (projects.nikhilk.net) using ASP.NET MVC. One of the things I had to implement was login/logout functionality.

When you create a new ASP.NET MVC Application, you get a sample AccountController that has Login/Logout actions. This controller depends on an IFormsAuthentication implementation. There is a default implementation of this interface within the sample that works against the underlying System.Web.Security.FormsAuthentication APIs (SetAuthCookie and SignOut). A mock implementation of this interface can be supplied to the controller for purposes of unit testing.

However, when it came time to implement Login/Logout in my own controller, I felt odd calling into FormsAuthentication (even through an interface) directly from my actions. I felt like the action should simply do the job of validating credentials, and then return an appropriate ActionResult that then took care of generating the response: in this case, setting or clearing the cookie, and redirecting to the appropriate URL. So I created FormsLoginResult and FormsLogoutResult. These are fairly obvious and simple, but I thought I'd go ahead and share anyway.

First I'll show the updated Login and Logout methods on AccountController:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Login(string username, string password, bool rememberMe) {
    ...
    if (ViewData.ModelState.IsValid) {
        // Attempt to login
        bool loginSuccessful = Provider.ValidateUser(username, password);
        if (loginSuccessful) {
            return new FormsLoginResult(username, rememberMe);
        }
        else {
            ...
        }
    }

    // If we got this far, something failed, redisplay form
    ...
    return View();
}

public ActionResult Logout() {
    return new FormsLogoutResult();
}

The Login action now no longer needs to be coupled to the fact that authentication is tracked via a cookie, or where it should redirect to (the default home page or an url specified by a returnUrl query string parameter, or that it should even need to do a redirect in the first place). Same goes for the Logout action.

Next is the unit test for this functionality. I am using Moq for implementing a mock MembershipProvider for this test that is still required, but no longer need a mock IFormsAuthentication implementation.

[TestClass]
public class AccountControllerTest {

    [TestMethod]
    public void LoginSuccessful() {
        string testUserName = "TestUser";
        string testPassword = "TestPassword";

        Mock<MembershipProvider> mockMembership = new Mock<MembershipProvider>();
        mockMembership.Expect<bool>(m => m.ValidateUser(testUserName, testPassword))
           .Returns(true).AtMostOnce().Verifiable();

        AccountController controller = new AccountController(mockMembership.Object);
        ActionResult result = controller.Login(testUserName, testPassword, false);

        Assert.IsInstanceOfType(result, typeof(FormsLoginResult));

        FormsLoginResult loginResult = (FormsLoginResult)result;
        Assert.AreEqual<string>(loginResult.UserName, testUserName);
        Assert.AreEqual<bool>(loginResult.PersistentCookie, false);
    }

    [TestMethod]
    public void LoginFailure() {
        string testUserName = "TestUser";
        string testPassword = "TestPassword";

        Mock<MembershipProvider> mockMembership = new Mock<MembershipProvider>();
        mockMembership.Expect<bool>(m => m.ValidateUser(testUserName, testPassword))
           .Returns(true).AtMostOnce().Verifiable();

        AccountController controller = new AccountController(mockMembership.Object);
        ActionResult result = controller.Login(testUserName, "badPassword", false);

        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }
}

Here is the code for the FormsLoginResult and FormsLogoutResult. These use the extensibility provided by MVC to build custom action results, which is pretty sweet in my opinion.

public class FormsLoginResult : ActionResult {
    private string _userName;
    private string _userData;
    private bool _persistentCookie;

    public FormsLoginResult(string userName)
        : this(userName, /* persistentCookie */ false) {
    }

    public FormsLoginResult(string userName, bool persistentCookie) {
        if (String.IsNullOrEmpty(userName)) {
            throw new ArgumentNullException("userName");
        }
        _userName = userName;
        _persistentCookie = persistentCookie;
    }

    public bool PersistentCookie {
        get { return _persistentCookie; }
    }

    public string UserData {
        get { return _userData; }
        set { _userData = value; }
    }

    public string UserName {
        get { return _userName; }
    }

    public override void ExecuteResult(ControllerContext context) {
        HttpResponseBase response = context.HttpContext.Response;

        if (String.IsNullOrEmpty(_userData)) {
            FormsAuthentication.SetAuthCookie(_userName, _persistentCookie);
        }
        else {
            FormsAuthenticationTicket ticket =
                new FormsAuthenticationTicket(1, _userName,
                                              DateTime.Now, DateTime.Now.AddMinutes(30),
                                              _persistentCookie,
                                              _userData, 
                                              FormsAuthentication.FormsCookiePath);
            string encryptedTicket = FormsAuthentication.Encrypt(ticket);

            HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName,
                                               encryptedTicket);
            cookie.HttpOnly = true;
            cookie.Secure = FormsAuthentication.RequireSSL;
            cookie.Path = FormsAuthentication.FormsCookiePath;
            if (FormsAuthentication.CookieDomain != null) {
                cookie.Domain = FormsAuthentication.CookieDomain;
            }

            response.Cookies.Add(cookie);
        }

        response.Redirect(FormsAuthentication.GetRedirectUrl(_userName, _persistentCookie));
    }
}

As a bonus, one thing that FormsLoginResult also shows is packaging some custom user data as part of the FormsAuth cookie. This user data can be retrieved by handling the Authenticate event of HttpApplication and using FormsAuthentication.Decrypt to convert the AuthCookie back into a FormsAuthenticationTicket instance, and fetching the UserData out of the ticket. I use this on my site to track some meta-information in order to recreate the IPrincipal instance upon subsequent requests.

The second action result, FormsLogoutResult is pretty straightforward.

public class FormsLogoutResult : ActionResult {
    private string _url;

    public FormsLogoutResult()
        : this(FormsAuthentication.DefaultUrl) {
    }

    public FormsLogoutResult(string url) {
        if (String.IsNullOrEmpty(url)) {
            throw new ArgumentNullException("url");
        }
        _url = url;
    }

    public string Url {
        get { return _url; }
    }

    public override void ExecuteResult(ControllerContext context) {
        FormsAuthentication.SignOut();
        context.HttpContext.Response.Redirect(_url);
    }
}

Theres certainly more than one way to do things in an MVC app - which suggests there should be some guidelines. It seems with a combination of action filters, action parameters and action results, a controller action itself may have few reasons to work against request/response objects directly. Thoughts? I am curious if there are any comments or thoughts on that last statement - one of the reason for this post.


[ Tags: | | ]
Posted on Thursday, 12/18/2008 @ 5:38 PM | #ASP.NET


Comments

14 comments have been posted.

Jonathan Carter

Posted on 12/18/2008 @ 10:34 PM
Great post Nikhil. I'm largely of the opinion that a controller should never interact with Http[Context | Response | Request] directly. As you said, there are much better (and elegant) ways to get around needing to access them within a controller action.

Jahedur Rahman

Posted on 12/18/2008 @ 10:34 PM
Hmm, Nice.

Andrew Davey

Posted on 12/19/2008 @ 3:29 AM
You can reduce coupling to HttpContext for input as well.
I've got a quick blog post that shows using a Mobel Binder to inject values into parameters from, for example, an HttpCookie.

http://www.aboutcode.net/2008/12/08/Model+Binders+For+HttpCookie+And+AppSetting.aspx

Morgan Cheng

Posted on 12/19/2008 @ 6:44 AM
Nice Post!
This approach is really Seperation-of-Concerns.

Dave R.

Posted on 12/19/2008 @ 8:48 AM
I think you raise a serious point here - the need for recommended best practices from Microsoft so newbies like me can pick up MVC without being concerned that we're writing our code incorrectly or introducing dependencies where they can be avoided.

Vadim

Posted on 12/19/2008 @ 12:33 PM
These should be imported into the main branch!

Nikhil Kothari

Posted on 12/19/2008 @ 1:05 PM
@Andrew - Yes, I totally forgot to mention model binders. They're of course key to staying away from HTTP-specific input mechanisms at the controller level.

@Dave - I couldn't agree more - its time to start building a set of guidelines.

@Vadim - I'll pass this along to the team here... though I suspect this falls out of the v1 radar at this point.

Vadim

Posted on 12/20/2008 @ 11:01 AM
Maybe it's possible to bring them into the Futures assembly?

798

Posted on 3/9/2009 @ 3:58 PM
Welcome to (www.warestrade.com) to wholesale and retail!We are specializing in sale of a series of brand products, such as Jardon Nike Adidas LV Gucci Prada Coach Fendi Chanel D&G,etc.Products in clude shoes clothes bags jewelry crafts electronic and so on.

jdlaj

Posted on 3/9/2009 @ 4:02 PM
Welcome to (www.warestrade.com) to wholesale and retail!We are specializing in sale of a series of brand products, such as Jardon Nike Adidas LV Gucci Prada Coach Fendi Chanel D&G,etc.Products in clude shoes clothes bags jewelry crafts electronic and so on.We can give you the most prefertial prices.Thanks!

jdlaj

Posted on 3/9/2009 @ 4:02 PM
Welcome to (www.warestrade.com) to wholesale and retail!We are specializing in sale of a series of brand products, such as Jardon Nike Adidas LV Gucci Prada Coach Fendi Chanel D&G,etc.Products in clude shoes clothes bags jewelry crafts electronic and so on.We can give you the most prefertial prices.Thanks!

77777777

Posted on 3/19/2009 @ 1:57 AM
777777777777777

Maku

Posted on 3/23/2009 @ 12:10 AM
Hello.We are chinese wholesalers.Our company is devoted to selling small wares. We are specializing in sale of a series of brand products, such as LV Gucci Prada Coach Fendi Chanel D&G etc.Products include shoes handbags purse jewelry and so on.We wholesale and retail jordan shoes,gucci shoes,nike shoes,puma shoes,adidas shoes,lacoste shoes,sport shoes,sneakers,coach shoes,prada chose,DG shoes.lv shoes,Burberry shoes,paul smith shoes,wholesale and retail lv handbags,gucci handbags,coach handbags,fendi handbags,juicy handbags,chanel handbags, wholesale and retail lv boots,gucci boots,fendi boots,prada boots,UGG boots,wholesale cheap lacoste shirts,polo shirts,AF shirts,DG shirts,EDshirts.wholesale and retail lv coach slippers,gucci sandals,chanel sandals.wholesale juicy jeans,juicy handbags,juicy shirts,ugg handbags and so on.All the goods we purchase directly from manufacturers,so the price is very low.You can just go to my webstire www.warestrade.com to choose any you want.We also can make products as you ask for.If you will,just give us the sample,We make them for you.Welcome to www.warestrade.com to wholesale and retail!Thanks!
Best regards!

Khaja Minhajuddin

Posted on 4/27/2009 @ 6:51 AM
Very neat article, This has really helped me come up with a clear solution for my MVC project. Wouldn't it be better if, instead of using the ActionResult to do the login and logout stuff a service is used. Let me know what your thoughts are on this.
P.S: You'll have to do something about the spam in the comments :)
Post your comment and continue the discussion.