Storing Custom Data in Forms Authentication Tickets

This article looks at storing custom data in asp.net forms authentication tickets. I recently updated the article to make the custom model binder generic, and add the necessary registration code which was missing from the first draft.

So you’ve decided to use FormsAuthentication, and perhaps enhanced it with your own custom providers. In your AccountController Login method you probably have a call along these lines:

FormsAuthentication.SetAuthCookie(account.Id.ToString(), model.RememberMe);

That all works great, but what if you need to store some extra data in the cookie. Perhaps the name you are passing into the AuthTicket isn’t actually the users name, but a GUID. Suddenly that built in ASP.Net login widget, in the top right of the page, doesn’t seem so great when it looks like this:

Hello 5D1D4743-9941-40B5-8931-6BC12617946C

What we need to do is store some extra data in that AuthTicket cookie right? That way we can keep the GUID as the authentication id, but still store things like the users first name in the cookie. Thus saving an expensive round trip to the db each time we render the widget.

Hmmm… whats this ‘UserData’ property we see on the AuthTicket? Perfect!

Erk… It’s read only?!?!?!

At least that’s how my thought process went.

So we need to make an authentication ticket ourselves:

var ticket = FormsAuthenticationTicket(int version, string name, DateTime issueDate,
	DateTime expiration, bool isPersistent, string userData, string cookiePath);

Unfortunately that’s quite a few more parameters than SetAuthCookie(…) required and they should be coming from the web.config rather than hard-coded.

On the plus side, there is access to the UserData!

To avoid losing the web.config driven settings, we can do a little trick and get FormsAuthentication to do the parsing for us. All we need to do is ask it for an AuthTicket and copy the settings from that into a new one we create.

To do this, a few steps are required. Firstly, after getting the ticket, we have to decrypt it, copy the data into a new ticket, and then make sure we encrypt that. Then we need to add it to the response.

Now before getting to the code, we should think about where it should live. It would seem logical to encapsulate this an extension method on FormsAuthentication, but being a static class we can’t. Instead we can attach it to HttpResponseBase which is not a bad home, especially as we have to add the cookie onto a response anyway. I’d recommend creating the following class in an ‘Infrastructure’ folder in your project:

	public static class HttpResponseBaseExtensions
	{
		public static int SetAuthCookie<T>(this HttpResponseBase responseBase, string name, bool rememberMe, T userData)
		{
			/// In order to pickup the settings from config, we create a default cookie and use its values to create a 
			/// new one.
			var cookie = FormsAuthentication.GetAuthCookie(name, rememberMe);
			var ticket = FormsAuthentication.Decrypt(cookie.Value);
			
			var newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration,
				ticket.IsPersistent, userData.ToJson(), ticket.CookiePath);
			var encTicket = FormsAuthentication.Encrypt(newTicket);

			/// Use existing cookie. Could create new one but would have to copy settings over...
			cookie.Value = encTicket;

			responseBase.Cookies.Add(cookie);

			return encTicket.Length;
		}
	}

There are a couple of things of note here, firstly we are accepting a generic type for the UserData, and secondly we are encoding it to Json!

Why? well lets think about the UserData field. Being on a cookie, this can only contain string data. Now we could do our own custom serialisation into this string, but my preference is to use JSON as it’s designed for the task. In this instance I’m using the serialiser from MongoDb as I happen to be using that in my project, but any Json serialiser will do. You might like to try the ServiceStack implementation for example.

I’m also returning the size of the cookie – cookies should never be longer than 4000 bytes as some browsers will just discard them. Its worth keeping an eye on this as it’s not just the size of your UserData but the other mandatory parts of the cookie too.

So let’s get this wired into our AccountController.

First we define a UserData class with a FirstName in it:

	public class UserData
	{
		public string FirstName { get; set; }

		public UserData()
		{
			FirstName = "Unknown";
		}
	}

Now here’s an example Login Action. There are some extras in here around validation, but you can use whatever approach here that fits your project.

[HttpPost]
		public ActionResult LogIn(AccountLoginVM model, string returnUrl)
		{
			try
			{
				if (ModelState.IsValid)
				{
					// Some code to validate and check authentication
					if (!Membership.ValidateUser(model.Email, model.Password))
						throw new RulesException("Incorrect username or password");

					Account account = _accounts.GetByEmail(model.Email);

					UserData userData = new UserData
					{
						FirstName = account.FirstName
					};

					Response.SetAuthCookie(account.Id.ToString(),
						model.RememberMe, userData);
				
					if (Url.IsLocalUrl(returnUrl))
					{
						return Redirect(returnUrl);
					}
					else
					{
						return RedirectToAction("Index", "Home");
					}
				}
			}
			catch (RulesException ex)
			{
				ex.CopyTo(ModelState);
			}

			model.Password = "";
			return View(model);
		}

That’s it. We’ve now got a cookie with our extra UserData in it.

Hang on… what about fixing that login widget in the top right?

One elegant way to crack this is to create a custom model binder, then if we swap the example widget from being a partial view to a partial action, all we need to do is demand a UserData object as an input param and the magic of binding will save us.

