Feel Good.

05 апреля 2010

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

Аутентификация клиента на стороне WCF сервиса - одна из важнейших задач при разворачивании безопасности на WCF сервисе, позволяющая ответить на главный вопрос "КТО?". В этой статье я расскажу, как я реализовывал аутентификацию клиента на стороне WCF сервиса. Наша конечная цель: развернуть WCF сервис, реализующий интерфейс ISecretService, содержащий метод GetSecretCode, доступ к которому разрешен только аутентифицированным клиентам.

Для начала создадим обычный WCF сервис без аутентификации, для этого откроем VisualStudio и добавим в Solution новый проект для WCF сервиса - AuthWCF. В созданный проект добавим интерфейс ISecretService:

[ServiceContract]

public interface ISecretService

{

    [OperationContract]

    string GetSecretCode();

}


Далее реализуем данный интерфейс в WCF сервисе:

public class SecretService : ISecretService

{

    public string GetSecretCode()

    {

        return "password";

    }

}


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

Первое с чем стоит определиться это с Security Mode: Transport, Message или TransportWithMessageCredential. Выбираем тип безопасности на уровне сообщений - Message.
Второе, это видом аутентификации клиента Client Credential Type в зависимости от расположения WCF сервиса(internet, intranet): Windows, Certificate, Digest, Basic, UserName, NTLM, IssuedToken. Выбираем аутентификацию по логину и паролю - UserName, как наиболее универсальный тип.

Определившись со связкой Message/UserName, настроим WCF сервис, добавив в конфигурационный файл (Web.config) строки:

<system.serviceModel>

    <bindings>

        <wsHttpBinding>

            <binding name="AuthWCF.SecretServiceBinding">

                <!-- Укажем Security Mode -->

                <security mode="Message">

                    <!-- Тип аутентификации -->

                    <message clientCredentialType="UserName"/>

                </security>

            </binding>

        </wsHttpBinding>

    </bindings>

    <services>

        <service

            name="AuthWCF.SecretService"

            behaviorConfiguration="AuthWCF.SecretServiceBehavior">

 

            <host>

                <baseAddresses>

                    <add baseAddress="http://localhost/authwcf/"/>

                </baseAddresses>

            </host>

 

            <endpoint

                address=""

                binding="wsHttpBinding"

                bindingConfiguration="AuthWCF.SecretServiceBinding"

                contract="AuthWCF.ISecretService">

            </endpoint>

 

            <endpoint

                address="mex"

                binding="mexHttpBinding"

                contract="IMetadataExchange"/>

        </service>

    </services>

    <behaviors>

        <serviceBehaviors>

            <behavior name="AuthWCF.SecretServiceBehavior">

 

                <serviceMetadata httpGetEnabled="true"/>                   

                <serviceDebug includeExceptionDetailInFaults="true"/>

 

                <serviceCredentials>

 

                    <!-- Укажем свой собственный UserNameValidator -->

                    <userNameAuthentication

                        userNamePasswordValidationMode="Custom"

                        customUserNamePasswordValidatorType="AuthWCF.CustomUserNameValidator, AuthWCF"/>

 

                    <!-- Сертификат из хранилища сертификатов на локальном компьютере

                    в разделе Личные с серийным номером 01 -->

                    <serviceCertificate

                        findValue="01"

                        storeLocation="LocalMachine"

                        storeName="My"

                        x509FindType="FindBySerialNumber"/>

                </serviceCredentials>

            </behavior>

        </serviceBehaviors>

    </behaviors>

</system.serviceModel>


В настройках мы указали не существующий CustomUserNameValidator, поэтому добавим в проект класс CustomUserNameValidator, являющийся наследником UserNamePasswordValidator, и реализующий процесс аутентификации пользователя:

using System.ServiceModel;

using System.IdentityModel.Selectors;

 

namespace AuthWCF

{

    public class CustomUserNameValidator : UserNamePasswordValidator

    {

        public override void Validate(string userName, string password)

        {

            // Доступ разрешен только пользователю "ilya" по паролю "pass"

            if (!(userName == "ilya" && password == "pass"))

            {

                throw new FaultException("Неверный логин или пароль");

            }

        }

    }

}


