Повторная разработка моей стратегии аутентификации с помощью ASP.NET

В настоящее время я использую собственный код аутентификации для моего сайта, который построен на .NET. Я не принимал стандартный маршрут Forms Auth, так как все примеры, которые я смог найти, были тесно интегрированы с WebForms, которые я не использую. Для всех целей и задач у меня есть все статические HTML, и любая логика выполняется через Javascript и вызовы веб-службы. Вещи, такие как вход в систему, выход из системы и создание новой учетной записи, выполняются, даже не выходя из страницы.

Вот как это работает сейчас: в базе данных у меня есть User ID, a Security ID и a Session ID. Все три являются UUID, а первые два никогда не меняются. Каждый раз, когда пользователь входит в систему, я проверяю таблицу user для строки, которая совпадает с этим именем пользователя и хешированным паролем, и я обновляю Session ID до нового UUID. Затем я создаю файл cookie, который представляет собой сериализованное представление всех трех UUID. В любых вызовах защищенных веб-сервисов я десериализую этот файл cookie, чтобы убедиться, что в таблице пользователей есть строка с этими тремя UUID. Это довольно простая система и работает хорошо, однако мне не очень нравится тот факт, что пользователь может войти в систему только с одним клиентом за раз. Это вызовет проблемы при создании мобильных и планшетных приложений и уже создает проблемы, если у них несколько компьютеров или веб-браузеров. По этой причине я собираюсь выбросить эту систему и перейти к чему-то новому. Поскольку я написал это много лет назад, я полагаю, что может быть что-то гораздо более рекомендуемое.

Я читал в классе FormsAuthentication в .NET Framework, который обрабатывает файлы cookie, и запускается как HttpModule для проверки каждого запроса. Мне интересно, могу ли я воспользоваться этим в своем новом дизайне.

Похоже, что файлы cookie не имеют статуса, и сеансы не нужно отслеживать в базе данных. Это делается из-за того, что файлы cookie шифруются с помощью закрытого ключа на сервере, которые также могут совместно использоваться в кластере веб-серверов. Если я сделаю что-то вроде:

FormsAuthentication.SetAuthCookie("Bob", true);

Затем в более поздних запросах я могу быть уверен, что Боб действительно является действительным пользователем, поскольку cookie будет очень сложно, если не невозможно подделать.

Было бы разумным использовать класс FormsAuthentication для замены моей текущей модели проверки подлинности? Вместо столбца Session ID в базе данных я полагался на зашифрованные файлы cookie для представления действительных сеансов.

Существуют ли рамки проверки подлинности .NET для сторонних разработчиков/с открытым исходным кодом, которые могут работать лучше для моей архитектуры?

Будет ли этот механизм аутентификации вызывать какое-либо горе с кодом, запущенным на мобильных и планшетных клиентах, например iPhone или Windows 8 Surface? Я бы предположил, что это сработает, если эти приложения могут обрабатывать файлы cookie. Спасибо!

1 ответ

Поскольку у меня не было ответов, я решил сделать это сам. Во-первых, я нашел проект с открытым исходным кодом, который реализует куки сессии в алгоритме агностически. Я использовал это как отправную точку для реализации аналогичного обработчика.

Одна из проблем, с которой я столкнулся со встроенной реализацией ASP.NET, которая является аналогичным ограничением в реализации AppHarbor, - это сеансы, на которые вводится только имя пользователя. Я хотел иметь возможность хранить произвольные данные для идентификации пользователя, такие как их UUID в базе данных, а также имя входа в систему. Поскольку в моем существующем коде предполагается, что эти данные доступны в файле cookie, потребуется много рефакторинга, если эти данные больше не будут доступны. Кроме того, мне нравится идея хранить основную информацию пользователя без необходимости попадания в базу данных.

Еще одна проблема с проектом AppHarbor, указанная в этой открытой проблеме, - это не алгоритм шифрования. Это не совсем так, поскольку AppHarbor является агностиком алгоритма, однако было предложено, чтобы образец проекта показывал, как использовать PBKDF2. По этой причине я решил использовать этот алгоритм (реализованный в .NET Framework через класс Rfc2898DeriveBytes) в моем коде.

Вот что я смог придумать. Это означало отправную точку для тех, кто хочет реализовать свое собственное управление сеансом, поэтому не стесняйтесь использовать его в любых целях, которые вам подходят.

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");
 }
 }
 }
}
</string></byte>

Кроме того, не забудьте включить следующий файл в файл web.config:

<httpmodules>
 </httpmodules>

В вашем коде вы можете найти текущего пользователя с помощью:

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

И установите файл cookie auth следующим образом:

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

licensed under cc by-sa 3.0 with attribution.