Аутентификация на 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, где объявлены специфичные для каждого конкретного сайта методы, нужные нашему решению. Например, получение открытого ключа, получение имени пользователя и прочее.

    Схематично все это выглядит так:


    Подготовка к аутентификации, как уже говорилось, сводится к формированию на Рутокен 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);
            
             }
    


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

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

    Исходники примера с тестовым сайтом и скриптом для базы данных можно скачать здесь
    • +19
    • 9,9k
    • 4
    «Актив» 60,74
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 4

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое