Приложения на основе заявок

Авторизация на основе заявок с помощью WIF

Мишель Леру Бустамант

Загрузка примера кода

За последние несколько лет модели федеративной безопасности и управления доступом на основе заявок (claims-based access control) стали заметно популярнее. В модели федеративной безопасности аутентификация может быть выполнена сервисом Security Token Service (STS), а STS может выдавать маркеры защиты с заявками (claims), которые подтверждают идентификацию аутентифицированного пользователя и его права доступа. При наличии федерации пользователи могут аутентифицироваться в своем домене и в то же время получать доступ к приложениям и сервисам, относящимся к другому домену, — при условии, что между этими доменами установлены доверительные отношения. Этот подход устраняет необходимость в создании и управлении дублирующимися учетными записями для каждого пользователя и обеспечивает поддержку сценариев применения с единым входом (single sign-on, SSO). Управление доступом на основе заявок занимает центральное место в модели федеративной безопасности, где приложения и сервисы авторизуют доступ к своим средствам и функциональности в соответствии с заявками от выпустивших их издателей (STS) в доверенных доменах. Заявки могут содержать информацию о пользователе, его ролях или разрешениях; это придает очень высокую гибкость модели авторизации. Федеративная защита в сочетании с доступом на основе заявок поддерживает множество вариантов интеграции между приложениями, отделами и партнерами в более крупную экосистему.

Инструментарий платформы в этой области тоже прошел долгий путь. Windows Identity Foundation (WIF) — полнофункциональная инфраструктура модели идентификаций, предназначенная для создания приложений и сервисов на основе заявок и поддержки вариантов с активной и пассивной федерацией. С помощью WIF вы можете без особых усилий поддерживать пассивную федерацию для любого ASP.NET-приложения и интегрировать в свои ASP.NET-приложения и WCF-сервисы модель авторизации на основе заявок. Более того, WIF предоставляет базовую инфраструктуру для реализации собственного STS и включает средства и элементы управления для поддержки вариантов аутентификации, при которых используются такие управляемые информационные карты и селекторы идентификаций, как Windows CardSpace.

WIF значительно уменьшает объем кода, необходимого для реализации полнофункционального приложения, которое использует федеративную и основанную на заявках защиту. В этой статье из двух частей я уделю основное внимание базовой функциональности инфраструктуры для поддержки пассивной федерации в ASP.NET-приложениях и моделей защиты на основе заявок как в WCF, так и в ASP.NET. В этой части статьи мы займемся WCF, а в следующей — ASP.NET.

Зачем нужна федеративная и основанная на заявках защита?

Преимущества федеративной и основанной на заявках защиты можно увидеть в контексте нескольких задач:

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

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

При проектировании приложений и сервисов модель аутентификации и авторизации является частью этих проектов. Так, в приложении интрасети обычно предполагается, что пользователи будут аутентифицироваться в конкретном домене по своим учетным записям в Windows, а в приложении для Интернета, как правило, применяется собственное хранилище учетных данных под управлением, например, Microsoft SQL Server. Приложения также могут требовать аутентификации на основе сертификатов или смарт-карт или поддерживать несколько типов удостоверений защиты, чтобы различные группы пользователей могли применять подходящий тип. Если в приложении всегда предполагается аутентификация пользователей только с одним типом удостоверений, ваша работа проста. Однако гораздо чаще набор удостоверений, поддерживаемых приложением, может постепенно расширяться для введения альтернативных режимов аутентификации или дополнительных режимов.

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

В том же духе приложения получают более высокую гибкость, если авторизация не увязана с фиксированным набором ролей. Если ваше приложение всегда полагается в авторизации доступа на определенный набор ролей и если эти роли всегда подразумевают одно и то же в плане прав доступа к средствам и функциональности, то вам опять повезло. Но смысл ролей часто варьируется в зависимости от отделов, в которых используется приложение, и поэтому необходимо соответствующая настройка. Это может потребовать разной интерпретации ролей в зависимости от домена пользователя или разрешения на создание собственных ролей для управления правами доступа. WIF упрощает введение модели защиты на основе заявок, чтобы вы могли отделить роли (если это допустимо) от механизма авторизации. Тогда логические роли можно будет преобразовывать в более гибкий набор заявок, и приложение будет авторизовать доступ на основе этих заявок. Если изменение или создание новых ролей потребует выдачи другого набора заявок, на само приложение это не повлияет.

Конечно, заявки — это нечто большее простых ролей или разрешений. Одно из дополнительных преимуществ работы с моделью на основе заявок состоит в том, что заявка может нести информацию об аутентифицированном пользователе, в частности адрес электронной почты, полное имя, дату рождения и др. Заявки также позволяют проверять информацию без ее открытия третьим сторонам, например реальный возраст пользователя (многие пользователи не хотят, чтобы такие сведения были общедоступны). В заявке может быть указано, что пользователь по крайней мере достиг того возраста, начиная с которого разрешается выполнять некую операцию (булева заявка, указывающая IsOver21 или IsOver13).

Хотя отделение механизма аутентификации и специфических ролей от приложений и сервисов упрощает внесение изменений, модель на основе заявок также занимает центральное место в сценариях с федеративной защитой, в которых пользователям, относящимся к любому доверенному домену, намного легче получать соответствующие права доступа. Объединение в федерацию сокращает издержки и устраняет некоторые риски, связанные с управлением идентификациями. Оно исключает необходимость в поддержке учетных данных пользователей между несколькими приложениями или доменами, а это избавляет от рисков, связанных с созданием и удалением учетных записей во множестве доменов, например ИТ-персонал может забыть удалить учетную запись в одном из доменов. Синхронизация паролей тоже перестает быть проблемой. Кроме того, объединение в федерацию упрощает поддержку SSO, так как пользователи могут входить в одно приложение и получать доступ к другому (даже размещенному в другом домене безопасности) без необходимости повторной аутентификации. Наконец, применение платформ с федеративной безопасностью, например Active Directory Federation Server (ADFS) и WIF, также упрощает добавление новых доверительных отношений между доменами. Таким образом, распространение приложения на дополнительные внутрикорпоративные домены или даже на внешние домены партнеров становится более прямолинейной задачей.

Активная федерация с применением WIF

Варианты с активной федерацией основаны на WS-Federation Active Requestor Profile (см.WS-Federation TC по ссылке oasis-open.org/committees/tc_home.php?wg_abbrev=wsfed) и спецификации WS-Trust (см.WS-Trust 1.3 по ссылке docs.oasis-open.org/ws-sx/ws-trust/200512/ws-trust-1.3-os.html). Если говорить в целом, то WS-Trust описывает контракт с четырьмя операциями сервиса: Issue, Validate, Renew и Cancel. Эти операции вызываются клиентами, чтобы соответственно запросить маркер защиты, проверить его, обновить истекший маркер или отменить тот, который больше не следует использовать. Каждой операции передаются сообщения в форме запроса маркера защиты (Request for Security Token, RST), на которые она реагирует в виде ответа на этот запрос (RST Response, RSTR) в соответствии со спецификацией WS-Trust. Эти средства WS-Trust реализуются STS (или издателем маркера) — важным участником в любом варианте федеративной защиты.

Простой вариант с активной федерацией показан на рис. 1. В нем задействованы клиентское Windows-приложение (запрашивающий), WCF-сервис [доверяющая сторона (relying party, RP)] и STS, принадлежащий домену RP (RP-STS). Как видно на иллюстрации, клиент использует WCF-прокси для аутентификации в RP-STS, запроса маркера защиты и вызова RP с передачей полученного маркера защиты.


Рис.1 Простой пример использования активной федерации

В этом примере RP-STS также является провайдером идентификаций (Identity Provider, IdP) для пользователей, аутентифицирующихся в домене RP. Это означает, что RPSTS отвечает за аутентификацию пользователей, назначение им идентификации и выдачу заявок, релевантных для RP, с целью авторизации. RP проверяет маркер защиты, выданный RP-STS, и авторизует доступ на основе выданных заявок.

Я создала приложение Todo List, чтобы было легче обсуждать реализацию в этом варианте. Сопутствующий пример кода включает WPF-клиент, WCF-сервис и активный STS, реализованный с помощью WIF. Чтобы предоставить последующий контекст, WCF-сервис TodoListService реализует контракт ITodoListService (рис. 2). Клиент вызывает сервис, используя WCF-прокси, чтобы получить все элементы Todo, а затем добавить, обновить или удалить какие-то элементы. При авторизации доступа к своим операциям TodoListService полагается на заявки create, read, update и delete.

Рис.2 Определение ITodoListService

[ServiceContract(Namespace="urn:TodoListApp/2009/06")]
public interface ITodoListService
{
    [OperationContract]
    List<TodoItem> GetItems();
    [OperationContract]
    string CreateItem(TodoItem item);
    [OperationContract]
    void UpdateItem(TodoItem item);
    [OperationContract]
    void DeleteItem(string id);
}

Для реализации этого варианта с активной федерацией вы должны придерживаться следующей схемы.

  1. Предоставьте конечную точку WCF для TodoListService с федеративной защитой.
  2. Сгенерируйте WCF-прокси для клиентского приложения и инициализируйте этот прокси удостоверениями для аутентификации в RP-STS.
  3. Включите WIF для TodoListService, чтобы разрешить авторизацию на основе заявок.
  4. Поместите запросы разрешений (IsInRole) или другие проверки авторизации для управления доступом к операциям сервиса или другой функциональности.

Все эти этапы мы обсудим в последующих разделах.

Предоставление федеративных конечных точек

WCF-сервисы на основе заявок обычно предоставляют федеративные конечные точки, которые принимают выданные маркеры, использующие, например, стандарт SAML. В WCF есть две привязки для поддержки федеративной защиты с применением WS-Trust. WSFederationHttpBinding — оригинальная стандартная привязка, основанная на WS-Trust 2005 (ранней версии этого протокола), а WS2007FederationHttpBinding — новая версия той же привязки (выпущена вместе с Microsoft .NET Framework 3.5) и поддерживает WS-Trust 1.3 (утвержденный стандарт). Как правило, вы должны использовать WS2007FederationHttpBinding, если только требования совместимости не заставляют использовать более раннюю версию. STS на основе ADFS версии 2 или WIF поддерживают любую версию WS-Trust.

Предоставляя федеративную конечную точку для сервиса, вы обычно включаете информацию об ожидаемом формате маркера защиты, обязательных и необязательных типах заявок, а также указываете доверенного издателя маркеров (token issuer). На рис. 3 приведен листинг system.serviceModel для TodoListService, который предоставляет единственную федеративную конечную точку через привязку WS2007FederationHttpBinding.

Рис.3 Федеративная конечная точка, предоставляемая TodoListService

<system.serviceModel>
  <services>
    <service name="TodoList.TodoListService" 
behaviorConfiguration="serviceBehavior">
      <endpoint address="" binding="ws2007FederationHttpBinding" bindingConfiguration="wsFed" contract="Contracts.ITodoListService" />
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost:8000/TodoListService"/>
        </baseAddresses>
      </host>
    </service>
  </services>
  <bindings>
    <ws2007FederationHttpBinding>
      <binding name="wsFed">
        <security mode="Message" issuedTokenType=
“http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-.1#SAMLV1.1" issuedKeyType="SymmetricKey" negotiateServiceCredential="true">
          <message>
            <claimTypeRequirements>
              <add claimType= 
“https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" isOptional="false"/>
              <add claimType= "urn:TodoListApp/2009/06/claims/permission" 
isOptional="false"/>
            </claimTypeRequirements>
            <issuerMetadata address="http://localhost:8010/rpsts/mex" />
          </message>
        </security>
      </binding>
    </ws2007FederationHttpBinding>
  </bindings>
  <behaviors>
    <serviceBehaviors>
      <behavior name="serviceBehavior">
        <serviceMetadata/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

В сценариях с федеративной защитой обычно полагаются на SAML-маркеры, хотя это не является обязательным требованием. В моем примере используются маркеры SAML 1.1, как указывает URI в issuedTokenType (docs.oasis-open.org/wss/oasis-wss-saml-token-profi le-1,0#SAMLV1.1). Для другого типа маркера вроде SAML 1.0 или SAML 2.0 используйте соответствующий URI. Конечно, STS, указанный в конфигурации федеративной привязки, должен поддерживать запрашиваемый вами тип маркера.

Прочие релевантные настройки в разделе message включают issuedKeyType и negotiateServiceCredential. Параметр issuedKeyType указывает, является ключ доказательства (см. blogs.msdn.com/vbertocci/archive/2008/01/02/onprooftokens.aspx) симметричным (по умолчанию) или асимметричным (увеличивает издержки). И вновь этот параметр должен быть совместим с STS. Если negotiateServiceCredential установлен в true, клиенту априори не нужен доступ к открытому ключу RP, но процесс согласования осуществляется по несовместимому протоколу. Если клиент не поддерживает WCF, вы должны установить negotiateServiceCredential в false. Но не волнуйтесь. Если этому параметру присвоено значение false, генерация прокси с помощью SvcUtil предоставит клиенту копию открытого ключа RP с кодированием по основанию base64.

Типы заявок, указываемые в разделе claimTypeRequirements, задают обязательные и необязательные типы, на которые сервис полагается при авторизации. В данном случае сервис ожидает заявку name, чтобы идентифицировать пользователя, и по крайней мере одну заявку permission — собственный тип заявки, который сообщает права пользователя в создании, чтении, обновлении или удалении элементов Todo. (Эти типы заявок перечислены на рис. 4.) Список типов заявок включается в метаданные сервиса, чтобы клиенты могли вставлять эту информацию в RST. Довольно часто STS знает, какие заявки он выдаст конкретной RP, а значит, в федеративной привязке делать этот список исчерпывающим не нужно.

Рис.4 Заявки, выдаваемые каждому пользователю в примере с приложением Todo List

Доверенный издатель маркера в данном примере — RP-STS, который реализован с применением WIF. RP-STS предоставляет единственную конечную точку WS-Trust с адресом http://localhost:8010/rpsts, а для обмена метаданными используется адрес http://localhost:8010/rpsts/mex. На рис. 3 адрес метаданных издателя указан в разделе issuerMetadata, так что, когда клиент генерирует прокси, он может распознавать доступные конечные точки STS.

Допустим, что STS должен предоставлять несколько конечных точек, скажем, для аутентификации пользователей из интрасети с учетными данными Windows по адресу http://localhost:8010/rpsts/internal и для аутентификации пользователей из Интернета с именем и паролем по адресу http://localhost:8010/rpsts/external. Сервис RP может указать конкретную конечную точку издателя, сопоставленную с его конфигурацией федеративной конечной точки, чтобы при генерации прокси клиентами эта конфигурация для взаимодействия с STS соответствовала заданной конечной точке, а не первой совместимой. Для этого вы сообщаете адреса для элементов issuerMetadata и issuer следующим образом:

<issuerMetadata address="http://localhost:8010/rpsts/mex" />
<issuer address="http://localhost:8010/rpsts/mex/external" />

Преимущество этого подхода — упрощение генерации прокси для клиентов при наличии выбора из нескольких конечных точек STS и необходимости для RP иметь возможность влиять на то, какая конечная точка будет использоваться. Если RP безразлично, через какую конечную точку аутентифицируется клиент, то лучше задать только параметр issuerMetadata и разрешить клиентскому приложению самому выбирать подходящую конечную точку для аутентификации.

Учтите:если в конфигурации сервиса опущен элемент issuerMetadata и указан только адрес issuer, этот адрес должен соответствовать логическому URI издателя (http://localhost:8010/rpsts/issuer), который не обязательно совпадает с физическим адресом конечной точки STS. При эквивалентной конфигурации на клиенте пользователю будет предложено выбрать управляемую информационную карту от того же издателя (через Windows CardSpace), и эта карта должна отвечать таким критериям, как формат запрошенного маркера и типы заявок. Подробнее о вариантах активной федерации с применением Windows CardSpace см. по ссылке wpfandcardspace.codeplex.com.

Генерация клиентского прокси

Когда вы генерируете прокси для Windows-клиента с помощью SvcUtil или Add Service Reference, для получения информации о конечных точках, предоставляемых издателем, используется адрес обмена метаданными издателя. Здесь возможно несколько вариантов.

  • Если в федеративной привязке для конечной точки сервиса RP указан адрес метаданных издателя без конкретного адреса издателя, клиентская конфигурация будет включать первую конечную точку STS, совместимую по протоколу, а любые другие совместимые конечные точки будут закомментированы для использования разработчиком клиента, если у него возникнет в том необходимость.
  • Если в федеративной привязке для конечной точки сервиса RP указан адрес метаданных издателя с конкретным адресом издателя, клиентская конфигурация будет включать этот конкретный адрес (при условии совместимости по протоколу).
  • Если в федеративной привязке для конечной точки сервиса RP указан только адрес метаданных, клиентская конфигурация будет включать лишь этот адрес без конфигурации привязки для издателя. То есть будет использоваться селектор идентификаций вроде CardSpace, о котором я уже упоминала.

Если клиент генерирует прокси для TodoListService, конфигурация которого показана на рис. 3, и STS предоставляет единственную конечную точку, то клиентская версия конфигурации WS2007FederationHttpBinding будет включать следующие параметры issuer и issuerMetadata:

<issuer address="http://localhost:8010/rpsts" 
        binding="ws2007HttpBinding" 
        bindingConfiguration="http://localhost:8010/rpsts">
  <identity>
    <certificate encodedValue="[base64 encoded RP-STS certificate]" />
  </identity>
</issuer>
<issuerMetadata address="http://localhost:8010/rpsts/mex" />

Заметьте, что элемент issuer указывает конечную точку издателя и необходимую конфигурацию привязки для взаимодействия с этой точкой. В данном случае клиент аутентифицируется в STS с именем и паролем, используя защиту на основе сообщений, как показано в следующей конфигурации WS2007HttpBinding:

<ws2007HttpBinding>
    <binding name="http://localhost:8010/rpsts" >
        <security mode="Message">
            <message clientCredentialType="UserName" 
                     negotiateServiceCredential="false" 
                     algorithmSuite="Default" 
                     establishSecurityContext="false" />
        </security>
    </binding>
</ws2007HttpBinding>

Конечная точка клиента сопоставляет конфигурацию федеративной привязки с конечной точкой RP:

<client>
  <endpoint address="http://localhost:8000/TodoListService" 
            binding="ws2007FederationHttpBinding" 
            bindingConfiguration="wsFed"
            contract="TodoList.ITodoListService" name="default">
    <identity>
      <certificate encodedValue="[base64 encoded RP certificate" />
    </identity>
  </endpoint>
</client>

При такой конфигурации клиентский прокси нужно лишь инициализировать правильным именем и паролем перед вызовом сервиса:

TodoListServiceProxy _Proxy = new TodoListServiceProxy("default");

if (!ShowLogin()) return;

this._Proxy.ClientCredentials.UserName.UserName = this.Username;
this._Proxy.ClientCredentials.UserName.Password = this.Password;
this._TodoItems = this._Proxy.GetItems();

Выдача маркеров

Прокси сначала предоставляет удостоверения для аутентификации в RP-STS, посылая RST с запросом маркера SAML 1.1, указывающего, что RP требует минимум двух заявок:name и permission. Пользователь аутентифицируется по хранилищу учетных данных в STS, и после успешной аутентификации ему выдаются соответствующие заявки. Затем прокси обрабатывает RSTR, содержащий выданный маркер, и передает этот маркер в RP, чтобы установить безопасный сеанс для аутентифицированного пользователя.

В данном примере STS был создан с применением WIF и аутентифицирует пользователей по собственному хранилищу учетных данных, выдавая заявки для каждого пользователя согласно рис. 4.

Заметьте, что STS на основе ADFS версии 2 аутентифицирует пользователей в Windows-домене и выдает заявки согласно вашей конфигурации ADFS. Собственный STS на основе WIF может выполнять аутентификацию, используя выбранное вами хранилище удостоверений, но вам потребуется написать свой код для управления хранилищем удостоверений и релевантным процессом сопоставления заявок.

Конфигурация модели идентификаций

Чтобы разрешить авторизацию на основе заявок для своего WCF-сервиса, используя WIF, вы инициализируете экземпляр ServiceHost для объединения в федерацию. Это можно сделать программно вызовом метода ConfigureServiceHost типа FederatedServiceCredentials:

ServiceHost host = new ServiceHost(typeof(TodoList.TodoListService));
FederatedServiceCredentials.ConfigureServiceHost(host);
host.Open();

или декларативно, используя расширения поведения ConfigurationServiceHostBehaviorExtension:

<serviceBehaviors>
  <behavior name="fedBehavior" > 
    <federatedServiceHostConfiguration/>
    <serviceMetadata />
  </behavior>
</serviceBehaviors>

В любом случае ServiceHost назначается экземпляр типа FederatedServiceCredentials, чтобы управлять поведением авторизации на основе заявок для сервиса. Этот тип можно инициализировать программно или через конфигурационный раздел microsoft.identityModel для данного сервиса. Параметры модели идентификаций специфичны для WIF и обеспечивают настройку авторизации на основе заявок в приложениях ASP.NET и WCF (большая часть элементов описывается на рис. 5).

Рис. 5 Важнейшие элементы microsoft.identityModel

Для WCF-сервисов, использующих WIF, вам больше не нужно инициализировать ServiceHost типичными WCF-поведениями аутентификации и авторизации. WIF заменяет их и обеспечивает более четкий способ настройки защиты в целом. (WIF полезна не только в сценариях на основе заявок и в федерациях.) На рис. 6 показаны настройки модели идентификаций для TodoListService.

Рис. 6 Настройки модели идентификаций, часто применяемые для WCF-сервисов

<microsoft.identityModel>
  <service>
    <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.
      ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, 
      Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
      <trustedIssuers>
        <add name="http://localhost:8010/rpsts" thumbprint=
"c3 95 cd 4a 74 09 a7 77 d4 e3 de 46 d7 08 49 86 76 1a 99 50"/>
      </trustedIssuers>
    </issuerNameRegistry>
    <serviceCertificate>
      <certificateReference findValue="CN=RP" storeLocation="LocalMachine" 
         storeName="My" x509FindType="FindBySubjectDistinguishedName"/>
    </serviceCertificate>
    <audienceUris mode="Always">
      <add value="http://localhost:8000/TodoService"/>
    </audienceUris>
    <certificateValidation certificateValidationMode="PeerTrust" />         
    <securityTokenHandlers>
      <remove type="Microsoft.IdentityModel.Tokens.Saml11.
         Saml11SecurityTokenHandler, Microsoft.IdentityModel, 
         Version=1.0.0.0, Culture=neutral, 
         PublicKeyToken=31bf3856ad364e35"/>
      <add type="Microsoft.IdentityModel.Tokens.Saml11.
         Saml11SecurityTokenHandler, Microsoft.IdentityModel, 
         Version=1.0.0.0, Culture=neutral, 
         PublicKeyToken=31bf3856ad364e35">
        <samlSecurityTokenRequirement >
          <roleClaimType 
            value="urn:TodoListApp/2009/06/claims/permission"/>
        </samlSecurityTokenRequirement>
      </add>
    </securityTokenHandlers>
    <claimsAuthorizationManager 
      type="TodoList.CustomClaimsAuthorizationManager, TodoList"/>
  </service>
</microsoft.identityModel>

Параметр issuerNameRegistry указывает любых издателей доверяемых сертификатов. Если вы используете ConfigurationBasedIssuerNameRegistry, как показано на рис. 6, то должны предоставить список издателей доверяемых сертификатов, указав их отпечатки (thumbprints). В период выполнения ConfigurationBasedIssuerNameRegistry проверяет маркеры защиты X509 по этому списку и отвергает те из них, чьи отпечатки отсутствуют в списке. Вы можете использовать SimpleIssuerNameRegistry, чтобы разрешить любые маркеры X509 или RSA, но скорее всего предпочтете предоставить собственный тип IssuerNameRegistry для проверки маркеров, который будет применять свою логику, если вас не устраивает ConfigurationBasedIssuerNameRegistry.

Конфигурация на рис. 6 отвергает любые маркеры, не подписанные RP-STS (с применением отпечатка сертификата для CN=RPSTS). В следующей конфигурации вместо этого указан собственный тип IssuerNameRegistry — TrustedIssuerNameRegistry:

<issuerNameRegistry type="TodoListHost.TrustedIssuerNameRegistry, TodoListHost"/>

Реализация TrustedIssuerNameRegistry используется для получения того же результата — отклонения маркеров, не подписанных CN=RPSTS; при этом проверяется имя субъекта (subject name) входящего маркера:

public class TrustedIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName(SecurityToken securityToken)
    {
        X509SecurityToken x509Token = securityToken as
            X509SecurityToken;
        if (x509Token != null)
        {
            if (String.Equals(x509Token.Certificate.SubjectName.Name,
                "CN=RPSTS"))
            {
                return x509Token.Certificate.SubjectName.Name;
            }
        }

        throw new SecurityTokenException("Untrusted issuer.");
    }
}

Содержимое serviceCertificate на рис. 6 указывает, что сертификат используется для расшифровки входящих маркеров защиты (в предположении, что все они шифруются для RP выпускающим их сервисом STS. В случае приложения Todo List сервис RP-STS шифрует маркеры по открытому ключу для RP, CN=RP.

Обычно SAML-маркер включает audienceURIs; его содержимое указывает на RP, для которого был выдан данный маркер. Вы можете явно отклонять маркеры, не предназначенные для отправки RP. По умолчанию режим audienceUris всегда устанавливается в Always, т. е. вы должны предоставлять минимум один URI для проверки входящих маркеров. На рис. 6 конфигурация разрешает только SAML-маркеры, которые включают URI получателя, подходящий адресу TodoListService. Вы можете установить режим audienceUris в Never (как правило, не рекомендуется), чтобы исключить проверку получателя для входящего SAML-маркера:

<audienceUris mode="Never"/>

Помните:когда клиент посылает RST в STS, он обычно включает элемент AppliesTo, указывающий, кому должен быть выдан маркер (в данном случае — RP). STS может использовать эту информацию для записи в SAML-маркер URI получателя.

Элемент certificateValidation контролирует, как проверяются входящие маркеры X509 (например, те из них, которые используются для подписей маркеров). На рис. 6 certificateValidationMode установлен в PeerTrust, т. е. сертификаты действительны, только если в хранилище TrustedPeople находится соответствующий сертификат. Для проверки издателя маркера это значение подходит больше, чем PeerOrChainTrust (по умолчанию), потому что требует от вас явной установки доверяемого сертификата в хранилище сертификатов. PeerOrChainTrust указывает, что подписи также проходят авторизацию, если корневой центр сертификатов (certificate authority, CA) является доверяемым, а на большинстве компьютеров список доверяемых CA весьма большой.

Теперь вкратце обсудим некоторые другие элементы, представленные на рис. 5 и рис. 6. Один из важных моментов в инициализации WIF — вместо инициализации в разделе microsoft.identityModel вы можете программно инициализировать экземпляр FederatedServiceCredentials и передать его в ConfigureServiceHost. Вот как выглядит такой код:

ServiceHost host = new ServiceHost(typeof(TodoList.TodoListService));

ServiceConfiguration fedConfig = new ServiceConfiguration();
fedConfig.IssuerNameRegistry = new TrustedIssuerNameRegistry();
fedConfig.AudienceRestriction.AudienceMode = AudienceUriMode.Always;
fedConfig.AudienceRestriction.AllowedAudienceUris.Add(new 
Uri("http://localhost:8000/TodoListService"));
fedConfig.CertificateValidationMode = 
X509CertificateValidationMode.PeerTrust;
fedConfig.ServiceCertificate = CertificateUtil.GetCertificate(
StoreName.My, StoreLocation.LocalMachine, "CN=RP");

FederatedServiceCredentials fedCreds = 
new FederatedServiceCredentials(fedConfig);

FederatedServiceCredentials.ConfigureServiceHost(host,fedConfig);
host.Open();

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

Архитектура компонентов WIF

Когда вы применяете WIF-поведение к ServiceHost, для упрощения авторизации на основе заявок инициализируются несколько компонентов WIF — многие из них являются WCF-расширениями. В конечном счете это ведет к тому, что ClaimsPrincipal подключается к потоку запроса и тем самым реализуется поддержка авторизации на основе заявок. На рис. 7 показаны взаимосвязи между базовыми компонентами WIF и ServiceHost.


Рис. 7 Базовые компоненты, устанавливаемые с WIF

Тип FederatedServiceCredentials заменяет исходное поведение ServiceCredentials, а IdentityModelServiceAuthorizationManager (устанавливаемые при инициализации FederatedServiceCredentials) — исходное поведение ServiceAuthorizationBehavior. Совместно эти типы управляют аутентификацией и авторизацией для каждого запроса; в этом процессе также участвуют ClaimsAuthenticationManager, ClaimsAuthorizationManager и SecurityTokenHandler (который применяется к конкретному запросу).

Рис. 8 иллюстрирует, как эти компоненты, взаимодействуя между собой, создают объект — участник системы безопасности (security principal) для потока запроса (в данном случае — тип ClaimsPrincipal) и позволяют авторизовать доступ на основе этого участника.


Рис.8 Компоненты, которые создают ClaimsPrincipal и могут выполнять авторизацию с его помощью

FederatedSecurityTokenManager возвращает подходящий обработчик маркеров для данного запроса — в данном случае это Saml11SecurityTokenHandler — и передает ему ссылку на ClaimsAuthorizationManager. Обработчик маркеров конструирует ClaimsIdentity из входящего маркера, создает ClaimsPrincipal (через класс-оболочку) и передает его в метод ValidateToken для ClaimsAuthorizationManager. Это открывает возможность модификации или замены ClaimsPrincipal, который будет подключен к потоку запроса. Реализация по умолчанию просто возвращает переданный ранее ClaimsPrincipal:

public virtual IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
{
    return incomingPrincipal;
}

Вы могли бы предоставить собственный ClaimsAuthenticationManager для преобразования входящих заявок из маркеров защиты во что-то, что RP использовал бы для авторизации доступа. Однако в этом примере SAML-маркер содержит соответствующие заявки RP, выданные сервисом RP-STS, поэтому для выполнения авторизации достаточно ClaimsPrincipal, сконструированного из этих заявок.

Далее IdentityModelServiceAuthorizationManager, который ссылается на ClaimsAuthorizationManager, вызывает его метод CheckAccess, давая возможность настраивать управление доступом. Реализация по умолчанию не ограничивает доступа:

public virtual bool CheckAccess(AuthorizationContext context)
{
    return true;
}

Параметр AuthorizationContext обеспечивает доступ к ClaimsPrincipal и сопоставленным с ним заявкам, набору действий, релевантных для запроса (например, URI, указывающий вызываемую операцию сервиса), и информацию о ресурсе, связанную с запросом (скажем, URI сервиса), которая может быть полезна для того, чтобы различать вызовы к нескольким сервисам, передаваемые по одному пути авторизации. Для реализации централизованной авторизации можно предоставить собственный ClaimsAuthorizationManager. Я опишу пример, когда буду рассматривать методики, применяемые при авторизации.

Ролевая защита в .NET Framework базируется на предпосылке, что участник системы безопасности, основанный на IPrincipal, подключен к каждому потоку и что этот участник обертывает идентификацию аутентифицированного пользователя в реализации IIdentity. Без WIF подсистема WCF подключает участник системы безопасности к каждому потоку запроса на основе конфигурации system.serviceModel для аутентификации и авторизации. Тип IIdentity конструируется на основе типа удостоверений, представленных для аутентификации. Например, учетные данные Windows оцениваются как WindowsIdentity, сертификат X.509 — как X509Identity, а маркер UserName — как GenericIdentity. ServiceAuthorizationBehavior управляет типом оболочки IPrincipal для идентификации. Так, при авторизации средствами Windows конструируется WindowsPrincipal, а при использовании провайдера членства в группах ASP.NET — RoleProviderPrincipal. Для конструирования нужного вам объекта IPrincipal применяется собственная политика авторизации. Объект IPrincipal предоставляет метод IsInRole, который можно вызывать напрямую или опосредованно через запрос разрешений; этот метод обеспечивает управление доступом к средствам и функциональности.

WIF расширяет эту модель, предоставляя типы ClaimsPrincipal и ClaimsIdentity (они основаны на IClaimsPrincipal и IClaimsIdentity), которые в конечном счете наследуют от IPrincipal и IIdentity. В WIF все маркеры преобразуются в ClaimsIdentity. При проверке каждого входящего маркера защиты сопоставленный с ним тип SecurityTokenHandler конструирует ClaimsIdentity, передавая ему соответствующие заявки. Этот ClaimsIdentity обертывается в ClaimsIdentityCollection (в том случае, если маркер приводит к созданию нескольких экземпляров ClaimsIdentity), а этот набор обертывается в ClaimsPrincipal и подключается к потоку запроса. И именно этот ClaimsPrincipal занимает центральное место в WIF-авторизации ваших WCF-сервисов.

Авторизация на основе заявок

В случае WCF-сервисов ваш подход к авторизации скорее всего будет включать одну из следующих методик:

  • применение ClaimsPrincipal для выполнения динамических проверок IsInRole;
  • использование типа PrincipalPermission для выполнения запросов на динамическую смену разрешений;
  • применение PrincipalPermissionAttribute для передачи декларативных запросов разрешений в каждую операцию;
  • централизация проверок прав доступа в одном компоненте с помощью собственного ClaimsAuthorizationManager.

В первых трех вариантах вы в конечном счете полагаетесь на метод IsInRole типа ClaimsPrincipal. Это вовсе не значит, что вы используете ролевую защиту; вы просто выбираете тип roleClaimType, чтобы проверять, что в IsInRole передаются нужные заявки. WIF-тип roleClaimType по умолчанию — schemas.microsoft.com/ws/2008/06/identity/claims/role. Если STS, задействованный в варианте с применением федерации, выдает этот тип заявки, вы можете дополнительно контролировать доступ на основе этого типа. В случае приложения Todo List, как я уже упоминала, для авторизации используется собственный тип заявки permission, поэтому в конфигурации модели идентификаций нужно указать его как roleClaimType (это упростит проверки с помощью IsInRole).

Вы передаете тип roleClaimType в SecurityTokenHandler для ожидаемого типа маркера, в данном случае — Saml11SecurityTokenHandler. Как показано на рис. 6, вы можете модифицировать конфигурацию SecurityTokenHandler по умолчанию, удалив ее, а потом снова добавив, но уже с нужными вам значениями свойств. В обработчиках SAML-маркеров есть раздел samlSecurityTokenRequirement, в котором можно задать значение для name или типа role-claim, а также другие значения, относящиеся к проверке сертификатов и маркеров Windows. В этом сценарии я указываю собственный roleClaimType:

<samlSecurityTokenRequirement >
  <roleClaimType value= "urn:TodoListApp/2009/06/claims/permission"/>
</samlSecurityTokenRequirement>

Это означает, что всякий раз, когда вызывается IsInRole для ClaimsPrincipal, я проверяю наличие правильной заявки permission. Один из способов выполнения этой задачи — явный вызов IsInRole до выполнения блока кода, требующего конкретную заявку. Получить доступ к текущему участнику системы безопасности можно через свойство Thread.CurrentPrincipal:

if (!Thread.CurrentPrincipal.
IsInRole("urn:TodoListApp/2009/06/claims/permission/delete"))
  throw new SecurityException("Access is denied.");

Помимо явных проверок IsInRole в период выполнения, вы также можете написать традиционные запросы разрешений на основе ролей, используя тип PrincipalPermission. Вы инициализируете этот тип заявкой нужной роли (второй параметр конструктора), и при вызове Demand вызывается метод IsInRole текущего участника системы безопасности. Если нужной заявки нет, генерируется исключение:

PrincipalPermission p = new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/delete");
p.Demand();

Кроме того, вы можете создать PermissionSet, чтобы поместить в него несколько заявок для проверки:

PermissionSet ps = new PermissionSet(PermissionState.Unrestricted);
ps.AddPermission(new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/create"));
ps.AddPermission(new PrincipalPermission("", "urn:TodoListApp/2009/06/claims/permission/read"));
ps.Demand();

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

[PrincipalPermission(SecurityAction.Demand, Role = Constants.Permissions.Create)]
[PrincipalPermission(SecurityAction.Demand, Role = Constants.Permissions.Read)]
public string CreateItem(TodoItem item)

В некоторых случаях удобно централизовать авторизацию в одном компоненте, а это означает, что вы могли бы предоставить собственный ClaimsAuthorizationManager для выполнения проверок прав доступа. Рис. 6 иллюстрирует, как сконфигурировать собственный ClaimsAuthorizationManager, а соответствующая реализация для TodoListService показана на рис. 9 (это не полный листинг).

Рис. 9 Собственная реализация ClaimsAuthorizationManager

class CustomClaimsAuthorizationManager : ClaimsAuthorizationManager
{
    public CustomClaimsAuthorizationManager()
    {
    }

    public override bool CheckAccess(AuthorizationContext context)
    {
        
        if (context.Resource.Where(x=> x.ClaimType == 
            System.IdentityModel.Claims.ClaimTypes.Name && x.Value == 
            "http://localhost:8000/TodoListService").Count() > 0)
        {
            if (context.Action.Where(x=> x.ClaimType == 
                System.IdentityModel.Claims.ClaimTypes.Name && x.Value == 
                Constants.Actions.GetItems).Count() > 0)
            {
                return
                    context.Principal.IsInRole(
                       Constants.Permissions.Read);
            }

        // other action checks for TodoListService
        }
        return false;
    }  
}

ClaimsAuthorizationManager содержит переопределенную версию CheckAccess, которая принимает параметр AuthorizationContext со ссылкой на ресурс (в данном случае — URI сервиса), набор действий (здесь — единственное действие, указывающее URI операции сервиса) и ClaimsPrincipal, который пока не подключен к потоку запроса. Вы можете проверить ресурс, если компонент совместно используется сервисами, как в этом примере в чисто иллюстративных целях. Как правило, вы будете проверять действие по списку URI операции сервиса и выполнять проверки IsInRole в соответствии с требованиями этой операции.

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

Продолжение во второй части

К этому моменту вы должны неплохо представлять, как настроить вариант с активной федерацией с использованием WCF и WIF, разбираться в федеративных привязках для WCF и семантике генерации прокси, понимать процесс выдачи маркеров, уметь настраивать WIF на стороне сервиса и реализовать различные методы авторизации на основе заявок. В следующей статье я перейду к варианту с пассивной федерацией с применением ASP.NET и WIF.

Мишель Леру Бустамант (Michele Leroux Bustamante) — главный архитектор компании IDesign Inc., региональный директор Microsoft в Сан-Диего и обладатель статуса Microsoft MVP в области Connected Systems. Ее последняя книга — «Learning WCF». С ней можно связаться по адресу mlb@idesign.net или через сайтidesign.net. Мишель ведет блог на сайте dasblonde.net.