Pull to refresh

Windows Identity Foundation — для ASP.NET MVC проектов

Reading time11 min
Views24K

В этой статье, хотелось бы рассказать о том, как можно использовать Windows Identity Foundation в своих ASP.NET MVC проектах, и написать свой Identity Server, на WIF платформе. Во первых, потому что, общей информации, в интернете, достаточно, а вот когда дело касается конкретики, тут возникают проблемы. Так как идеологию и частные случаи можно ещё найти, а вот когда дело касается конкретики, приходится собирать по крупицам. И во вторых, то что сейчас предлагает Microsoft, используя надстройки над Visual Studio, не совсем годится, я бы даже сказал, совсем не годится при разработке решений, сложнее домашней странички или сайта — визитки. Кроме всего прочего, я не очень люблю, когда мифический мастер настройки что сделал с солюшеном, и сказал что «вроде должно работать».

В качестве примера, создадим, и настроим, самый примитивный клиент, который будет авторизоваться через самый примитивный сервер авторизации (Identity Server) работающий по WS-Federation протоколу.

Для того, что бы определиться, в каких же случаях имеет смысл «городить огород» со своим сервером авторизации, достаточно посмотреть как же он работает. Итак, мы хотим предоставлять клиенту несколько сервисов, например несколько сайтов, например с WCF сервисами, и допустим REST API. Согласитесь, что пользователю будет достаточно дискомфортно отдельно авторизоваться на каждом из наших сайтов, при переходе с одного на другой. Тут достаточно простая идея, которая заключается в том, что пользователю, при авторизации на одном из сервисов (ресурсов), выдаётся некий Token. А в дальнейшем, другой, или тот же, сервис (ресурс) уже доверяет авторизированному пользователю, на основе существования у клиента этого самого токена, ну и так далее…

Просто для наглядности понимания, приведу понравившееся сравнение (к сожалению ссылки на нашел). Например человек получает паспорт, после некой процедуры проверки, а в дальнейшем, предоставляя свой паспорт, человеку доверяют, на основе наличия у него паспорта. В данном сравнении, паспорт человека, и есть токен клиента.

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

Конечно же, у токена, как и у паспорта, есть время жизни, и точно так же, токен может быть пересоздан, на основе устаревшего. И по аналогии, как и паспорт, токен может быть разным, т.е. представлять из себя практически всё что угодно, или это заголовок реквеста, или base64 строка, как например JWT Token (JSON Web Token). А по факту, сам токен содержит информацию о себе (время когда он был создан последний раз, публичный ключ сертификата, и т.п.), а так же список клеймов, содержащих информацию о клиенте. Для описания токена, мы будем использовать язык SAML (Security Assertion Markup Language).

Ещё одно важное понятие, — это клеймы (Claims). Клеймы входят в состав нашего токена, и несут информацию о клиенте в целом. Фактически — это словарь, состоящий из пары Key/Value, в котором Key — namespace описывающий тип поля Value, а само по себе поле Value — это простая строка. В .Net это представлено типизированным списком:

var claimsList = new List<Claim>
{
       	new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "Tester")
};

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

Отсюда можно сделать пожалуй два основных вывода, первый — это что для просто авторизации пользователя, на сайте, использовать Identity Server, просто неоправданно, и второй — что поскольку мы собираемся передавать токен, между различными ресурсами, нам понадобится безопасность транспорта, в первую очередь. И так приступим, и начнём пожалуй с настройки транспорта, поскольку если у нас не будет trusted транспорта, ничего работать просто не сможет, а для этого, в первую очередь, нам понадобится сертификат.

Создание сертификата


Вот примерно с этого момента, у многих разработчиков, начинаются вопросы, по поводу создания сертификата, настройки HTTPS на сервере, и так далее. Для работы и отладки, нам понадобится сам IIS установленный локально, простое ASP.NET MVC приложение, которое будет нашим клиентским сайтом, и доверенный сертификат. Нам нужен не просто сертификат, а сертификат выданный на какое либо доменное имя, покупать его, для тестовых целей — экономически не выгодно, поэтому мы сделаем его сами.

