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.
Posted on Thursday, 12/18/2008 @ 5:38 PM
| #
ASP.NET