Аутентификация на Asp.net сайтах с помощью Rutoken WEB

в 10:05, , рубрики: .net, ASP.NET, аутентификация, Блог компании Компания «Актив», информационная безопасность, рутокен, рутокен web, эцп, метки: , , , ,

Аутентификация на Asp.net сайтах с помощью Rutoken WEB
Решение Рутокен WEB позволяет реализовать строгую аутентификацию для web-ресурсов, используя электронную подпись по ГОСТ Р 34-10.2001. Более подробно про алгоритмы можно прочитать в этой статье. Здесь покажем как сделан действующий вариант использования Рутокен WEB на сайтах под управлением Asp.net и приведем инструкцию по сборке.

Сделать так, чтобы все работало, действительно просто.

Решение Рутокен WEB состоит из следующих компонентов:

  • USB-токена Рутокен WEB (не требует установки драйверов)
  • клиентских кроссплатформенных мультибраузерных плагинов
  • клиентских скриптов для работы с плагином
  • серверных компонентов

Само устройство можно купить, например здесь. Плагин для браузера можно скачать тут. Осталось сделать серверный компонент, клиентский javascript и все это соединить.

Алгоритм аутентификации

Аутентификация пользователя, не зависимо от платформы, подразумевает предъявление некоего идентификатора субъекта, проверку идентификатора и принятия решения о доступе. Например, предъявление логина с паролем, проверка данных по базе, и установка аутентифицирующей cookie в случае успеха.

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

Реализация алгоритма

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

Поэтому, первым делом сформируем на клиенте, в его устройстве Рутокен WEB, контейнер, содержащий ключевую пару, и передадим на сервер открытый ключ и уникальный идентификатор устройства Рутокен WEB. Закрытый ключ является не извлекаемым, соответственно, не покидает устройство.

Контейнер назовем тоже не абы как, а по схеме {логин}#%#{sitename}{port}. Например, yandex@gmail.com#%#dotnet.rutokenweb.ru:80. Название будет использовано в дальнейшем, при отображении списка логинов на токене.

На сервере получаем открытый ключ и id токена и привязываем их существующему клиенту. Мы же должны знать, кто попытается получить доступ.

Этап подготовки закончен, можно аутентифицировать клиентов.

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

  1. Клиент отправляет на сервер запрос, содержащий идентификатор и признак того, что нужно аутентифицироваться.
  2. Сервер генерирует случайные данные, например, строку; хэширует данные, запоминает в сессии и отправляет клиенту. Назовем эти данные s1.
  3. Клиент получает хэш данных, генерирует свои случайные данные (s2), формирует хэш суммы строк и подписывает данный хэш (получаем ЭЦП). Далее клиент передает на сервер те данные, что сгенерировал сам (s2) и ЭЦП суммы строк.
  4. Сервер получает случайные данные клиента (s2)и ЭЦП, аналогично формирует хэш суммы случайных данных клиента (s2) и данных, сформированных в начале сервером (s1).
  5. В результате на сервере есть данные (хэш s1 + s2) и подпись этих данных. Остается только проверить корректность подписи.

Пример реализации на C#

В моем случае аутентификацию по Рутокен WEB нужно было прикрутить на 3 сайта. 2 из них используют аутентификацию Forms, еще один работает с Windows Identity Foundation, использует STS сервис для аутентификации. Все три сайта работают на WebForms.

Сделаем для них WebControl с нужным функционалом, на самом деле два контрола. Один будет использоваться при аутентификации, другой для управления привязками Рутокен WEB, например — в личном кабинете.

Все запросы на сервер будут ajax запросами, без полного постбэка. Таким образом контролы нужны, по большому счету, для представления на странице необходимых элементов и javascript-ов, а обработкой аякс-запросов займется httpHandler. Он же будет отдавать клиенту локализованный javascript.

И, наконец, с остальным сайтом контрол и хэндлер будут взаимодействовать с помощью объекта, реализующего интерфейс ITokenProcessor, где объявлены специфичные для каждого конкретного сайта методы, нужные нашему решению. Например, получение открытого ключа, получение имени пользователя и прочее.

Схематично все это выглядит так:
Аутентификация на Asp.net сайтах с помощью Rutoken WEB

Подготовка к аутентификации, как уже говорилось, сводится к формированию на Рутокен WEB контейнера с закрытым и открытым ключом и передаче на сервер открытого ключа и id токена, с привязкой данных к аккаунту пользователя. Данная операция должна быть доступна уже аутентифицированным пользователям, а сам контрол с функционалом можно разместить например в личном кабинете. Этим будет заниматься контрол с редким названием Administration, а контрол с названием Login займется процессом аутентификации.

Реализация httpHandler

Задачи нашего обработчика:

1. Обработать ajax запрос клиента с Рутокен WEB.
Хэндлер будет обрабатывать ajax запросы только с известными ему headers ('X-Requested-With','XhrRutoken').

Сделаем класс для разбора запроса (CMessageRequest) и класс для формирования ответа (CMessageResponse). При запросе создаем экземпляр класса для разбора запроса, присваивая его мемберу хэндлера. Разбор происходит в конструкторе.

_mRequest = new CMessageRequest(context);

В запросе передается название метода, который и запускаем, если конечно найдем, рефлексией.

GetType().InvokeMember(_mRequest.act, BindingFlags.InvokeMethod, null, this,  new object[] {});

В методе запрос обрабатывается, в результате создаем экземпляр класса с ответом. В конце концов ответ сериализуется в json и передается в Response.

2. Отдать локализованный javascript на страницу.
Javascript добавляется на страницу так —

<script src=" /RutokenWebSTS/rutokenweb/ajax.rtw?getRutokenJavaLocal=1" type="text/javascript"></script>

Разметку выдает контрол (об этом ниже). В случае запроса с getRutokenJavaLocal=1 опять задействуем наш хэндлер, на этот раз для отдачи javascript.

Все javascript-ы добавлены в сборку как Embedded Resource. Можно было бы ограничиться простым добавлением ресурса. Вначале так и было. Но вот появился заказчик из белоарабии и захотел возможность локализации. Поэтому добавляем не простую, а золотую, локализованную версию, так:

        private void SendLocalizeScript()
        {
            using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(
                        "RutokenWebPlugin.javascript.tokenadmin.js"))
            {
                if (stream != null)
                {
                    var reader = new StreamReader(stream);
                    HttpContext.Current.Response.Write(Utils.LocalizeScript(reader.ReadToEnd()));
                }
            }
        }

LocalizeScript выдает уже локализованную версию скрипта, для чего парсит скрипт и выдает нужную нам строку, заменяя все вхождения LOCALIZE(ключ_ресурса) на строку из ресурсного файла RutokenLocalText.resx

private static Regex REGEX = new Regex(@"LOCALIZE(([^))]*))", RegexOptions.Singleline | RegexOptions.Compiled);
…
public static string LocalizeScript(string text)
        {
            var matches = REGEX.Matches(text);

            foreach (Match match in matches)
            {
                string strResourceStringID = match.Groups[1].Value;
                string str = (string)HttpContext.GetGlobalResourceObject("RutokenLocalText", strResourceStringID) ?? strResourceStringID;
                text = str != strResourceStringID ? text.Replace(match.Value, MakeValidString(str)) : text.Replace(match.Value, string.Format("'LOCALIZE.{0}'", str));
            }

            return text;
        }

Ресурсы есть в исходниках примера.

ITokenController

Наши контролы и хэндлер будут взаимодействовать с сайтом/приложением посредством интерфейса ITokenController. Методы интерфейса подробно расписаны в исходниках. Они реализуют специфичный для сайта функционал. Например, получение/сохранение ключей, получение имени пользователя и т.п.

Чтобы все заработало, объект, реализующий этот интерфейс, нужно передавать в метод контролов, например:

class CustomTokenProcessor : ITokenProcessor
...
// tokenLogin - контрол
tokenlogin.SetRequired(new CustomTokenProcessor(), returnurl);

метод фактически помещает объект в сессию

        public void SetRequired(ITokenProcessor processor, string successurl)
        {
            var session = HttpContext.Current.Session;
            if (session != null)
            {
                if (session["TokenProcessor"] == null)
                {
                     session["TokenProcessor"] = processor;
                }
               
                session["SuccessUrl"] = successurl;
            }
            
        }

И объект становится доступным хэндлеру.

Так же в хэндлере используется event OnSuccessAuth, срабатывающий при успешной аутентификации. Причем на событие можно подписываться в контроле, а не в хэндлере. Сделано так для возможности доступа к сессии в методе, который добавлен к евенту. При этом методу передается объект сесии

if ((OnSuccessAuth = (EventHandler) _mContext.Session["OnSuccessAuth"]) != null)
        {
               OnSuccessAuth(_mContext.Session, new EventArgs());
                _mContext.Session["OnSuccessAuth"] = null;
         }

