Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Framework »

Обсудить на форуме Обсудить на форуме

Система аутентификации на базе протокола HTTP Digest. Усиление модуля.

В статье "Система аутентификации на базе протокола HTTP Basic был рассмотрен алгоритм Basic аутентификации и с помощью него была построена система Basic аутентификации на основе ролей, работающая без специальной настройки IIS сервера и использующая базу данных для хранения учетных записей пользователей.

У Basic есть один недостаток, а именно username и password передаются по сети открыто (clear text), base64 кодировка не может считаться защитой.

Аутентификация Digest

В этой статье будет рассмотрен алгоритм Digest аутентификации, решающей некоторые проблемы, имеющиеся у HTTP Basic Authentication. Например эта схема не передаёт password по сети открытым текстом. Официальное название схемы - "Digest Access Authentication".

Расширим нашу систему из предыдущей статьи.

К преимуществам Digest можно отнести следуещее:

  1. passwords не передаются открыто по сети
  2. способность защиты от повторяющихся атак (monitoring http nc value)
  3. способность создать защиту (monitoring nonce)
    • в определённый промежуток времени
    • от определённого client
    • от определённого request

Один сайт может одновременно использовать несколько систем защиты, например Basic и Digest

Пришло время рассмотреть работу алгоритма Digest аутентификации:

1. Первый запрос от User Agent к Http Server заголовок Authorization пустой - значит server должен возвратить запрос на аутентификацию. Например такой:

2. ответ сервера:

HTTP/1.1 401 Unauthorized
        WWW-Authenticate: Digest
                 realm="testrealm@host.com",
                 qop="auth",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41",
                 algorithm=MD5,
                 stale=false

Разберём заголовок WWW-Authenticate (как вы заметили, он усложнился по сравнению с заголовком Basic):

realm Строка, указывающая юзеру где он и какой пароль вводить например "registered_users@gotham.news.com".
nonce Уникальная строка, которая генерируется на сервере в момент ответа 401. запрещено использовать кавычку, так как внутри заголовка строка в кавычках рекомендуется также закодировать её base64, например time-stamp H(time-stamp ":" ETag ":" private-key)
opaque Строка, которую юзер должен будет вернуть на сервер в неизменённом виде Рекомендуется закодировать base64
stale true/false
Индикатор, который показывает, что если true - запрос был правильный, username-password тоже, nonce неправильный false или любое другое значение или отсутствие stale - неправильные username, password
algorithm optional, MD5 = default
qop указывает "quality of protection".
"auth" указывает authentication,
"auth-int" указывает authentication + integrity protection. могут быть оба через запятую

3. У юзера всплывает модальное окно, предлагающее ввести username и password (обратите внимание, окно отличается от Basic окна). Происходит запрос юзера для аутентификации:

GET ... ... HTTP/1.1
        Authorization: Digest
                 username="Mufasa",
                 realm="testrealm@host.com",
                 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                 uri="/dir/index.html",
                 qop=auth,
                 nc=00000001,
                 cnonce="0a4f113b",
                 response="6629fae49393a05397450978507c4ef1",
                 opaque="5ccc069c403ebaf9f0171e9517f40e41"

Теперь разберём заголовок Authorization:

username имя юзера
realm см. WWW-Authenticate
qop см. WWW-Authenticate (должно совпадать с одним из списка qop WWW-Authenticate)
algorithm см. WWW-Authenticate (должно совпадать)
opaque см. WWW-Authenticate (должно совпадать)
uri запрос (например страница)
response 32-character строка - именно c её помощью проверяется пароль.
nonce  
nc once count - сколько раз был использован текущий nonce
cnonce уникальная строка, посылаемая браузером на сервер

4. Последовательность обработки запроса пользователя

  1. Проверяем, существует ли заголовок Authorization
  2. Проверяем, является ли он Digest
  3. Отсекаем слово Digest
  4. Берём username (обратите внимание - в заголовке отсутствует password - по крайней мере его не видно)
  5. проверяем базу данных с этим юзером, существует ли он - запоминаем его password из базы
  6. проверяем запрос по ролям против страниц с ролями, как мы уже делали в Basic.

