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

  • 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>();

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

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

services.AddIdentityServer()    
        .AddCustomTokenRequestValidator<ClientTokenValidatorService>();

Применение

Итак, 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. 

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

Комментарии (9)


  1. Free_ze
    09.07.2024 09:15
    +2

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

    Тут главное:

    IdentityServer4 will be maintained with security updates until November 2022.

    Может пора закопать стюардессу?


    1. remzalp
      09.07.2024 09:15

      Тут скорей особенность в том, что в этой версии лицензия еще позволяет где попало использовать


      1. Free_ze
        09.07.2024 09:15

        Для "где попало" актуальная версия и так будет бесплатной, а для компании с доходом $1M+ это обойдется в сумму с десяток тысяч долларов в год. Не факт, что поддерживать на своих плечах легаси выйдет дешевле.


    1. alex_smite Автор
      09.07.2024 09:15

      Ссылки перебил, по поводу "закопать стюардессу". Для относительно простых задач авторизации, в частности, доступ к полному списку сущностей или к конкретной сущности IS используем до сих пор и, как по мне, он прекрасно справляется, ну, для более сложных ACL, ABAC или RBAC (https://habr.com/ru/companies/custis/articles/248649/)


      1. Free_ze
        09.07.2024 09:15

        А я вам и не предлагаю отказываться от IS. Речь про версию 4, которая уже не поддерживается, в то время, как есть актуальная и живая 7.


        1. alex_smite Автор
          09.07.2024 09:15

          Спасибо за предложение, обязательно попробуем!


  1. rus_sus
    09.07.2024 09:15
    +2

    задача разграничения прав доступа к ресурсам, то есть задача аутентификации

    только это задача авторизации


  1. VanGoghDev
    09.07.2024 09:15

    Большое спасибо автору за статью! У меня есть запрос на рассказ про "другую историю". Интересно узнать как настраивается валидация токенов на IS и как клиентское приложение конфигурируется при таком workflow.


    1. alex_smite Автор
      09.07.2024 09:15

      Валидация токенов осуществляется в каждом сервисе отдельно, IS мы используем только для выдачи полномочий. То есть шифровать или записывать токен может только IS, а читать каждый сервис самостоятельно:

      var tokenValidationParameters = new TokenValidationParameters
      {
          ValidateAudience = false,
          ValidateIssuerSigningKey = true,
          ValidateLifetime = false,
          IssuerSigningKey = securityKey,
          ValidateIssuer = false,
      };
      
      services.AddSingleton(tokenValidationParameters)
          .AddSingleton<JwtSecutiryTokenDecodeService>()
          .AddSingleton<AccessVerificator>();
      
      services.AddAuthentication("Bearer")
      .AddJwtBearer("Bearer", options =>
      {
          options.TokenValidationParameters = tokenValidationParameters;
      });
      
      services.AddAuthorization(options =>
      {
          // политика по-умолчанию общая для всех
          options.AddPolicy("TestPolicy", policy =>
          {
              policy.AuthenticationSchemes = new List<string> { "Bearer" };
              policy.RequireAuthenticatedUser();
              policy.RequireClaim("aud", "test");
          });
      });