So, the custom model binder, again leveraging the MongoDb Json deserialiser:

	/// <summary>
	/// Binder to pull the UserData out for any actions that may want it.
	/// </summary>
	public class UserDataModelBinder<T> : IModelBinder
	{
		public object BindModel(ControllerContext controllerContext,
			ModelBindingContext bindingContext)
		{
			if (bindingContext.Model != null)
				throw new InvalidOperationException("Cannot update instances");
			if (controllerContext.RequestContext.HttpContext.Request.IsAuthenticated)
			{
				var cookie = controllerContext
					.RequestContext
					.HttpContext
					.Request
					.Cookies[FormsAuthentication.FormsCookieName];

				if (null == cookie)
					return null;

				var decrypted = FormsAuthentication.Decrypt(cookie.Value);

				if (!string.IsNullOrEmpty(decrypted.UserData))
					return BsonSerializer.Deserialize<T>(decrypted.UserData);
			}
			return null;
		}
	}

This is a generic so you can use whatever class suits to store the userdata. This then needs to be registered in Application_Start() in ‘Global.asax.cs’ :

ModelBinders.Binders.Add(typeof(UserData), new UserDataModelBinder<UserData>());

Now our login widget action, which passes a UserData object into our view (wrapped in a view model as we may not always want to pass all the UserData into the view).

		public ActionResult LoginWidget(UserData userData)
		{
			AccountLoginWidgetVM model = new AccountLoginWidgetVM();
			if (null != userData)
				model.UserData = userData;

			return PartialView(userData);
		}
@model TestProj.Web.Models.AccountLoginWidgetVM
         
@if(Request.IsAuthenticated) {
    <text>Welcome <b>@Model.UserData.FirstName</b>!
    [ @Html.ActionLink("Logout", "Logout", "Account") ]</text>
}
else {
...
}

We’ve covered quite a broad range of topics here, but hopefully its clear and of use. If you need any clarification leave a comment.

Next time… a change of tack. I’m going to look at how to get some performance out of a devexpress WPF grid.

9 thoughts on “Storing Custom Data in Forms Authentication Tickets

  1. Christoph Müller

    Hey thank you for you post. This is what I was looking for.
    But I got some problems to implement it in my project.
    This is what I did (and you didn’t mentioned explicitly)
    1. I registered the model-binder in application_start like this
    ModelBinders.Binders[typeof(UserData)] = new UserDataModelBinder();
    2. I created a LoginModel
    public class AccountLoginWidgetVM
    {
    public UserData UserData { set; get; }
    }
    3. I created a view” LoginWidget” with the View code you created
    4. In my layout file I called the action like this
    @Html.RenderAction(“LoginWidget”);

    So now when I start the application I get the error
    Compiler Error CS1502: The best overloaded method match for ‘ System.Web.WebPages.WebPageExecutingBase.Write(System.Web.WebPages.HelperResult)’ has some invalid arguments

    Do I miss anything?

    Thanks

    Reply
    1. Dan Harman Post author

      ah – I see I left off the binder registration. You need to do the following (will fix article too):

      ModelBinders.Binders.Add(typeof(UserData), new UserDataModelBinder());

      Not quite sure whether that is the cause of your error?

      I have also made a change to the binder to make it generic rather than bound to the specific UserData class.

      Reply
  2. shawn

    I’m having what is undoubtedly a stupid issue signing out a user logged in with a custom ticket created this way. Right now I’m grabbing it out of Request.Cookies, setting its expiration in the past, and putting it back in the Response, calling FormsAuthentication.SignOut, and then a RedirectToRoute call. I’ve tried a bunch of variations on this with no success, including creating a new HttpCookie, etc. What are you doing? I’ve searched fairly extensively and I think I’ve covered the obvious gotchas, but there must be something dumb I’m overlooking.

    Reply
  3. voss

    Do you have a sample working project?
    I can’t get this to work at all.

    Is this correct? Seems like it should be returning the model, but either way still doesn’t look like it works.
    return PartialView(userData);

    I get javascript errors and the partical view isn’t working for me either.

    Reply
  4. Jamie

    pretty fantastic article actually, worked like a dream!!

    also didn’t want to use the MongoDb stuff so this line worked for me

    …return Json.Decode(decrypted.UserData);

    also Christoph Müller , I had same issue and used this

    @{Html.RenderAction(“LoginWidget”);}

    Reply
  5. Lelala

    What about storing more sensitive information in the cookie?
    So far we’ve used sessionState, whats wrong this it? (Apart from the memory consumption the sessionState has…)

    Reply
    1. Dan Harman Post author

      You need to be very careful storing more sensitive data in a cookie. e.g. SSL is a requirement and it needs to be encrypted. Session state is fine if it works for you. Obviously it tends to imply large data transfers with each page fetch which won’t be great if you have mobile clients, lots of users or limited bandwidth. However if its working for you at the moment, then don’t feel obliged to throw it away because its technically suboptimal. If it ain’t broken…

      Reply
  6. Saphiroth

    Hello Dan!
    Nice Work!

    Ummm… but I followed your steps to a point on “fixing that login widget ” and then I was “killed” by exceptions of:
    {“Non-static method requires a target.”}

    on everywhere I go. Including Linq expressions even.
    Then I looked into that stuff, and I found once I put
    ModelBinders.Binders.Add(typeof(Users), new Infrastructure.UserDataModelBinder());
    under
    protected void Application_Start()
    {
    in Global.asax.cs, the exception starting to pop up. Once I comment it out, everything is fine, however I could not get the information stored in the cookie :(.

    So could I get a help?

    My Email is cxismostii@gmail.com

    Thank you!

    Reply
    1. Dan Post author

      Not really sure whats going on there, although this tutorial is quite old now so things may have changed. All I can do is wish you good luck.

      Reply

Leave a Reply