Если что-то не так - переходим к пункту 6. Если всё ok - идём дальше (это ещё далеко не всё :))

5. Проверка пароля пользователя

  1. создаём строку A1 вида
    A1 = unq(username) : unq(realm) : passwd
  2. хешируем A1
    HA1 = MD5(A1)
  3. создаём строку A2 вида
    A2 = Http Method ":" digest-uri
  4. хешируем A2
    HA2 = MD5(A2)
  5. создаём строку GENRESPONSE вида
    GENRESPONSE = HA1 ":" nonce ":" nc ":" cnonce ":" qop ":" HA2
  6. хешируем GENRESPONSE
    HGENRESPONSE = MD5(GENRESPONSE)

если HGENRESPONSE равен response в заголовке Authorization запроса юзера и nonce в порядке - всё ok, нет - переходим к пункту 6

6. выдаём состояние ответа сервера 401, поднимающее модальное окно как в пункте 2 иначе говоря формируем WWW-Authenticate по типу Digest

HTTP Модуль AuthDigest

Ну что ж, вооружившись рассмотренными выше теоретическими выкладками напишем наш HttpModule, который воплотит Digest аутентификацию.

   // наследует HttpModule

   public class AuthDigest : IHttpModule
   {
      public AuthDigest()  { }
      public void Dispose()  { }


      public void Init(HttpApplication application)
      {
         application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
         application.EndRequest += new EventHandler(this.OnEndRequest);
      }


      // Как обычно, нам нужно 2 метода:
      //                              OnAuthenticateRequest
      //                              OnEndRequest

      /*
      ##########################################################################
      # OnAuthenticateRequest
      # <summary>
      #
      # </summary>
      ##########################################################################
      */
      public void OnAuthenticateRequest(object source, EventArgs eventArgs)
      {
         HttpApplication app = (HttpApplication) source;


         // достаём заголовок, проверяем его

         // get Authorization header; check if not empty
         string authorization = app.Request.Headers["Authorization"];
         if ((authorization == null) || (authorization.Length == 0))
         {
            AccessDenied(app);
            return;
         }


         // Digest не Digest?

         // is it digest scheme?
         authorization = authorization.Trim();
         if (authorization.IndexOf("Digest", 0) != 0)
         {
            AccessDenied(app);
            return;
         }


         // записываем части заголовка в dictionary

         // get Header parts
         ListDictionary dictAuthHeaderContents = new ListDictionary();
         dictAuthHeaderContents = getHeaderParts(authorization);


         // проверяем юзера против базы на основе ролей
         // если всё нормально - запоминаем password
         // в этом подходе - отличие от Basic
         // получаем группы юзера - для экземпляра GenericPrincipal

         //
         // check against DATABASE (by roles)
         string username = (string)dictAuthHeaderContents["username"];
         string password = "";
         string[] groups;
         if (!AuthenticateAgentDigest(app, username, out password, out groups))
         {
            AccessDenied(app);
            return;
         }


         // см. пункт 5 алгоритма Digest

         //
         // check against DIGEST SCHEME
         string realm = ConfigurationSettings.AppSettings["HTTPDigest.Components.AuthDigest_Realm"];


         // a)
         // A1 = unq(username-value) ":" unq(realm-value) ":" passwd
         string A1 = String.Format("{0}:{1}:{2}",
                                   (string)dictAuthHeaderContents["username"],
                                   realm,
                                   password);

         // b)
         // HA1 = MD5(A1)
         string HA1 = CvtHex(A1);

         // c)
         // A2 = HTTP Method ":" digest-uri-value
         string A2 = String.Format("{0}:{1}",
                                   app.Request.HttpMethod,
                                   (string)dictAuthHeaderContents["uri"]);

         // d)
         // HA2 = MD5(A2)
         string HA2 = CvtHex(A2);

         // e)
         // GENRESPONSE = HA1 ":" nonce ":" nc ":" cnonce ":" qop ":" HA2
         string GENRESPONSE;
         if (dictAuthHeaderContents["qop"] != null)
         {
            GENRESPONSE = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
                                        HA1,
                                        (string)dictAuthHeaderContents["nonce"],
                                        (string)dictAuthHeaderContents["nc"],
                                        (string)dictAuthHeaderContents["cnonce"],
                                        (string)dictAuthHeaderContents["qop"],
                                        HA2);
         }
         else
         {
            GENRESPONSE = String.Format("{0}:{1}:{2}",
                                        HA1,
                                        (string)dictAuthHeaderContents["nonce"],
                                        HA2);
         }

         string HGENRESPONSE = CvtHex(GENRESPONSE);


         // Проверяем nonce

         bool isNonceStale = !IsValidNonce((string)dictAuthHeaderContents["nonce"]);
         app.Context.Items["staleNonce"] = isNonceStale;


         // Сверяем HGENRESPONSE с response заголовка
         // Проверяем nonce
         // если всё ok - создаём GenericPrincipal с группами юзера

         if (((string)dictAuthHeaderContents["response"] == HGENRESPONSE) && (!isNonceStale))
         {
            app.Context.User = new GenericPrincipal(new GenericIdentity(username, "HTTPDigest.Components.AuthDigest"), groups);
         }
         else
         {
            AccessDenied(app);
            return;
         }
      }


      /*
      ##########################################################################
      # OnEndRequest
      # <summary>
      #  set server response WWW-Authenticate header (digest scheme)
      #  build header string according to scheme
      #  lift up modal window
      # </summary>
      ##########################################################################
      */
      public void OnEndRequest(object source, EventArgs eventArgs)
      {
         HttpApplication app = (HttpApplication) source;
         if (app.Response.StatusCode == 401)
         {
            // from config.
            string lRealm     = ConfigurationSettings.AppSettings["HTTPDigest.Components.AuthDigest_Realm"];
            string lOpaque    = ConfigurationSettings.AppSettings["HTTPDigest.Components.AuthDigest_Opaque"];
            string lAlgorithm = ConfigurationSettings.AppSettings["HTTPDigest.Components.AuthDigest_Algorithm"];
            string lQop       = ConfigurationSettings.AppSettings["HTTPDigest.Components.AuthDigest_Qop"];

            // generate
            string lNonce = GenerateNonce();

            bool isNonceStale = false;
            object staleObj = app.Context.Items["staleNonce"];
            if (staleObj != null)
               isNonceStale = (bool)staleObj;


            // Поднимаем модальное окно типа Digest
            // Для этого собираем заголовок WWW-Authenticate

            // build authHeader string
            StringBuilder authHeader = new StringBuilder("Digest");
            authHeader.Append(" realm=\"");
            authHeader.Append(lRealm);
            authHeader.Append("\"");
            authHeader.Append(", nonce=\"");
            authHeader.Append(lNonce);
            authHeader.Append("\"");
            authHeader.Append(", opaque=\"");
            authHeader.Append(lOpaque);
            authHeader.Append("\"");
            authHeader.Append(", stale=");
            authHeader.Append(isNonceStale ? "true" : "false");
            authHeader.Append(", algorithm=\"");
            authHeader.Append(lAlgorithm);
            authHeader.Append("\"");
            authHeader.Append(", qop=\"");
            authHeader.Append(lQop);
            authHeader.Append("\"");

            app.Response.AppendHeader("WWW-Authenticate", authHeader.ToString());


            // Задаём код состояния 401

            app.Response.StatusCode = 401;
         }
      }


      /*
      ##########################################################################
      # GenerateNonce
      # <summary>
      #  generate unique server nonce
      # </summary>
      ##########################################################################
      */
      protected virtual string GenerateNonce()
      {

         // Создаём unique nonce - самый облегчённый вариант
         // настоящее время + 3 минуты, закодированное base64
         // проверка на качество nonce тоже будет происходить по времени
         // Пример усиленного варианта - использование ETag и ещё одного ключа, который
         // знает server

         DateTime nonceTime = DateTime.Now + TimeSpan.FromMinutes(3);
         string expireStr = nonceTime.ToString("G");

         Encoding enc = new ASCIIEncoding();
         byte[] expireBytes = enc.GetBytes(expireStr);
         string nonce = Convert.ToBase64String(expireBytes);


         // base64 приписывает знак равенства, запрещённый в заголовке
         // отсекаем его

         nonce = nonce.TrimEnd(new Char[] {'='});
         return nonce;
      }


      /*
      ##########################################################################
      # IsValidNonce
      # <summary>
      #  string nonce : in
      # </summary>
      ##########################################################################
      */
      protected virtual bool IsValidNonce(string nonce)
      {

         // Проверяем nonce
         // раскодируем из base64 и сверяем
         // в этом примере nonce простой - поэтому и проверка простая -
         // сверяем с временем

         DateTime expireTime;
         int numPadChars = nonce.Length % 4;
         if (numPadChars > 0)
            numPadChars = 4 - numPadChars;
         string newNonce = nonce.PadRight(nonce.Length + numPadChars, '=');

         try
         {
            byte[] decodedBytes = Convert.FromBase64String(newNonce);
            string preNonce = new ASCIIEncoding().GetString(decodedBytes);
            expireTime = DateTime.Parse(preNonce);
         }
         catch (FormatException)
         {
            return false;
         }
         return (expireTime >= DateTime.Now);
      }


      /*
      ##########################################################################
      # CvtHex - hashes strings
      # <summary>
      #  string sToConvert : in
      #  string SConverted : out
      # </summary>
      ##########################################################################
      */
      private string CvtHex(string sToConvert)
      {

         // Хэширование

         Encoding enc = new ASCIIEncoding();
         MD5 md5 = new MD5CryptoServiceProvider();
         byte[] bToConvert = md5.ComputeHash(enc.GetBytes(sToConvert));
         string sConverted = "";
         for (int i = 0 ; i < 16 ; i++)
            sConverted += String.Format("{0:x02}", bToConvert[i]);
         return sConverted;
      }


      /*
      ##########################################################################
      # getHeaderParts(string authorization)
      # <summary>
      #  convert Authorization header
      #  from string to Dictionary
      #  string authorization : in
      #  ListDictionary       : out
      # </summary>
      ##########################################################################
      */
      private ListDictionary getHeaderParts(string authorization)
      {


         // Функция, которая переводит заголовок HTTP со всем его содержимым
         // в объект ListDictionary

         ListDictionary dict = new ListDictionary();
         string[] parts = authorization.Substring(7).Split(new char[] {','});
         foreach (string part in parts)
         {
            string[] subParts = part.Split(new char[] {'='}, 2);
            string key = subParts[0].Trim(new char[] {' ', '\"'});
            string val = subParts[1].Trim(new char[] {' ', '\"'});
            dict.Add(key, val);
         }
         return dict;
      }


      /*
      ##########################################################################
      # AccessDenied
      # 401 - Access Denied
      # <summary>
      # app      in ;  HttpApplication
      # </summary>
      ##########################################################################
      */
      private void AccessDenied(HttpApplication app)
      {

         // Вход воспрещён
         // пишем в браузер

         app.Response.StatusCode = 401;
         app.Response.StatusDescription = "Access Denied";
         app.Response.Write("401 Access Denied");
         app.CompleteRequest();
      }


      // следующий метод реализует проверку против базы данных
      // на основе ролей
      // если всё нормально, возвращает true и список групп пользовател
      // который нужен для создания экземпляра GenericPrincipal
      // + его password - в этом отличие от Basic


      /*
      ##########################################################################
      # AuthenticateAgentDigest
      #
      # <summary>
      # Authenticates Agent, returns true/false
      # app      in ;  HttpApplication
      # User     in ;  username
      # Password out;  password to hash then and check
      # groups   out;  agent groups to create GenericPrincipal
      # </summary>
      ##########################################################################
      */
      protected virtual bool AuthenticateAgentDigest(HttpApplication app, string username, out string password, out string[] groups)
      {
         password = "";
         groups = null;
         int lagentID = 0;
         string lpageURL = "";


         // экземпляр класса, который осуществляет работу с базой
         // код прилагаетс

         SqlDataProvider dataProvider = new SqlDataProvider();


         // проверим есть ли вообще такой юзер
         // если есть - достанем его password

         // get agent if exists
         lagentID = dataProvider.getAgentByUsername(username, out password);
         if (lagentID == 0)
            return false;


         // проверим есть ли вообще у него группы

         // get agent groups
         ArrayList arrAgentsGroups = new ArrayList();
         arrAgentsGroups = dataProvider.getGroupsByAgentID(lagentID);
         if (arrAgentsGroups.Count == 0)
            return false;


         // проверим есть ли группы у запрашиваемой страницы

         // get pages groups
         lpageURL = app.Request.Path;
         ArrayList arrPagesGroups = new ArrayList();
         arrPagesGroups = dataProvider.getGroupsByPageURL(lpageURL);
         if (arrPagesGroups.Count == 0)
            return false;


         // проверим если хотя бы одна группа юзера
         // находится в списке групп запрашиваемой страницы
         // если да - возвращаем true

         // check if at least one agent group is in Page Groups List
         string[] pagegroups = (String[]) arrPagesGroups.ToArray(typeof(String));
         groups = (string[]) arrAgentsGroups.ToArray(typeof(string));

         foreach (String groupagentID in groups)
         {
            foreach (String grouppageID in pagegroups)
            {
               if (groupagentID == grouppageID)
                  return true;
            }
         }
         return false;
      }
   }