Например, имя домена, который мы будем использовать в тестовых целях будет identity.com. Сначала, для создания сертификата воспользуемся утилитой makecert.
makecert -r -n "CN=*.identity.com" -cy authority -b 01/01/2000 -e 01/01/2099 -a sha1 -sr localMachine -sky Exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 -sv identity.com.pvk identity.com.cer

В результате выполнения мы получим два файла identity.com.pvk и identity.com.cer.Тут все предельно понятно, да и информации по утилите makecert более чем достаточно. Единственный момент, на котором хотелось бы остановиться по подробнее, так это на CN на который мы выдаём сертификат. Если выдать сертификат просто на identity.com, то нам, в дальнейшем, будет неудобно локально моделировать ситуацию с распределёнными ресурсами, а вот использование *, значительно упрощает нашу задачу т.е. *.identity.com. Использование *, даёт нам возможность локально создавать произвольное количество доменов вида «any name».identity.com.

Далее, для проверки сертификата издателя, воспользуемся утилитой cert2spc.

cert2spc identity.com.cer identity.com.spc

И в результате, получим файл identity.com.spc, который нам понадобится для утилиты pvk2pfx. С помощью которой мы сгенерируем необходимый нам pfx файл.
pvk2pfx -pvk identity.com.pvk -spc identity.com.spc -pfx identity.com.pfx -pi "qwerty"

В результате, мы получили identity.com.pfx файл, содержащий private ключ, с паролем qwerty. Осталось его зарегистрировать IIS и в системе.

Настройка HTTPS


Для того что бы, наш IIS сервер начал работать с нашим сертификатом, во первых, нам нужно импортировать наш pfx файл, в Trusted Root Certification Authorities зону через оснастку MMC, и выполнить Import в самом IIS сервере, в разделе Server Certificates.

Теперь всё готово к настройке нашего клиентского сайта. Для этого создадим новый сайт в IIS, с именем client.identity (в прочем, без разницы с каким), главное что бы App Pool нашего сайта работал под .Net 4.0 (это если сайт скомпилирован под .Net 4.0, 4.5). И указываем Physical path, на директорию нашего клиентского сайта.

Далее, настраиваем наш HTTPS, в разделе Binding. После выбора https в поле type, нам необходимо выбрать наш сгенерированный сертификат, и только после этого нам становится доступно для редактирования поле Host name. Если сертификат мы сгенерировали с "*", то мы можем указывать практически любое имя нашего сайта, главное сто бы оно заканчивалось на identity.com, т.е. имя нашего тестового домена. В дальнейшем мы можем изменить наши биндинги в разделе Bindings, нашего сайта. Остался только последний «штрих», так это изменить hosts файл, по пути (c:\Windows\System32\drivers\etc\) и добавить туда строку с именем биндинга нашего сайта:
127.0.0.1   client.identity.com

Всё, можно проверять работу локального https, просто по адресу: client.identity.com.

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

Если Вы скачаете IdentityTrainingKitVS2010 с сайта Microsoft, то можно немного облегчить себе жизнь, выполнив SetupCertificates.cmd, например по пути IdentityTrainingKitVS2010\Labs\MembershipAndFederation\Source\Setup\Scripts\SetupCertificates.cmd

Этот скрипт сделает практически тоже самое, только для уже готового сертификата из примеров localhost.pfx (у него пароль xyz). Соответственно обращение к сайту (например Default Web Site), будут работать через localhost, а все ваши приложения которые должны работать через https, должны быть созданы ни как, Web Site, а как Web Application, под localhost сайтом.

Настройка клиентского приложения


Теперь надо произвести некоторую настройку нашего клиентского приложения. Для начала, в настройках проекта, установим адрес нашего сайта для поля Custom Web Server.

Это даст Visual Studio возможность, при запуске, автоматически делать Attach to Process к w3wp.exe процессу (процесс сайта IIS).

