Применение 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.
Конфигурация валидации и чтения токена доступа будет храниться в одном месте. Этот подход может стать лучшей альтернативой, поскольку он решает все минусы предыдущего, но здесь появляется взаимодействие по сети, балансировка нагрузки и прочие особенности микросервисного подхода, что в целом добавляет время проверки токена и, к слову, каждый ресурс должен знать о том к кому обращаться, что приводит нас к единому хранилищу конфигураций, впрочем это уже другая история.