---------------------------------------------------------

Всё. HttpModule готов. Подключим его к веб приложению: Для этого в файле web.config в разделе sytem.web пишем

      <httpModules>
         <add name="DigestAuthenticationModule"  type="HttpDigest.Components.AuthDigest, HttpDigest" />
      </httpModules>

отменяем встроенную аутентификацию

      <authentication mode="None" />

      <authorization>
         <deny users="?" />
      </authorization>

и сохраняем данные для Digest аутентификации (realm string, алгоритм, qop string, server opaque)

   <appSettings>
      <add key="HttpDigest.Components.AuthDigest_Realm"     value="testrealm@host.com" />
      <add key="HttpDigest.Components.AuthDigest_Opaque"    value="5ccc069c403ebaf9f0171e9517f40e41" />
      <add key="HttpDigest.Components.AuthDigest_Algorithm" value="MD5" />
      <add key="HttpDigest.Components.AuthDigest_Qop"       value="auth" />
   </appSettings>

Вот и всё.

Алгоритм Digest аутентификации не претендует на решение всех задач, связанных с безопасностью в интернете. Эта схема не кодирует содержимое запроса-ответа. Цель Digest заключается в том, чтобы обеспечить систему аутентификации, такую же простую и удобную, как и Basic, но в которой бы отсутствовали недостатки, присущие Basic аутентификации. Тем не менее эта система гораздо сильнее, чем например CRAM-MD5, которая была предложена для LDAP, POP и IMAP(rfc 2195).

К коду прилагаются database scripts и упрощённый класс для работы с базой.

При написании этой статьи использовались  rfc 2617 - HTTP Authentication: Basic and Digest Access Authentication и rfc 1321 - MD5 алгоритм.


Текст примеров данной статьи можно выкачать здесь


Может пригодится:


Автор: Anatoly Lubarsky
Прочитано: 16124
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру