Redesigning my authentication strategy with ASP.NET

I am currently using my own authentication code for my site, which is built on .NET. I did not accept the standard Forms Auth route, since all the examples that I could find were tightly integrated with WebForms, which I do not use. For all purposes and tasks, I have all the static HTML, and any logic is executed through Javascript and web service calls. Things like logging in, logging out and creating a new account are done without even leaving the page.

Here's how it works now: in the database, I have a User ID , a Security ID and a Session ID . All three are UUIDs, and the first two never change. Each time a user logs in, I check the user table for a row that matches this username and hashed password, and I update the Session ID to a new UUID. Then I create a cookie, which is a serialized representation of all three UUIDs. For any calls to secure web services, I will deserialize this cookie to make sure that the user table has a row with these three UUIDs. This is a fairly simple system and works well, but I don’t really like the fact that a user can log in with only one client at a time. This will cause problems when creating mobile and tablet applications and already creates problems if they have several computers or web browsers. For this reason, I am going to throw away this system and move on to something new. Since I wrote this many years ago, I believe there may be something much more recommended.

I read in the FormsAuthentication class in the .NET Framework that processes cookies, and runs as an HttpModule to validate each request. I am wondering if I can take advantage of this in my new design.

It seems that cookies have no status, and sessions do not need to be tracked in the database. This is because cookies are encrypted using a private key on the server, which can also be shared in a cluster of web servers. If I do something like:

 FormsAuthentication.SetAuthCookie("Bob", true); 

Then, in later requests, I can be sure that Bob is indeed a valid user, as a cookie will be very difficult if not impossible to fake.

Would it make sense to use the FormsAuthentication class to replace my current authentication model? Instead of a Session ID column in the database, I relied on encrypted cookies to represent valid sessions.

Is there a third-party / open source .NET authentication framework that might work better for my architecture?

Will this authentication mechanism cause any grief with code running on mobile and tablet clients such as iPhone or Windows 8 Surface? I would suggest that this would work if these applications could handle cookies. Thanks!

+4
source share
1 answer

Since I had no answers, I decided to do it myself. First, I found an open source project that implements session cookies in an agnostic algorithm. I used this as a starting point for implementing a similar handler.

One of the problems that I have encountered with the built-in ASP.NET implementation, which is a similar limitation in the AppHarbor implementation, is the sessions that only enter the username. I wanted to be able to store arbitrary data for user identification, such as their UUID in the database, as well as the login name. Since my existing code assumes that this data is available in a cookie, it will take a lot of refactoring if this data is no longer available. Also, I like the idea of ​​storing basic user information without having to go to the database.

Another problem with the AppHarbor project, identified in this open issue , is not the encryption algorithm. This is not entirely true, since AppHarbor is an agnostic of the algorithm, but it was suggested that a sample project show how to use PBKDF2 . For this reason, I decided to use this algorithm (implemented in the .NET Framework through the Rfc2898DeriveBytes class ) in my code.