Теперь, нам надо разобраться с референсами нашего сайта, и добавить две сборки из GAC, System.IdentityModel.dll и System.IdentityModel.Services.dll. А так же удалить лишнее, то что нам будет мешать — это NUget пакеты DotNetOpenAuth, они нам не понадобятся, а будут только мешать, а для этого, необходимо удалить пакет Microsoft.AspNet.WebPages.OAuth. Если, по каким либо причинам, Вы не хотите их трогать, то как опциональный вариант, — это настройка регистрации в web.config.

И последним шагом, в настройке клиентского приложения — это настройка самого web.config. Во первых, в разделе sysytem.web, установить метод authentication в none.
<authentication mode="None"/>

Далее регистрируем наши секцию, для Identity Model в разделе configSections:
<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />

Добавляем и настраиваем эти секции, сначала system.identityModel:
<system.identityModel>
    <identityConfiguration saveBootstrapContext="true">
      <audienceUris>
        <add value="https://client.identity.com/" />
      </audienceUris>
      <issuerNameRegistry type="System.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
        <trustedIssuers>
          <add thumbprint="BDB8FF11361F312C06EDF1D20B05E775A123BE22" name="https://server.identity.com/issue/wsfed" />
        </trustedIssuers>
      </issuerNameRegistry>
      <certificateValidation certificateValidationMode="None" />
   </identityConfiguration>
</system.identityModel>

Для начала, в секции audienceUris, добавляем адрес нашего клиенского сайта, далее добавляем запись в секциию trustedIssuers, где thumbprint это значение Thumbprint из нашего сгенерированного файла identity.crt, или другого сертификата транспорта, по которому будет работать сервер.

Только без пробелов. А поле name — это и есть адрес нашего бедующего сервера, например, в нашем случае server.identity.com + /issue/wsfed. Использовать именно wsfed не обязательно, оссобенно в нашем случае бедующего сервера — это может быть что угодно. Просто wsfed — это сокращение от WS-Federation.
Далее добавляем секцию system.identityModel.services:
<system.identityModel.services>
    <federationConfiguration>
      <cookieHandler requireSsl="true" />
      <wsFederation passiveRedirectEnabled="true" 					issuer="https://server.identity.com/issue/wsfed"
                    realm="https://client.identity.com/"
                    reply="https://client.identity.com/" requireHttps="true" />
    </federationConfiguration>
</system.identityModel.services>

Тут всё достаточно просто, iisuer — это наш сервер из секции выше, а reply — это адрес куда, в дальнейшем отвечать серверу.

Осталось зарегистрировать модули, в разделе system.webServer.
<modules>
      <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
      <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
</modules>

Всё, можно проверять работу нашего клиента, достаточно попробовать с View вызвать любой метод или метод контроллера помеченного атрибутом Authorize.

После запуска приложения (по F5), и попытке пользователем вызвать наш метод, мы увидим GET запрос по адресу нашего бедующего сервера:
https://server.identity.com/issue/wsfed?wa=wsignin1.0&wtrealm=https%3a%2f%2fclient.identity.com%2f&wctx=rm%3d0%26id%3dpassive%26ru%3d%252fAccount&wct=2014-09-23T13%3a36%3a21Z&wreply=https%3a%2f%2fclient.identity.com%2f

Это запрос на авторизацию, от клиента серверу, по адресу server.identity.com/issue/wsfed.

Описанного выше, вполне хватает, для минимальной настройки клиентского приложения, работающего с WIF Identity Server, даже если Вы используете сервер сторонних разработчиков, главное что бы сервер поддерживал WS-Federation.

Создание сервера


Перед началом создания Identity сервера, надо обязательно упомянуть о такой вещи как STS (Security Token Service). Фактически — это сервис, и не важно на каком языке и платформе он написан. А наш ASP.NET MVC Identity Server — это по сути UI оболочка. Для создания своего STS, поскольку мы всё таки используем .Net, то нам будет удобно воспользоваться уже теми инструментами которые есть в платформе.

Наш Identity Server, так же как и клиент, по сути представляет собой, ASP.NET MVC приложение, для которого тоже необходимо настроить HTTPS, и, в нашем случае, назначим ему биндинг server.identity.com, используя все тот же, сгенерированный нами сертификат.

