The FormsAuthenticationTicket class is used in ASP .NET to provide forms authentication to identify users through an encrypted cookie. The ticket also provides a UserData property which can be used to store additional encrypted data. I have created the FormsAuthenticationCookie class which represents a collection of keys and values that are serialized into binary, compressed and encoded into the UserData property.
How it Works
To create an instance of FormsAuthenticationCookie for an authenticated user, the FormsAuthenticationCookie.Current static property attempts to locate a cookie from the current response or from the user's identity:
/// <summary> /// Creates a new instance of the FormsAuthenticationCookie for an authenticated user or null if the user is not authenticated. /// </summary> public static FormsAuthenticationCookie Current { get { //get the current context: HttpContext context = HttpContext.Current; FormsAuthenticationTicket ticket; //check if cookie exists in response without creating it if (context.Response.Cookies.AllKeys.Contains(FormsAuthentication.FormsCookieName)) { //if existing authentication cookie in response, get the cookie and decrypt the FormsAuthenticationTicket HttpCookie existing = context.Response.Cookies[FormsAuthentication.FormsCookieName]; ticket = FormsAuthentication.Decrypt(existing.Value); } //otherwise, if the user is authenticated: else if (context.User != null && context.User.Identity.IsAuthenticated && context.User.Identity is FormsIdentity) //gets the FormsAuthenticationTicket from the user identity: ticket = ((FormsIdentity)context.User.Identity).Ticket; else //not authenticated return null; //return new FormsAuthenticationCookie return new FormsAuthenticationCookie(ticket); } }
User data is read by converting from base-64 encoding to binary, decompressing and separating data into keys and values:
//convert base=64 encoded data to binary byte[] binary = Convert.FromBase64String(userdata); //output stream is used to read uncompressed data MemoryStream output = null; try { if (binary[0] == 1) //if compression flag { //using compression CompressUserData = true; //create stream to compressed data using (MemoryStream compressed = new MemoryStream(binary)) { //skip initial compression flag byte compressed.Seek(1, SeekOrigin.Current); //create deflate stream to decompress data: using (DeflateStream deflate = new DeflateStream(compressed, CompressionMode.Decompress, true)) { //create output stream output = new MemoryStream(); //decompress data to output deflate.CopyTo(output); //seek to beginning of stream output.Seek(0, SeekOrigin.Begin); } } } else { //no compression CompressUserData = false; //create output stream output = new MemoryStream(binary); //skip initial compression flag byte output.Seek(1, SeekOrigin.Current); } using (BinaryReader reader = new BinaryReader(output, Encoding.UTF8, true)) { do { string key = reader.ReadString(); DataType type = (DataType)reader.ReadByte(); object value; switch (type)//get the property data type { case DataType.Bool: //read Boolean: value = reader.ReadBoolean(); break; case DataType.Int32: //read integer: value = reader.ReadInt32(); break; case DataType.Int64: //read long: value = reader.ReadInt64(); break; case DataType.String: //read string: value = reader.ReadString(); break; case DataType.Char: //read char value = reader.ReadChar(); break; case DataType.DateTime: //read long and convert to date time: value = DateTime.FromBinary(reader.ReadInt64()); break; default: throw new NotSupportedException("Type not supported."); } //add data: _UserData.Add(key, value); } while (output.Position < output.Length); //read until end of stream } } finally { if (output != null) output.Dispose(); }
To save user data, the process is reversed. Keys and values are saved to binary, compressed and then converted to a base-64 encoded string:
string encodedUserData; //stream to write binary data to: using (MemoryStream stream = new MemoryStream()) { //if this stream will not be compressed if (!CompressUserData) //write no compression flag stream.WriteByte(0); //write all the values in the user data dictionary using (BinaryWriter writer = new BinaryWriter(stream, Encoding.UTF8, true)) { //write each key and value foreach (KeyValuePair<string, object> item in _UserData) { //write the item key writer.Write(item.Key); //write the item value _WriteObjectSwitch.Switch(item.Value.GetType(), writer, item.Value); } } //if stream should be compressed if (CompressUserData) { //seek to the beginning stream.Seek(0, SeekOrigin.Begin); //create a stream to output compressed data to using (MemoryStream output = new MemoryStream()) { //write the compression flag output.WriteByte(1); //compress stream using deflate algorithm using (DeflateStream deflate = new DeflateStream(output, CompressionLevel.Optimal, true)) stream.CopyTo(deflate); //convert binary to base-64 encoded string encodedUserData = Convert.ToBase64String(output.ToArray()); } } else //convert binary to base-64 encoded string encodedUserData = Convert.ToBase64String(stream.ToArray()); }
When the cookie is saved, a new FormsAuthenticationTicket is created using the original ticket or FormsAuthentication values:
DateTime issueDate, expirationDate; int version; string cookiePath; if (_Ticket == null) { //ticket issue date issueDate = DateTime.UtcNow; //ticket expiration date expirationDate = issueDate.Add(FormsAuthentication.Timeout); //use version 2, default FormsAuthenticationTicket.Version version = 2; //ticket cookie path cookiePath = FormsAuthentication.FormsCookiePath; } else //otherwise, use original values { //get ticket issue date issueDate = _Ticket.IssueDate; //get ticket expiration date expirationDate = _Ticket.Expiration; //get ticket version version = _Ticket.Version; //get ticket cookie path cookiePath = _Ticket.CookiePath; } //create a new ticket: FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(version, _User, issueDate, expirationDate, _IsPersistent, encodedUserData, cookiePath); //create a new cookie HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName); //encrypt ticket cookie.Value = FormsAuthentication.Encrypt(ticket); //set expiration cookie.Expires = ticket.Expiration; //set ssl requirements: cookie.Secure = FormsAuthentication.RequireSSL; //no script access: cookie.HttpOnly = true; //get current http context HttpContext context = HttpContext.Current; //if no ticket exists and cookie is not set in response if (_Ticket == null && !context.Response.Cookies.AllKeys.Contains(FormsAuthentication.FormsCookieName)) //add the cookie context.Response.Cookies.Add(cookie); else //otherwise //set the cookie context.Response.Cookies.Set(cookie); //set cookie
Data can be get or set by using the indexer or the generic Get/Set properties:
/// <summary> /// Gets or sets the element with the specified key. /// </summary> /// <param name="key">The key of the element to get or set.</param> /// <returns>The element with the specified key.</returns> public object this[string key] { get { object obj; if (_UserData.TryGetValue(key, out obj)) return obj; else return null; } set { ThrowIfInvalidKey(key); if (value == null) _UserData.Remove(key); else _SetTypeSwitch.Switch(value.GetType(), key, _UserData, value); } } /// <summary> /// Gets the value with the specified key. /// </summary> /// <typeparam name="T">The type of element to get.</typeparam> /// <param name="key">The key of the element to get.</param> /// <returns>The value associated with the specified key.</returns> public T Get<T>(string key) { return (T)this[key]; } /// <summary> /// Sets the specified key and value in the cookie. /// </summary> /// <typeparam name="T">The type of element to set.</typeparam> /// <param name="key">The key of the element to set.</param> /// <param name="value">The value of the element to set.</param> public void Set<T>(string key, T value) { this[key] = value; }
Static ActionTypeSwitch instances are created and populated with acceptable types and delegates to write objects to binary or set the key's value in the static constructor:
/// <summary> /// Defines a type switch to set user data. /// </summary> private static readonly ActionTypeSwitch<string, Dictionary<string, object>, object> _SetTypeSwitch = new ActionTypeSwitch<string, Dictionary<string, object>, object>(); /// <summary> /// Defines a type switch to write data. /// </summary> private static readonly ActionTypeSwitch<BinaryWriter, object> _WriteObjectSwitch = new ActionTypeSwitch<BinaryWriter, object>(); /// <summary> /// Initializes static type switches. /// </summary> static FormsAuthenticationCookie() { #region Writing Object Data //case DataType: _WriteObjectSwitch.Add(typeof(DataType), (type, writer, value) => { //write type of data: writer.Write((byte)value); }); //case bool: _WriteObjectSwitch.Add(typeof(bool), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.Bool); //write Boolean: writer.Write((bool)value); }); //case Int32: _WriteObjectSwitch.Add(typeof(Int32), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.Int32); //write integer: writer.Write((Int32)value); }); //case Int64: _WriteObjectSwitch.Add(typeof(Int64), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.Int64); //write long: writer.Write((Int64)value); }); //case char: _WriteObjectSwitch.Add(typeof(char), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.Char); //write char: writer.Write((char)value); }); //case DateTime: _WriteObjectSwitch.Add(typeof(DateTime), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.DateTime); //write serialized DateTime: writer.Write(((DateTime)value).ToBinary()); }); //case string: _WriteObjectSwitch.Add(typeof(string), (type, writer, value) => { //write DataType: _WriteObjectSwitch.Switch(typeof(DataType), writer, DataType.String); //write string: writer.Write((string)value); }); //default: _WriteObjectSwitch.Default = (type, builder, value) => { throw new NotSupportedException("The type '" + type.FullName + "' is not supported."); }; #endregion #region Set Dictionary Value //defines a method that sets a dictionary value Action<Type, string, Dictionary<string, object>, object> setValue = (Type type, string key, Dictionary<string, object> userData, object value) => { userData[key] = value; }; //add cases for each type of acceptable data: _SetTypeSwitch.Add(typeof(int), setValue); _SetTypeSwitch.Add(typeof(Int64), setValue); _SetTypeSwitch.Add(typeof(bool), setValue); _SetTypeSwitch.Add(typeof(char), setValue); _SetTypeSwitch.Add(typeof(DateTime), setValue); _SetTypeSwitch.Add(typeof(string), setValue); //default to handle invalid object types: _SetTypeSwitch.Default = (Type type, string key, Dictionary<string, object> userData, object value) => { throw new NotSupportedException("The type '" + type.FullName + "' is not supported."); }; #endregion }
Using
In the Login action of an Mvc 4 project after the WebSecurity.Login method is called to create the FormsAuthenticationTicket, the FormsAuthenticationCookie can be used to include user data in the ticket:
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe)) { //creates FormsAuthenticationCookie from an existing ticket: FormsAuthenticationCookie auth = FormsAuthenticationCookie.Current; //add user data: auth["Key1"] = "Value1"; auth["Key2"] = 1000; auth["Key3"] = 1000L; auth["Key4"] = DateTime.Now; auth["Key5"] = true; auth["Key6"] = 'A'; //saves cookie to response: auth.Save(); return RedirectToLocal(returnUrl); } // If we got this far, something failed, redisplay form ModelState.AddModelError("", "The user name or password provided is incorrect."); return View(model); }
To access user data in an authorized action, use FormsAuthenticationCookie.Current and get the value through the indexer.
Remarks
You should limit the amount of data stored in the FormsAuthenticationCookie to ensure the final size of the FormsAuthenticationTicket.UserData property does not result in an invalid cookie.