Here is what I could come up with. This meant a starting point for those who want to implement their own session management, so feel free to use it for any purpose that suits you.

 using System; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; using System.Security; using System.Security.Cryptography; using System.Security.Principal; using System.Web; namespace AuthTest { [Serializable] public class AuthIdentity : IIdentity { public Guid Id { get; private set; } public string Name { get; private set; } public AuthIdentity() { } public AuthIdentity(Guid id, string name) { Id = id; Name = name; } public string AuthenticationType { get { return "CookieAuth"; } } public bool IsAuthenticated { get { return Id != Guid.Empty; } } } [Serializable] public class AuthToken : IPrincipal { public IIdentity Identity { get; set; } public bool IsInRole(string role) { return false; } } public class AuthModule : IHttpModule { static string COOKIE_NAME = "AuthCookie"; //Note: Change these two keys to something else (VALIDATION_KEY is 72 bytes, ENCRYPTION_KEY is 64 bytes) static string VALIDATION_KEY = @"MkMvk1JL/ghytaERtl6A25iTf/ABC2MgPsFlEbASJ5SX4DiqnDN3CjV7HXQI0GBOGyA8nHjSVaAJXNEqrKmOMg=="; static string ENCRYPTION_KEY = @"QQJYW8ditkzaUFppCJj+DcCTc/H9TpnSRQrLGBQkhy/jnYjqF8iR6do9NvI8PL8MmniFvdc21sTuKkw94jxID4cDYoqr7JDj"; static byte[] key; static byte[] iv; static byte[] valKey; public void Dispose() { } public void Init(HttpApplication context) { context.AuthenticateRequest += OnAuthenticateRequest; context.EndRequest += OnEndRequest; byte[] bytes = Convert.FromBase64String(ENCRYPTION_KEY); //72 bytes (8 for salt, 64 for key) byte[] salt = bytes.Take(8).ToArray(); byte[] pw = bytes.Skip(8).ToArray(); Rfc2898DeriveBytes k1 = new Rfc2898DeriveBytes(pw, salt, 1000); key = k1.GetBytes(16); iv = k1.GetBytes(8); valKey = Convert.FromBase64String(VALIDATION_KEY); //64 byte validation key to prevent tampering } public static void SetCookie(AuthIdentity token, bool rememberMe = false) { //Base64 encode token var formatter = new BinaryFormatter(); MemoryStream stream = new MemoryStream(); formatter.Serialize(stream, token); byte[] buffer = stream.GetBuffer(); byte[] encryptedBytes = EncryptCookie(buffer); string str = Convert.ToBase64String(encryptedBytes); var cookie = new HttpCookie(COOKIE_NAME, str); cookie.HttpOnly = true; if (rememberMe) { cookie.Expires = DateTime.Today.AddDays(100); } HttpContext.Current.Response.Cookies.Add(cookie); } public static void Logout() { HttpContext.Current.Response.Cookies.Remove(COOKIE_NAME); HttpContext.Current.Response.Cookies.Add(new HttpCookie(COOKIE_NAME, "") { Expires = DateTime.Today.AddDays(-1) }); } private static byte[] EncryptCookie(byte[] rawBytes) { TripleDES des = TripleDES.Create(); des.Key = key; des.IV = iv; MemoryStream encryptionStream = new MemoryStream(); CryptoStream encrypt = new CryptoStream(encryptionStream, des.CreateEncryptor(), CryptoStreamMode.Write); encrypt.Write(rawBytes, 0, rawBytes.Length); encrypt.FlushFinalBlock(); encrypt.Close(); byte[] encBytes = encryptionStream.ToArray(); //Add validation hash (compute hash on unencrypted data) HMACSHA256 hmac = new HMACSHA256(valKey); byte[] hash = hmac.ComputeHash(rawBytes); //Combine encrypted bytes and validation hash byte[] ret = encBytes.Concat<byte>(hash).ToArray(); return ret; } private static byte[] DecryptCookie(byte[] encBytes) { TripleDES des = TripleDES.Create(); des.Key = key; des.IV = iv; HMACSHA256 hmac = new HMACSHA256(valKey); int valSize = hmac.HashSize / 8; int msgLength = encBytes.Length - valSize; byte[] message = new byte[msgLength]; byte[] valBytes = new byte[valSize]; Buffer.BlockCopy(encBytes, 0, message, 0, msgLength); Buffer.BlockCopy(encBytes, msgLength, valBytes, 0, valSize); MemoryStream decryptionStreamBacking = new MemoryStream(); CryptoStream decrypt = new CryptoStream(decryptionStreamBacking, des.CreateDecryptor(), CryptoStreamMode.Write); decrypt.Write(message, 0, msgLength); decrypt.Flush(); byte[] decMessage = decryptionStreamBacking.ToArray(); //Verify key matches byte[] hash = hmac.ComputeHash(decMessage); if (valBytes.SequenceEqual(hash)) { return decMessage; } throw new SecurityException("Auth Cookie appears to have been tampered with!"); } private void OnAuthenticateRequest(object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; var cookie = context.Request.Cookies[COOKIE_NAME]; if (cookie != null && cookie.Value.Length > 0) { try { var formatter = new BinaryFormatter(); MemoryStream stream = new MemoryStream(); var bytes = Convert.FromBase64String(cookie.Value); var decBytes = DecryptCookie(bytes); stream.Write(decBytes, 0, decBytes.Length); stream.Seek(0, SeekOrigin.Begin); AuthIdentity auth = formatter.Deserialize(stream) as AuthIdentity; AuthToken token = new AuthToken() { Identity = auth }; context.User = token; //Renew the cookie for another 100 days (TODO: Should only renew if cookie was originally set to persist) context.Response.Cookies[COOKIE_NAME].Value = cookie.Value; context.Response.Cookies[COOKIE_NAME].Expires = DateTime.Today.AddDays(100); } catch { } //Ignore any errors with bad cookies } } private void OnEndRequest(object sender, EventArgs e) { var context = ((HttpApplication)sender).Context; var response = context.Response; if (response.Cookies.Keys.Cast<string>().Contains(COOKIE_NAME)) { response.Cache.SetCacheability(HttpCacheability.NoCache, "Set-Cookie"); } } } } 

Also, be sure to include the following file in the web.config :

 <httpModules> <add name="AuthModule" type="AuthTest.AuthModule" /> </httpModules> 

In your code, you can find the current user with:

 var id = HttpContext.Current.User.Identity as AuthIdentity; 

And set the auth cookie as follows:

 AuthIdentity token = new AuthIdentity(Guid.NewGuid(), "Mike"); AuthModule.SetCookie(token, false); 
+1
source

Source: https://habr.com/ru/post/1441682/