Создадим SignIn View для SignIn метода контроллера Account, и добавим в web.config запись:
<authentication mode="Forms">
	<forms loginUrl="~/Account/signin" timeout="2880" />
</authentication>

И добавим в роуты, запись для метода контроллера, который будет выполняться при обращении по пути issue/wsfed.
routes.MapRoute("wsfederation",
                    "issue/wsfed",
                    new { controller = "WSFederation", action = "issue" });

Именно по этому пути обращается наш клиент <>/issue/wsfed. Если мы контроллер, пометим атрибутом Authorizе, то клиентское приложение, при попытке выполнить вход, в систему, будет попадать сначала на метод SignIn, контроллера Account, который в свою очередь будет возвращать View, с логин формой нашего сервера.

Далее пользователь вводит данные, необходимые для входа (например логин пароль), и попадает на метод Issue. Обратите внимание, что RequestString при этом не меняется, а остаётся таким же, с которым обратилось клиентское приложение.

Для того что бы наш сервер понимал, что же именно клиент от него хочет, разберём аргументы запроса:
        public ActionResult Issue()
        {
            WSFederationMessage message = WSFederationMessage.CreateFromUri(HttpContext.Request.Url);

            // Sign in 
            var signinMessage = message as SignInRequestMessage;
            if (signinMessage != null)
            {
                return ProcessWSFederationSignIn(signinMessage);
            }

            // Sign out
            var signoutMessage = message as SignOutRequestMessage;
            if (signoutMessage != null)
            {
                return ProcessWSFederationSignOut(signoutMessage);
            }

            return View("Error");
        }

Соответственно метод WSFederationMessage.CreateFromUri, возвращает инстанцию наследников абстрактного класса WSFederationMessage. Далее выполняем действия, или входа в систему, или выхода.

При выполнении входа в систему по WS-Federation протоколу, выполняем статический метод:
FederatedPassiveSecurityTokenServiceOperations.ProcessSignInRequest

Этот метод, на основе полученной инстанции класса SignInRequestMessage, и списка клеймов (Claims), сформирует некий объект RequestSecurityToken, который по сути и является нашим токеном клиента, и отдаст его на метод GetScope нашего STS сервиса. Для создания нашего STS сервиса, унаследуемся от абстрактного класса SecurityTokenService:
public class TokenService : SecurityTokenService

и перекроем метод GetScope:
protected override Scope GetScope(ClaimsPrincipal principal, RequestSecurityToken request)

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

В принципе, вышеописанного в самом своём минимуме достаточно, для того что бы получить примитивный Identity Server, через который клиент может авторизоваться.

Если будет интересно, как это всё работает, я «склеил» упрощённый клиент и сервер, на с github.
Это просто облегченная версия сервера https://github.com/thinktecture/Thinktecture.IdentityServer.v2, собранная пока что, на данный момент, в целях демонстрации и не более того, и конечно не выдерживает никакой критики.

В заключение


Что хотелось бы, лично мне получить в результате, так это полноценный Identity Server, в который вынесена вся работа с профилем пользователя т.е. логин, регистрация, соцсети, просмотр своего профиля и т.д. Соответственно с должным уровнем security. Но по факту, что бы сам сервер можно было бы подключить к разрабатываемому ресурсу, и каждый раз не тратить на время на систему авторизации. Ну и конечно же, хотелось бы, интегрировать работу сервера, с WCF и REST сервисами, с распределением доступа к методам по ролям клиента. Но это пока только в планах.

Полезные ссылки


Что такое Windows Identity Foundation: http://msdn.microsoft.com/ru-ru/library/ee748475.aspx
Исходники нескольких Identity серверов, и полезных библиотек: https://github.com/thinktecture
Хороший доклад о Claims-based авторизации: https://www.youtube.com/watch?v=WHSDIiwQlS8
Ещё примеры: http://claimsid.codeplex.com
И кодепроджект: http://www.codeproject.com/Articles/504399/Understanding-Windows-Identity-Foundation-WIF
Tags:
Hubs:
Total votes 19: ↑18 and ↓1+17
Comments7

Articles