Применение Identity Server 4 в распределенном монолите

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

Identity Server

Наверное лучший способ понять, что такое Identity Server — это прочитать документацию. Если вкратце, то могу сказать, что IS представляет собой OpenID Connect и OAuth 2.0 фреймворк для ASP.NET Core. Стоит отметить, что потенциал фреймворка достаточно огромный. 

Если говорить о способах его применения, то, в целом, можно отметить два основных:

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

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

Ресурсами, которые защищаются с помощью Identity Server, могут быть хранилища файлов, различные сервисы предоставляющие данные, адаптеры конфигурации и прочее. Чтобы ограничить доступ к ресурсам необходимо сконфигурировать IS. Сама конфигурация может храниться в различных местах, например, конфигурация может храниться в базе данных, взаимодействие с которой может осуществляться через EF Core. 

var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
const string connectionString = …
services.AddIdentityServer()
    .AddConfigurationStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));
    })
    .AddOperationalStore(options =>
    {
        options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
            sql => sql.MigrationsAssembly(migrationsAssembly));
    });

Здесь подключается IS через AddIdentityServer (), а методы AddConfigurationStore и AddOperationalStore инициализируют конфигурацию и загружают оперативные данные из БД. Если использовать базу данных, то можно «на лету» менять конфигурацию Identity Server и при этом не нужно будет обновлять сервис.     

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

services.AddIdentityServer()
        .AddInMemoryClients(Clients)
        .AddInMemoryIdentityResources(Resources)
        .AddInMemoryApiResources(ApiResources)
        .AddInMemoryApiScopes(Scopes);

Здесь загружаются Clients, Resources, Scopes, ApiResources, ApiScopes в оперативную память, о них пойдет речь ниже.

Особенности конфигурации

Из прошлых вставок кода можно было заметить конфигурационный набор в виде Clients, Resources, ApiResources, Scopes. Наверное, это основные конфигурации, которые нужно встраивать в проект, но что же они означают? Давайте разбираться

Клиенты

Clients —это клиенты, которые могут подключиться к IS и получить разрешение на использование некоторых ресурсов или областей. 

{
        "ClientId": "client_id",
        "ClientSecrets": [ { "Value": "xxx" } ],
        "AccessTokenLifetime": "86400",
        "AllowedGrantTypes": [ "client_credentials" ],
        "AllowedScopes": [
          "openid",
          "profile",
        ]
}

Как и каждый пользователь в современном приложении, в конфигурации клиента есть логин и пароль. В качестве логина здесь выступает »ClientId», а в качестве пароля служит »ClientSecrets» в зашифрованном виде.  

Нужно понимать, что клиентами, как правило, являются приложения — это приводит нас к необходимости иметь различные типы аутентификации клиентов, за это отвечает параметр »AllowedGrantTypes» в конфигурации клиента. Рассмотрим основные 5 не гибридных типов, каждый из них предназначен для своего сценария входа. 

  • client credentials — предназначен для коммуникации машины к машине — токен запрашивается непосредственно от имени клиента;

  • authorization code — предназначен для работы с интерактивными пользователями клиентского приложения;

  • device flow — предназначен для работы с устройствами без браузера или с ограниченными возможностями ввода, к таким относятся например Apple TV;

  • implicit —в настоящее время всё больше теряет актуальность, так он был предназначен для собственных приложений и приложений JS, где токен доступа возвращался немедленно без дополнительного шага обмена кода авторизации;

  • resource Owner Password — используется в случае доверительных отношений с клиентом, например для приложений с высоким уровнем привилегий;

Полученный доступ был бы бесконечным, если бы в недрах Identity Server не было бы ограничений по времени, которое настраивается полем »AccessTokenLifetime».  AccessTokenLifetime — это время жизни токена доступа. Помимо токена доступа в некоторых типах авторизации используется »RefreshToken». RefreshToken токен обновления, который позволяет продлевать доступ, для этого необходимо проставить параметр »AllowOfflineAccess» в true и выполнить запрос вида:

POST /connect/token
    client_id=client&
    client_secret=secret&
    grant_type=refresh_token&
    refresh_token=hdh922

Ресурсы

Ресурсы в Identity Server 4 разделяются на два вида:

  • Identity Resources — это ресурсы пользователя такие как: идентификатор пользователя, логин, e-mail и так далее;

  • API Resources — это функциональные ресурсы, к которым может получить доступ клиент, сюда могут относиться как методы апи, так и очереди сообщений, и прочий функционал;

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

Пространства

Наверное самый простой способ понять, что такое пространства это представить себе минимальный ресурс, например, сервис, который работает с пользователями и реализует операции CRUD (create, read, update, delete) — эти виды операций с сервисом и есть пространства, однако стоит отметить, что существуют и другие варианты адаптации ресурсов и пространств к задаче. Например, пространства могут уточнять определенные ресурсы, к которым запрашивается доступ. В частности, мы можем запросить доступ не ко всем пользователям, а к определенной группе, которая будет указана в пространстве.

Профиль сервис

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

services.AddIdentityServer()    
        .AddProfileService();

Сервис валидации     

Помимо ProfileService можно реализовать свой сервис валидации, который будет проверять как токен доступа, так и весь запрос. Валидация производится после выполнения запроса через основную логику аутентификации и перед отправкой ответа клиенту. С её помощью, можно менять токен доступа, влиять на сведения в этом токене или ограничивать доступ к определенным сущностям (подробнее). Сервис должен реализовать интерфейс ICustomTokenRequestValidator и встроить его в конвейер:

services.AddIdentityServer()    
        .AddCustomTokenRequestValidator();

Применение

Итак, Identity Server мы сконфигурировали, а также приняли решение держать его отдельно как самостоятельный сервис, который выдает токены доступа на определенные ресурсы при правильных параметрах запроса. Теперь необходимо заставить сервисы или ресурсы проверять эти токены. И здесь также есть два пути:

Проверка токена на самом ресурсе

Проверка токена на самом ресурсе

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

var tokenValidationParameters = new TokenValidationParameters
{
   ValidateAudience = false,
   ValidateIssuerSigningKey = true,
   ValidateLifetime = false,
   IssuerSigningKey = securityKey,
   ValidateIssuer = false,
   ClockSkew = TimeSpan.FromMinutes(5)
};
services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", options =>
   {
       options.TokenValidationParameters = tokenValidationParameters;
   });

здесь используется схема аутентификации Bearer, а валидируется только цифровая подпись и учитывается поправка на время функционирования токена (актуально если ресурс и IS на разных машинах, где отличается время). В свою очередь некоторый открытый сервис работы с юзерами может валидировать токен по собственными внутренним правилам.

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

Отдельный сервис проверки токена

Отдельный сервис проверки токена

Здесь появляется отдельный сервис, проверки токена и управления аутентификацией. Есть также варианты, когда сервис проверки токена встроен с Identity Server. 

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

© Habrahabr.ru