Итак, мы реализовали WCF сервис со встроенной аутентификацией клиента. Попробуем запустить его, откроем Web-браузер, и введем адрес к WCF сервису (http://localhost/authwcf), но в ответ увидим следующую картину:

Эта ошибка означает, что WCF сервис не смог найти, указанный в настройках сертификат. Зачем здесь нужен сертификат? Сертификат нужен для того, чтобы сервис мог пройти аутентификацию на стороне клиента. Если мы доверяем сертификату - то мы доверяем и сервису. Мы же не хотим передавать свои credential неизвестно кому и куда. Для этого нашему WCF-сервису необходимо обзавестись собственным сертификатом ("Создаем сертификаты: OpenSSL").
Сертификат создан и добавлен в соответствующее хранилище, но сервис выдает новую ошибку:

Это означает, что сервис не имеет доступа к private-ключам сертификата. Для того чтобы дать доступ, надо выяснить, под каким пользователем запускается сервис (по умолчанию это ASPNET) и в каком файле хранится ключ. Для этого воспользуемся утилитой Find Private Key Tool:

#FindKey.exe My LocalMachine


В открывшемся окошке находим наш сертификат (обратите внимание на серийный номер сертификата):

Нажимаем OK, после чего в консоль будет выведено имя файла с ключами, который можно будет найти в папке C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys\. Зная имя файла, разрешаем процессу ASPNET читать данный файл:

cacls.exe "C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys\xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" /E /G "ASPNET":R


Где xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx имя файла с private-ключом.

Серверная сторона настроена, осталось разобраться с клиентской. Создадим новый консольный проект, в котором добавим "Service Reference" на наш WCF-сервис (http://localhost/authwcf/). Файл WCF прокси и файл конфигурации сгенерятся автоматически. После чего, можно произвести вызов сервис методов, предварительно передав ClientCredentials:

using (SecretService.SecretServiceClient srv = new SecretService.SecretServiceClient())

{

    srv.ClientCredentials.UserName.UserName = "ilya";

    srv.ClientCredentials.UserName.Password = "pass";

    Console.WriteLine(srv.GetSecretCode());

}


Если заглянуть в конфигурационный файл клиента, то вы увидите раздел, который содержит сертификат сервиса, сохраненном в формате Base64:

<identity>

    <certificate encodedValue="AwAAAAEAAAAUAAAAjuVIDNadCpvwDY2t8ZTIqrD4F88gAAAAAQAAADYCAAAwggIyMIIBmwIBATANBgkqhkiG9w0BAQQFADBkMQswCQYDVQQGEwJSVTEPMA0GA1UECBMGUnVzc2lhMQ8wDQYDVQQHEwZNb3Njb3cxEjAQBgNVBAoTCU15Q29tcGFueTELMAkGA1UECxMCQ0ExEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMDAzMzAxNTEwMTJaFw0xMTAzMjUxNTEwMTJaMF8xCzAJBgNVBAYTAlJVMQ8wDQYDVQQIEwZSdXNzaWExEjAQBgNVBAoTCU15Q29tcGFueTEXMBUGA1UECxMOSVQgRGVwYXJ0YW1lbnQxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxDGVBTIZI6tHRaJhBggS0CvCrf2/Gc4AvxBWm8tzhX6LFSwd7CdJ7c9/rc1fDF6P3LAio/E87Vo+a5qWXTI+yx1/UX8OrQSADbDeKdno8mgfc8rHLaAH64CziHIUuO7gMa3WzIX5ZzAQi1hCrpYwI8r+ZXPvJmW46ZjODQz4IfsCAwEAATANBgkqhkiG9w0BAQQFAAOBgQALHEvG2J0BC3YOgxAZyikRfU0AAf5CUmvb4hTZVICvsgxkQUzjD6GdyPi1RvUi8t4G+7EL1yWuiMx+R+AZjrTTBk83uRUOzM+8qcpT1nwK3TEHuaN9PzyLhbT3fQyOtxW2aXoCcVEVqin6hZE129yGkgeLlMZjzrxN0bqpX3QjdA==" />

</identity>


Можно настроить аутентификацию, на основе доверия CA сертификату (важно о отметить, в таком случае, клиент должен доверять CA, издавшему сертификат для нашего сервиса. Эта проблема решается путем добавления CA сертификата в доверенные центры сертификации текущего клиента), для этого надо добавить следующие строки в конфигурационный файл клиента:

<behaviors>

    <endpointBehaviors>

        <behavior name="ClientCertificateBehavior">

            <clientCredentials>

                <serviceCertificate>

                    <authentication certificateValidationMode="ChainTrust" trustedStoreLocation="LocalMachine" />

                </serviceCertificate>

            </clientCredentials>

        </behavior>

    </endpointBehaviors>

</behaviors>


Если у Вас возникло исключение ("The X.509 certificate CN=localhost, OU=IT Departament, O=MyCompany, S=Russia, C=RU chain building failed. The certificate that was used has a trust chain that cannot be verified. Replace the certificate or change the certificateValidationMode. Функция отзыва не смогла произвести проверку отзыва для сертификата."), то скорее всего Вы не добавили список отозванных сертификатов для CA в хранилище из предыдущей "статьи".

На стороне WCF сервиса, можно узнать, КТО аутентифицировался в данной сессии через свойство:

String login = ServiceSecurityContext.Current.PrimaryIdentity.Name;



Progg it

17 комментариев:

  1. Спасибо за статью, особено за работу с сертификатами. Работа с ними порой не так очевидна. MS сами говорят что WCF Security одна из наиболее сложных вещей.
    Важный момент, который хотел бы отметить тут - действительно важен первоночальный выбор Security Mode, поскольку может в дальнейшем сказаться на возможностях WCF, которые можно использовать на сервисе (например, не будет возможности при Message Security использовать Streamed режим).

    ОтветитьУдалить
  2. Спасибо! Очень хорошая статья. Только забыл упомянуть, что:
    1) Надо зарегистрировать сертификат на клиенте
    2) FindKey не видит сертификат пока ты его не добавишь в в самозаверенные сертификаты на IIS.
    3) Лично я открывал доступ не на "ASPNET", а на пул, который использует мой сайт

    Но может это частные случаи конечно))

    ОтветитьУдалить
  3. @Bon
    Да, надо чтобы клиент доверял корневому сертификату.
    Кстати, если Вы используете IIS7, то там настройка безопасности на основе сертификатов упрощена.

    ОтветитьУдалить
  4. Да, я использую IIS7, но особых удобств не заметил, если можно поподробнее..

    ОтветитьУдалить
  5. Вот здесь можно глянуть:
    http://www.techdays.ru/videos/1331.html

    ОтветитьУдалить
  6. во-первых огромное спасибо за статью!
    во-вторых не могли бы вы прояснить возникшие у меня вопросы:
    1. этот сервис будет доступен только для одного клиента, сертификата?

    2. сертификаты СА и клиентов должны быть установлены как на сервере так и у клиентов?

    ОтветитьУдалить
  7. @BeelDx
    1. Нет, для многих (для тех клиентов, которые доверяют CA) В нашем случае на сервер выдан единственый сертификат, подписанный нашим же CA.

    2. Да, либо добавить серверный сертификат, либо CA сертификат в список доверенных, но в случае покупки сертификата у авторизованного центра, довавлять сертификаты в списки доверенных не нужно.

    ОтветитьУдалить
  8. 1. Вообщем все сделал как тут прописано, только на разных машинах (сервис и клиент). Выводит ошибку при запуске клиента:
    Console.WriteLine(srv.GetSecretCode()); - SOAP security negotition with for target failed.

    в чем причина данной ошибки и как ее исправить? =)

    2. В данном примере тока сервер имеет сертификат, а клиенты "подписываются" (или что там) на него, так?

    3. Если второе верно, то можно ли сделать, так сказать: "двухсторонее сертифицирование", чтоб каждый клиент имел личный сертификат и через него аутентифицироваться на сервере, ну и клиенты должны быть "подписанными" на сертификат сервера. Вообщем что-то типа того), мб и где-то и не так выразился, но думаю общий смысл передал)) Как реализовать такую аутентификацию, защиту??

    ОтветитьУдалить
  9. 1. Попробуйте задать http://msdn.microsoft.com/ru-ru/library/aa347698.aspx.

    2. Да, только сервер. Клиент должен только ему доверять, либо доверять его CA. Клиент не отдаст свои login/password сервису если не доверяет ему(сертификату).

    3. Да, можно сделать двухстороннее(вместо явного указания login/password передавать сертификат, к которому имеется доверие со стороны сервера), я постараюсь написать статью, а пока только http://msdn.microsoft.com/en-us/library/ff648360.aspx.

    ОтветитьУдалить
  10. Спасибо за ссылки, особенно за последнюю :)
    вот еще нашел по ошибке http://www.gotdotnet.ru/blogs/natale/4273/ тока не помогло ничего((
    Думаю тут дело в сертификатах, у них CN=localhost - вот и работают тока на одной машине (сервис и клиент), где-то читал для сайта в сертификат надо прописывать имя сайта (ну или сервера), исправлять не стал) начал сразу с изучения второй ссылки)))

    ОтветитьУдалить
  11. Как всегда оказалось все на много проще)) выше описанная ошибка решилась добавлением на клиенте списка отозванных сертификатов :)

    ОтветитьУдалить
  12. а возможна аутентификации по паролу, но чтоб сертификаты не использовать?

    ОтветитьУдалить
  13. Не пробовал, но предполагаю что да, возможно.

    Могу лишь посоветовать:
    http://stackoverflow.com/questions/379526/wcf-username-without-certificate и http://stackoverflow.com/questions/6584794/wcf-security-username-password-without-certificate

    ОтветитьУдалить
  14. Здравствуйте Сделал все как у вас. Подскажите пожалуйста, с чем может быть связана ошибка: chain building failed. The certificate that was used has a trust chain that cannot be verified. Replace the certificate or change the certificateValidationMode. Нельзя проверить подпись сертификата.?

    ОтветитьУдалить
  15. В самом конце этой статьи я написал, что: "...скорее всего Вы не добавили список отозванных сертификатов для CA в хранилище из предыдущей статьи (http://www.handcode.ru/2010/03/openssl.html)..."

    ОтветитьУдалить
  16. 1. Как сделать тоже самое только без сертификата
    2. Как быть с ролями пользователей?

    ОтветитьУдалить