AVK Selected

Показавшиеся интересными, на мой вкус, посты

Настройка федеративной аутентификации в WCF

Михаил Романов Михаил Романов
Сергей, добрый день.

Что ж, я в принципе разобрался со всеми сложностями и напастями, что мне встречались и теперь могу рассказать, что же у меня получилось.
На текущий момент, у меня готов первый вариант, из тех, что я описывал, т.е. практически всю работу на себя берет WCF (правда в .Net 4.5 работа так или иначе идет с использованием WIF, но от вас лично не потребует ни строчки кода для WIF — разве что вы начнете делать какие-то кастомные расширения).

Для начала, чтобы у нас было единое понимание того, что происходит, позволю себе небольшое описание процесса (см. картинку):
http://files.rsdn.org/29231/Common_process_25012014.png
0. Пользователь вводит свои UserName и Password (U/P)
1. Клиент пытается подключиться к WCF Service используя переданные ему U/P. Однако, инфраструктура WCF, анализирует указанные ей настройки и выясняет, что от нее требуется работать по Federation протоколу, а это значит, что вместо прямого обращения WCF Service, она формирует специальный Security Token Request (STR), который отсылает на STS. При этом в качестве credentials в SRT используются наши U/P.
2. STS получает STR и первым делом анализирует credentials (надо сказать, что в общем случае это не обязательно должны быть именно UserName/Password, а любой допустимый способ аутентификации Windows, Certificate, … — вплоть до полученного ранее сертификата! Но мы рассматриваем простейший случай).
Получив и аутентифицировав STR, наш STS начинает его анализировать и обрабатывать.
Делает он это следующим образом:
  • смотрит, для какого именно сервера будет выдаваться токен (в терминологии WIF такой сервер-получатель токена называется Relying Party, RP). Если в списке его доверенных такого сервера то формируется ошибка и токен не выдается
  • извлекает из своей базы данные пользователя (пользователя он берет из переданных credentials)
  • генерирует ключи симметричного шифрования для клиента и сервера — этими ключами будет защищаться канал/сессия между клиентом и RP
  • формирует токен, для RP (состоящий из данных пользователя и ключа шифрования), подписывает его своим закрытым ключом и шифрует открытым ключом RP.
  • затем, зашифрованный пакет вкладывает в еще один пакет, предназначенный уже для клиента (там могут быть те же данные, но обычно там только шифрования для клиента). Этот пакет тоже подписывается
  • результат возвращается клиенту (Security Token Response, STRes)
3. Клиент, получивший STRes проверяет подпись STS, достает свои данные и зашифрованный пакет для RP и уже с этим пакетом делает запрос на установление соединения с WCF Service
4. WCF Service получает запрос, достает оттуда зашифрованный пакет, расшифровывает его своим закрытым ключом и проверяет подпись STS. Если все в порядке, то устанавливается сессия с клиентом, а данные из токена используются как данные аутентифицированного пользователя.

Примерно так выглядит общий процесс (я тоже владею далеко не всем предметом в совершенстве, так что местами мог слегка и наврать).
Если мы его внимательно просмотрим, то получится, что для успешного решения нам желательно выполнить следующее:
  • Создать и правильно прописать SSL сертификаты для STS и WCF Service (если они на разных машинах, если на одной — хватит и одного).
  • Создать и положить в хранилище сертификат для подписания. Thinktecture IdentityServer по умолчанию берет такие сертификаты из Local Machine / My, поэтому кладем туда. Обратите внимание, что для сертификата надо дать права на доступ к закрытым ключам ученой записи под которой запущен STS (самое простое — дать права IIS_IUSRS, хотя это и не совсем правильно)
  • Аналогично создать и положить в хранилище сертификат для шифрования. Только теперь это нужно сделать на машине WCF Service + на базе этого сертификата сделать файл сертификата только с открытым ключом (.cer)

Теперь перейдем к настройке STS.
Тут нужно не забыть сделать следующее:
  • Имхо, в разделе "General Configuration" лучше снять галочку "Only Users in the 'IdentityServerUsers' role can request tokens:" (иначе всех пользователей придется добавлять в эту группу)
  • В разделе "Key Configuration" указать наш сертификат для подписи в Signing Certificate
  • В Разделе Protocols, проверьте, что включен WS-Trust (нам нужен только он), а затем зайдите в сами настройки WS-Trust и проверьте что стоит галочка "Enable Mixed Mode Security Endpoints" (это так ребята обозначают возможность аутентификации по User/Password на STS
    http://files.rsdn.org/29231/wstrust_config_25012014.png
  • В разделе "Relying Parties & Resources" создать новую запись об RP. Обратите внимание, здесь в качестве Encrypting Certificate нужно указать тот самый .cer который мы создали ранее на основе сертификата шифрования для WCF Service. В качестве Realm указывайте адрес сервиса в том виде, как он указывается в настройках WCF.
    http://files.rsdn.org/29231/RP_config_25012014.png
  • Ну и конечно, создать пользователя, которого мы будем аутентифицировать.

Все, на этом настройки STS закончены.

Однако, прежде чем перейти к настройкам WSF Service, сделаем следующее: выйдем на страницу Home (она в корне нашего STS или можно через закладки), а оттуда на Application integration. На этой странице есть 2 важных для нас адреса: "WS-Trust metadata" и "WS-Trust mixed mode security (user name)" — сами мы будем использовать только 1-ый, а второй чисто для проверки, что все метаданные верно подхватились.

Переходим к нашему WCF Service!
Здесь все делается в настройке, поэтому я просто приведу config с комментариями:
<?xml version="1.0"?>
<configuration>
  <configSections>
    <!-- Обязательно указать эту секцию!!! Почему ее нет по умолчанию в machine.confug - ума не приложу -->
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
  </configSections>

  <system.serviceModel>
    <bindings>
      <ws2007FederationHttpBinding>
        <binding name="Default">
          <!--    Мы используем TransportWithMessageCredential безопасность, чтобы не заморачиваться с настройками,
                так как SSL мы уже настроили.
                В качестве issuerMetadata нужно указать адрес, который содержится на странице Application integration
                и называется "WS-Trust metadata"
          -->
          <security mode="TransportWithMessageCredential">
            <message>
              <issuerMetadata address="https://мой_STS/IdSrv/issue/wstrust/mex" />
            </message>
          </security>
        </binding>
      </ws2007FederationHttpBinding>
    </bindings>
    
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- Это чтобы не возиться с настройкой клиента... -->
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
          <!-- Здесь мы указываем, что в pipeline WCF будет работать WIF, конфигурация которой (DefaultIConf) описывается ниже-->
          <serviceCredentials identityConfiguration="DefaultIConf" useIdentityConfiguration="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    
    <services>
      <!-- Ну и сам сервис. Т.к. хостим под IIS, адрес не указываем, binding и contract - обязательно -->
      <service name="WcfService.Service">
        <endpoint binding="ws2007FederationHttpBinding" bindingConfiguration="Default" contract="WcfService.Service" />
      </service>
    </services>
  </system.serviceModel>

  <!-- Здесь указываются все настройки WIF, т.е., собственно, все настройки обработки токенов -->
  <system.identityModel>

    <!-- Это требование MS в WIF - создавать и использовать отдельные конфигурации -->
    <identityConfiguration name="DefaultIConf">
      <securityTokenHandlers>

        <securityTokenHandlerConfiguration>
          <!-- Здесь мы указываем, что не будем проверять какому именно серверу предназначен пришедший токен
                если все же хочется проверять, то нужно будет добавить к узлу audienceUris дочерний.
                Что-то типа <add value="https://мой_хост/WcfService/Service.svc"/> - главное следить, чтобы он совпадал 
                с тем, что указано в поле Realm/Scope Name на STS
          -->
          <audienceUris mode="Never" />

          <!--    Здесь указывается класс, который будет для пришедшего токена искать закрытый ключ для расшифровки.
                К сожалению, я не нашел как для этого параметра указать настройку (т.е. в каком хранилище искать сертификаты)
                это можно сделать, если задавть настройку программно (например, делать свой наследник текущего класса и там 
                в конструкторе заполнять нужные параметры, а здесь указывать наш класс).
                Однако по умолчанию этот класс ищет сертификаты в Local Machine / My так что просто положим его туда
          -->
          <serviceTokenResolver
              type="System.IdentityModel.Tokens.X509CertificateStoreTokenResolver, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>

          <!--    Этот тэг задает список доверенных STS, т.е. тех от которых мы готовы получать токены.
                Опознаем мы их довольно просто - по thumbprint сертификата, которым они ставят подпись
                Информация о сертификате подписи добавляется в саму подпись, поэтому ничего регистрировать 
                дополнительно не надо, надо только указать строку thumbprint (имя произвольное)
          -->
          <issuerNameRegistry>
            <trustedIssuers>
              <add name="STS" thumbprint="9B14A55B4A435YYY8C88D14779F57193905C2"/>
            </trustedIssuers>
          </issuerNameRegistry>

        </securityTokenHandlerConfiguration>

      </securityTokenHandlers>
    </identityConfiguration>
  </system.identityModel>

</configuration>


Теперь переходим к клиенту.
Здесь тоже ничего специально делать не надо, просто создать ServiceReference для нашего WCF Service.

После создания перепроверьте, что все настройки соединений подхватились правильно.
Я привожу свой с комментариями, на что обратить внимание.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <system.serviceModel>
    <bindings>
      <!-- !!! Должно быть 2 биндинга: ws2007FederationHttpBinding и ws2007HttpBinding  !!! -->
      <ws2007FederationHttpBinding>
        <binding name="WS2007FederationHttpBinding_Service">
          <security mode="TransportWithMessageCredential">
            <message>
              <!-- !!! Обратите внимание на адрес. Он должен совпадать с "WS-Trust mixed mode security (user name)" 
                  на странице Application integration  !!! 
              -->
              <issuer address="https://мой_STS/IdSrv/issue/wstrust/mixed/username"
                  binding="ws2007HttpBinding" bindingConfiguration="https://мой_STS/IdSrv/issue/wstrust/mixed/username" />
              <issuerMetadata address="https://мой_STS/IdSrv/issue/wstrust/mex" />
              <tokenRequestParameters>
                <trust:SecondaryParameters xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
                  <!-- Тут я выпущу кусок -->
                </trust:SecondaryParameters>
              </tokenRequestParameters>
            </message>
          </security>
        </binding>
      </ws2007FederationHttpBinding>

      <ws2007HttpBinding>
        <!--  !!! Модель безопасности должна быть TransportWithMessageCredential
              а  clientCredentialType должен стоять в UserName  !!! -->
        <binding name="https://мой_STS/IdSrv/issue/wstrust/mixed/username">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None" />
            <message clientCredentialType="UserName" establishSecurityContext="false" />
          </security>
        </binding>
      </ws2007HttpBinding>
    </bindings>

    <client>
      <!-- Ну а тут просто адрес наших WCF Service и указание биндинга с контрактом -->
      <endpoint address="https://мой_WCF_Service/WcfService/Service.svc"
          binding="ws2007FederationHttpBinding" bindingConfiguration="WS2007FederationHttpBinding_Service"
          contract="ServiceReference.Service" name="WS2007FederationHttpBinding_Service" />
    </client>
  </system.serviceModel>
</configuration>


Ну все, настройка закончена

Теперь пишем пробный код клиента
(создаем proxy и указываем User/Password в качестве credentials)
using (var client = new ServiceReference.ServiceClient())
{
    client.ClientCredentials.UserName.UserName = "User1";
    client.ClientCredentials.UserName.Password = "123456";

    Console.WriteLine(client.GetData());
}


Ну и осталось привести код сервиса
Там ничего сложного, просто показываю как добраться до переданных Claims. В отличие от WIF 3.5, WIF 4.5 не меняет Principal текущего потока (хотя может где-то и есть такая опция), а предлагает как и в целом в WCF все получать через OperationContext

namespace WcfService
{
    [ServiceContract]
    public class Service
    {
        [OperationContract]
        public string GetData()
        {
            var princinpal = OperationContext.Current.ClaimsPrincipal;
            var name = princinpal.Identity.Name;
            var email = princinpal.FindFirst(ClaimTypes.Email);
            var webPage = princinpal.FindFirst(ClaimTypes.Webpage);

            return string.Format("User: {0}, mail: {1}, web page: {2}", name, 
                email == null ? "" : email.Value,
                webPage == null ? "" : webPage.Value);
        }
    }
}



Надеюсь, все получилось достаточно доходчиво.
Если останутся вопросы — спрашивайте без смущения (если я не буду долго отвечать — спросите в личку, я не постоянно читаю RSDN, а так должно прийти уведомление).

По поводу второго варианта (отдельное получение токена и затем вручную добавление его в вызовы сервисов в качестве Credentials) — он тоже должен быть возможен (мы именно так и работаем), но с ходу накидать пример на WCF/WIF 4.5 я не смогу, а больше пока времени на исследования, увы, не осталось.
WSA
WSA Вариант №1 "Чистый" WCF
25.01.2014 03:00
Приветствую Михаил.

С ума сойти! Сейчас бум курить...
Михаил Романов
Михаил Романов
27.01.2014 07:52
Здравствуйте, WSA, Вы писали:

WSA>Сейчас бум курить...


Потом отпишите о результатах, по возможности.
Я постараюсь в течение недели выделить время и сделать еще один пример — с явным получением токена и дальнешей работой через него. Но обещать не буду — времени не хватает ни на что, как обычно.
WSA
WSA
27.01.2014 08:30
Михаил, отправил вам личное сообщение, но не понял тут как его судьбу просдедить. Вы получили что-то от меня?
WSA
WSA Вариант №1 "Чистый" WCF
27.01.2014 11:39
Приветствую Михаил,

Проект с сервисом создаю по шаблону VS2012: WCF Service Application.
После редактирования конфига с данными, приведёнными в статье для WCF сервиса и попытки обратиться к сервису через браузер, возникает ошибка
"Could not find a base address that matches scheme https for the endpoint with binding WS2007FederationHttpBinding. Registered base address schemes are [http]."

Что я делаю не так?
Михаил Романов
Михаил Романов
27.01.2014 12:22
Здравствуйте, WSA, Вы писали:

WSA>Что я делаю не так?

Обычно такая ошибка возникает, когда ост сервиса не может найти https среди доступных ему биндингов сайта (не биндингов WCF, а биндингов IIS-сайта!!!).
На сколько я помню, по умолчанию проект WCF Application хостится под IIS Express, у которого по умолчанию же SSL не настроен.

К сожалению, при всей удобстве работы с IIS Express, в данном конкретном случае я рекомендую переключиться на полноценный IIS — как минимум там проще и нагляднее настраивать SSL.
WSA
WSA
27.01.2014 12:26
МР>К сожалению, при всей удобстве работы с IIS Express, в данном конкретном случае я рекомендую переключиться на полноценный IIS — как минимум там проще и нагляднее настраивать SSL.

К сожалению в моём случае не получится перейти на IIS, т.к. используются self-hosted WCF — сервисы, которые крутятся в windows — службе.
В данном контекстк, видимо нужно будет укказать более явно адрес конечной точки... А сертификат руками привязать к порту?
WSA
WSA Вариант №1 "Чистый" WCF
30.01.2014 07:58
Михаил, здравствуйте.

Кажется получается.

2 Сервиса, натсроенные с той же конфигурацией, что приведена в вашем описании вполне себе аутентифицируют клиента по переданному токену, без указания пароля.
Обращаюсь из клиента примерно так:


public void RequestSecuredWCFOne(GenericXmlSecurityToken token){
            using (var cf = new ChannelFactory<IService2>("WS2007FederationHttpBinding_IService2"))
            {
                cf.Credentials.UseIdentityConfiguration = true;
                var proxy = cf.CreateChannelWithIssuedToken(token);

                var data = proxy.GetData(1);
                Console.WriteLine(data);

                ((IDisposable)proxy).Dispose();
            }
        }



Токен получается заранее на клиенте прямо с STS.
Я пока не знаю как поведёт себя эта схема если будут разные сертификаты или адреса RP... тут много вариантов. Сейчас займусь этим исследованием. Но самое главное, что есть сильная подвижка.
Благодарю Вас за помощь. Но всё равно с нетерпением буду ждать второй части статьи. У вас здорово получается, уверен, можно будет многое из неё почерпнуть!

С уважением,
Сергей
Михаил Романов
Михаил Романов
30.01.2014 11:03
Здравствуйте, WSA, Вы писали:

WSA>Кажется получается.


Все, верно.
Просто у этого варианта нужно понимать, что:
1. В идеале, разным сервисам нужен разный токен.
Это не обязательное требование. По большому счету, чтобы воспользоваться токеном сервис должен уметь его расшифровать (т.е. у него должен быть сертификат с закрытым ключом) и проверить подпись STS. Еще WIF позволяет проверять кому назначен токен (там указывается URL сервиса), но эту проверку можно и отключить (<audienceUris mode="Never" />)
Когда все сервисы принадлежат вам, можно просто сделать везде единые настройки и забыть об этом.

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

2. Токен имеет ограничение по времени использования и время от времени его надо обновлять.

3. Смотря конечно для чего использовать токены...
Но вообще — это не только механизм аутентификации, но и способ для передачи разной полезной информации о пользователе: права, роли, личные данные (ФИО, должность, email, ...) которая хранится и обновляется централизованно. Поэтому логично хотябы часть этой информации получать, например, на клиенте.
Михаил Романов
Михаил Романов
30.01.2014 11:06
Здравствуйте, WSA, Вы писали:

WSA>Но всё равно с нетерпением буду ждать второй части статьи. У вас здорово получается, уверен, можно будет многое из неё почерпнуть!

Да не так уж и много — вы уже почти все нашли сами.
WSA
WSA Вариант №1 "Чистый" WCF
20.02.2014 11:10
Михаил, здравтсвуйте.
Нашёл тут одну полезную фичу, возможно вам будет полезно тоже.

МР>Ну и осталось привести код сервиса

МР>Там ничего сложного, просто показываю как добраться до переданных Claims. В отличие от WIF 3.5, WIF 4.5 не меняет Principal текущего потока (хотя может где-то и есть такая опция), а предлагает как и в целом в WCF все получать через OperationContext

МР>
МР>namespace WcfService
МР>{
МР>    [ServiceContract]
МР>    public class Service
МР>    {
МР>        [OperationContract]
МР>        public string GetData()
МР>        {
МР>            var princinpal = OperationContext.Current.ClaimsPrincipal;
МР>            var name = princinpal.Identity.Name;
МР>            var email = princinpal.FindFirst(ClaimTypes.Email);
МР>            var webPage = princinpal.FindFirst(ClaimTypes.Webpage);

МР>            return string.Format("User: {0}, mail: {1}, web page: {2}", name, 
МР>                email == null ? "" : email.Value,
МР>                webPage == null ? "" : webPage.Value);
МР>        }
МР>    }
МР>}
МР>




Когда в настройках WCF-сервиса мы укажем

<behaviors>
      <serviceBehaviors>
        <behavior>
          <!-- ... -->
          <serviceCredentials  useIdentityConfiguration="true"/>
<serviceAuthorization principalPermissionMode="Always"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>


в результате появляется возможность авторизировать доступ пользователя к операции WCF-сервиса через аттрибуты операции:


        [PrincipalPermission(SecurityAction.Demand, Role = @"admin")]
        public List<Items> GetItems()



В добавок System.Threading.Thread.CurrentPrincipal становится вместо WindowsPrincipal — ClaimsPrincipal'ом


С уважением, Сергей
Михаил Романов
Михаил Романов
20.02.2014 11:32
Сергей, добрый день

Да, существенное дополнение. Спасибо!

WSA>В добавок System.Threading.Thread.CurrentPrincipal становится вместо WindowsPrincipal — ClaimsPrincipal'ом

Меня это (что ребята отказались от выставления System.Threading.Thread.CurrentPrincipal, как это было в WIF 3.5 и поломали тем самым совместимость) — очень смущало. А оказалось, что я просто не досмотрел настройки.

WSA>в результате появляется возможность авторизировать доступ пользователя к операции WCF-сервиса через аттрибуты операции:

Конкретно у нас это оказалось не актуальным, но по причине того, что мы не смогли использовать стандартный claim для ролей — у нас чуть более сложная система прав и только плоским спиком ролей её не опишешь, пришлось использовать свои типы Claims, свои атрибуты разметки и писать свой менедежер авторизации. Впрочем, это оказалось совсем не сложно, зато на порядок гибче (что для нас было существеннее всего).

Но в любом случа — спасибо!

P.S. Есть не нулевая вероятность, что я таки доберусь (с подачи нашего учебного центра) до полноценного цикла статей/внутреннего курса по WIF (и ряду других тем), поэтому любые новые знания и находки очень к месту.