А в методе получаем сессию так

        private void tokenlogin_OnSuccessAuth(object sender, EventArgs e)
        {
            HttpSessionState  session = (HttpSessionState) sender;
            if (session != null)
            {
                // используем сессию
                session["dssVerify"] = true;
            }
        }

Реализация контролов.

Для начала сделаем родителя обоих контролов. Его основные задачи следующие:

1. Обеспечение возможности задать Template
Делаем так

[TemplateContainer(typeof (AdministrationData)), TemplateInstance(TemplateInstance.Single)]
        public virtual ITemplate Template { get; set; }

2. Добавление на страницу объекта для работы с Рутокен WEB
Работа с браузерным плагином сводится к вызовам методов специально объявленного object. Объявляется в таком формате:

<object id="cryptoPlugin" type="application/x-rutoken" width="0" height="0"></object>

для этого в onLoad контрола делаем

  private void EnsureRutokenPlugin()
        {

            var rtObject = new HtmlGenericControl("object") {ClientIDMode = ClientIDMode.Static, ID = JStokenObjectID};
            rtObject.Attributes.Add("type", "application/x-rutoken");
            rtObject.Attributes.Add("width", "0");
            rtObject.Attributes.Add("height", "0");

            var rtParam = new HtmlGenericControl("param") {TagName = "onload"};
            rtParam.Attributes.Add("value", "pluginit");
            rtObject.Controls.Add(rtParam);

            // ищем контрол с возможностью добавить и кидаем объект туда
            bool bControlAdded = false;
            if (Page.Form == null)
            {
                throw new Exception("define 'Form' tag on page!");
            }
            foreach (PlaceHolder control in Page.Form.Controls.OfType<PlaceHolder>())
            {
                (control).Controls.Add(rtObject);
                bControlAdded = true;
                break;
            }
            if (!bControlAdded)
            {
                throw new Exception("define an empty 'PlaceHolder' tag after the tag 'Form'");
            }

            // объект токена
            Utils.IdToJavaScript(rtObject, JScontrolVar, "token", Page);

            // объект с настройками
            Page.ClientScript.RegisterStartupScript(typeof(Control), "settings",
                string.Format("{0}.settings = {{}}; {0}.settings.mainurl = '{1}/rutokenweb/ajax.rtw';",
                JScontrolVar, HttpContext.Current.Request.ApplicationPath)
                , true);
        }

Здесь есть одна особенность. Объект плагина не должен находиться в скрытом элементе, у которого display:none; на пример, а то работать отказывается. А размещать мы его будем в PlaceHolder-е, который специально для этого объявим на основной странице с нашими контролами. Если используется masterpage, то на ней, причем сразу за тэгом Form.

    <form id="form1" runat="server">
    <asp:PlaceHolder ID="tokenPlaceHolder" runat="server"></asp:PlaceHolder>

Это и позволит избежать непреднамеренного попадания объекта токена в скрытый элемент страницы.

Теперь займемся реализацией контролов – наследников. Один для управления токенами, второй для аутентификации клиентов. Контролы templated, так что нужно им задать разметку на странице, причем в шаблоне обязательно должны присутствовать определенные элементы разметки с определенными именами. Кнопки, надписи и т.д. Наличие проверяется в коде.
В обоих контролах переопределим CreateChildControls:

protected override void CreateChildControls()
        {
            if (Template != null) // задан темплэйт
            {
                Controls.Clear();


                administrationData = new AdministrationData();
                Template.InstantiateIn(administrationData);
...

Дальше в методе найдем кнопки, таблицы и прочее, выставим им свойства если надо. Например кнопка привязки токена:

var rtwConnect = (Button)administrationData.FindControl("rtwConnect");

а также добавим на страницу переменные – указатели на эти dom-объекты, как свойства глобальной javascript переменной $grd_ctrls

IdToJavaScript(rtwConnect, JScontrolVar, "rtwConnect", Page);

    public static void IdToJavaScript(Control ctrl, string jsvar, string field, Page page)
        {
            page.ClientScript.RegisterStartupScript(typeof (Control), field,
                                                    jsvar + "." + field + " = rtwGID('" + ctrl.ClientID +
                                                    "');  ", true);
        }

Итого, у нас будет нужная разметка и ссылки на эти элементы разметки как свойства $grd_ctrls.

Рассмотрим шаблоны контролов:

Administration

Разметка у этого контрола достаточно громоздкая. Зато все данные есть.

<token:Administration ID="backoffice" runat="server" Port="12345">
  <template>
            <label>Список токенов:</label>
              <asp:GridView runat="server" ID="rtwEnable" CssClass="OrdersGr" AutoGenerateColumns="False" GridLines="None" ShowHeaderWhenEmpty="False">
                   <EmptyDataTemplate>
                        Нет привязанных токенов
                   </EmptyDataTemplate>
                   <Columns>
                     <asp:TemplateField HeaderText="Token Id">
                       <ItemTemplate>
                           <%# ((uint)Container.DataItem) %>
                       </ItemTemplate>
                     </asp:TemplateField>
                     <asp:TemplateField HeaderText="Активен">
                        <ItemTemplate>
                          <asp:Label ID="rtwEnabledToken" runat="server"></asp:Label>
                        </ItemTemplate>
                     </asp:TemplateField>
                    <asp:TemplateField HeaderText="Управление">
                       <ItemTemplate>
                           <asp:Button runat="server" ID="rtwEnableSwitch" OnClientClick="return false;"/>
                            <asp:Button ID="rtwRemove" runat="server" Text="Отвязать токен" OnClientClick="return false;" ClientIDMode="Predictable"/> 
                        </ItemTemplate>
                     </asp:TemplateField>
                   </Columns>
                </asp:GridView>
    <br />
 <label>Связка с Рутокен Web:</label>
 <asp:Button ID="rtwConnect" runat="server" Text="Привязать токен"/>                         
    <br />
  <asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="~/ajax_loader.gif" />
   <br />
  <asp:Label ID="rtwErrorMessage" runat="server" CssClass="errLabel" />
   <asp:Label ID="rtwMessage" runat="server" CssClass="status ok" />
 </template>
</token:Administration>

В принципе, здесь, всего лишь, таблица с токенами, кнопки привязки, отвязки и переключения токенов, а также два span – для информационных сообщений и сообщений об ошибках.
Данные для таблицы токенов отдает метод интерфейса ITokenController GetUserTokens

 // List<uint> GetUserTokens(string login);
 rtwEnable.DataSource = m_tokenProcessor.GetUserTokens(m_tokenProcessor.GetUserName());
 rtwEnable.DataBind();
Login, Remember

Контрол для аутентификации или восстановления доступа. Восстановление возможно без использования токена, нужно ввести свой логин и код восстановления, указанный на карточке Рутокен WEB (поставляется в комплекте с токеном)
Пример разметки логина:

<aktivlogin:Login ID="tokenlogin" runat="server" LoginType="Login">
     <Template>
        <asp:Literal ID="rtwUsers" runat="server" />
        <asp:Label ID="rtwErrorMessage" runat="server" CssClass="rutoken error" style="display: block; color: #c00;" />
        <asp:Label ID="rtwMessage" runat="server" CssClass="rutoken message" style="display: block; color: green;" />
         <asp:Button ID="rtwLogin" runat="server" OnClientClick="return false;" Text="Войти" style="margin-top:12px;" />
          <asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="~/ajax_loader.gif" />
     </Template>                    
</aktivlogin:Login> 

Здесь есть контрол типа Literal, который в результате будет выдавать select. Можно было бы использовать DropDownList, но в селект мы будем javascript-ом добавлять список логинов на токене и если будет postback, EventValidation страницы ругнется. Чтобы не выключать его, нарисуем select сами.

rtwUsers.Text = "<select id="rtwUsers"></select>";

Пример разметки восстановления доступа:

<aktivlogin:Login ID="tokenlogin" runat="server" LoginType="Remember">
       <Template>
          Логин: <asp:TextBox ID="rtwRepairUser" runat="server" /><br />
          Код восстановления: <asp:TextBox ID="rtwRepair" runat="server" /><br />
          <asp:Label ID="rtwErrorMessage" runat="server" style="display: block; color: #c00;" />
          <asp:Label ID="rtwMessage" runat="server" style="display: block; color: green;" />
           <asp:Button ID="rtwRepairBtn" runat="server" OnClientClick="return false;" Text="Войти" style="margin-top:12px;" />
           <asp:Image ID="rtwAjaxImg" runat="server" ImageUrl="/ajax_loader.gif" />
       </Template>                    
</aktivlogin:Login> 

Как видно, они отличаются указанием LoginType = Login или Remember.

Javascript

Основной javascript расположен в tokenadmin.js, его отдает хэндлер. Скрипт связывает элементы пользовательского интерфейса, плагин и сервер.
Элементы интерфейса привязаны к свойствам глобальной переменной $grd_ctrls, привязываем в коде контролов, помещая переменные на страницу с помощью page.ClientScript.RegisterStartupScript. Объект плагина — $grd_ctrls.token.

Tokenadmin.js делает следующее: в начале проверяем, доступен ли плагин и есть ли токен(если это логин). Затем делаем обработку запросов пользователя с колбэками. Например, при аутентификации сначала скрипт считывает все логины на токене и добавляет их в select (rtwUsers).

var containerCount = g.token.rtwGetNumberOfContainers();
  for (i = 0; i < containerCount; i++) {
                    var contName = g.token.rtwGetContainerName(i);
                    g.rtwUsers.options[i] = new Option(contName.replace("#%#", " - "), contName);
                }

Пользователь выбирает нужный логин и жмет кнопку «Войти».
Посылаем на сервер запрос с командой rnd и id токена. Если все ок, получаем в ответ json вида
{«text»:«94156e9a6642d42a47fc94c6f4b1b8c000dab4bfd24f321f5976e4d3a5a4e994»,«type»:«Notify»}
Это сгенерированная сервером последовательность, к которой по алгоритму нам надо прибавить свои случайные данные. Колбэк функция генерирует эти данные, делает конкатенацию с тем что прислал сервер, считает хэш и подписывает в плагине браузера. Подпись данных требует ввода пинкода. Пользователь вводит пин. Если все ок и пин корректен, отправляем на сервер подпись и случайные данные. Сервер производит конкатенацию строк и проверку подписи. Если подпись верна, получаем ответ:
{«text»:«True»,«type»:«Notify»,«url»:"/RutokenWebSTS/Admin/"}
Вместе с ответом должна приехать и аутентификационная cookie, поэтому делаем редирект пользователя на присланный url. Аутентификация пройдена.

Проверка подписи

Всю криптографию вынес в отдельную dll. Наружу смотрят три метода:

  • Генерация случайного хэша
  • Вычисление хэша строки
  • Проверки подписи

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

И в заключение короткая инструкция по сборке.

(.net 4.0, тестировалось под iis 7.5)

1. Добавить сборки RutokenWebPlugin.dll и Rutoken.dll в проект
2. Добавляем httpHandler в Web.config

    <system.webServer>
       <handlers>
            <add name="AjaxHandler" path="/RutokenWebSTS/rutokenweb/ajax.rtw"  verb="*" type="RutokenWebPlugin.TokenAjaxHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode" />
        </handlers>
    </system.webServer>

Path обязательно должен заканчиваться на '/rutokenweb/ajax.rtw'. Если сайт/приложение установлено в виртуальный каталог, как в примере выше, включите его в путь.

И при необходимости надо сделать хэндлер доступным всем

    <location path="rutokenweb">
        <system.web>
            <authorization>
                <allow users="*" />
            </authorization>
        </system.web>
    </location>

3. Реализуем интерфейс ITokenProcessor

    public class CustomTokenProcessor : ITokenProcessor 
    {
      …..

Самый ответственный момент, пример реализации с комментариями есть в исходниках
4. Добавляем контрол для управления токенами (личный кабинет)

<%@ Register TagPrefix="token" Namespace="RutokenWebPlugin" Assembly="RutokenWebPlugin" %>

И шаблон контрола (пример шаблона был в статье)

<token:Administration ID="backoffice" runat="server" Port="12345" >
                                        <Template>
…..

Порт указываем, если приложение работает не на 80 порту.

5. В codebehind контрола администрирования добавляем объект, реализующий ITokenProcessor

    protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
	     // CustomTokenProcessor : ITokenProcessor
            var processor = new CustomTokenProcessor();

            // метод процессора покажет данные токенов на странице
            backoffice.TokenProcessor = processor;

            // объект станет доступен хэндлеру
            backoffice.SetRequired(processor, "/");
        }

6. Добавляем контрол для аутентификации на страницу логина

<%@ Register TagPrefix="aktivlogin" Namespace="RutokenWebPlugin" Assembly="RutokenWebPlugin" %>

и его шаблон

   <aktivlogin:Login ID="tokenlogin" runat="server" SuccessUrl="http://localhost/Secured/" LoginType="Login">
                    <Template>
…….

8. В Codebehind контрола с логином добавляем объект, реализующий ITokenProcessor

protected override void OnInit(EventArgs e)
        {
            	base.OnInit(e);

		// returnurl определяет урл, на который переходим после аутентификации
               tokenlogin.SetRequired(new CustomTokenProcessor(), returnurl);
        
         }

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

Приведенный пример не составит труда доработать под свои нужды, либо можно использовать библиотеку с проверкой подписи и сделать все «с нуля».

Исходники примера с тестовым сайтом и скриптом для базы данных можно скачать здесь

Автор: ac_dev

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js