687
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Динамическое добавление провайдеров аутентификации OpenId Connect в ASP.NET Core

Время чтения 58 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
687
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Всем привет! Меня зовут Дмитрий Бахтенков, и я .NET-разработчик в компании Skillaz. Мы разрабатываем SaaS-решение для автоматизации подбора персонала на ASP.NET Core.

Недавно я столкнулся с кейсом динамического добавления схем аутентификации OpenIdConnect. Погуглив, понял, что в Сети по этой теме практически ничего нет:

  1. Вопрос на Stack Owerflow 6-летней давности без ответа «с галочкой»:
  2. Странная платная библиотека, у которой на GitHub только Samples, то есть примеры ее использования.

Увидев слабую поисковую выдачу по этой теме, решил написать статью. В ней мы разберемся, как подключить аутентификацию через множество провайдеров OpenId Connect к приложению на ASP.NET Core — с динамическим добавлением и удалением настроек, выводом информации о провайдерах в интерфейс и авторизацией через отдельный Identity Provider.

Динамическое добавление провайдеров аутентификации OpenId Connect в ASP.NET Core

Немного теории

Аутентификация — процедура проверки подлинности, например проверка подлинности пользователя путем сравнения введенного им пароля с паролем, сохраненным в базе данных. Авторизация — предоставление определенному лицу или группе лиц прав на выполнение определенных действий. 

В мире современных мультитенантных систем авторизация играет ключевую роль, обеспечивая безопасный доступ к ресурсам для множества пользователей из различных организаций. Часто организации предпочитают использовать централизованные провайдеры авторизации, такие как Active Directory или Keycloak, для управления пользователями и ролями. Это не только обеспечивает централизованное управление правами доступа, но и позволяет реализовывать механизм единого входа (SSO) для различных цифровых продуктов.

Для реализации единой авторизации в таких мультитенантных системах широко используется протокол OpenID Connect. Этот протокол предоставляет последовательность шагов и правил для выполнения аутентификации и передачи информации о пользователе между приложениями и Identity Provider. Важным аспектом этого протокола является понятие клеймов (claims) — единиц информации о пользователе, которые могут содержать различные атрибуты, такие как его роль, уровень доступа и другие кастомные данные.

SSO и провайдеры аутентификации

Множество организаций используют у себя различные системы для управления пользователями, например Active Directory, который можно применять с протоколом аутентификации LDAP, или Keycloak. Преимущество использования этих программ заключается в централизованном управлении правами и доступами пользователей, а также эти решения позволяют реализовывать SSO (single sign on) в используемых внутри организации цифровых продуктах.

В реализации единой авторизации эти программы выступают в роли Identity Provider — системы управления доступами. Она предоставляет сторонним приложениям некоторую информацию о пользователе — его роль, уровень доступа и т. д.

Одна единица информации о пользователе называется клеймом — claim. Там может содержаться разная информация — от роли и идентификатора пользователя до кастомных полей вроде его даты рождения или города проживания. 

Существуют различные протоколы, которые описывают процесс и правила аутентификации в приложении. Самыми популярными являются SAML и OpenId Connect (OIDC). Со вторым мы и будем работать.

Разбираемся с OpenID Connect

Этот протокол определяет последовательность шагов и правил для выполнения аутентификации: какие данные куда передаются, какая часть приложения куда выполняет редирект и как шифруются и передаются claims в приложение-клиент.

В OpenId Connect существует три подхода к реализации аутентификации:

  1. Implicit Flow — используется, когда вся логика авторизации происходит на фронтенде с помощью JavaScript. Основная проблема тут в том, что пользователь может посмотреть все передаваемые данные в dev tools браузера, поэтому нам необходимо скрывать/шифровать эту информацию.
  2. Authentication Flow — стандартный процесс аутентификации, который подразумевает вызовы методов Identity Provider на стороне сервера.
  3. Hybrid Flow — объединяет предыдущие два подхода: разные токены могут передаваться как через фронтенд, так и через серверную часть.

Мы реализуем стандартный Authentication Flow: аутентификация будет происходить на сервере и сохранять информацию о пользователе в cookie.

Keycloak

Это open source identity провайдер — приложение, с помощью которого можно управлять пользователями, ролями, настраивать аутентификацию и авторизацию и др.

Преимущества:

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

На этом сайте в разделе Getting Started можно посмотреть гайды по установке и настройке приложения. Я воспользуюсь Docker, так как это самый простой и удобный вариант. Вот пример файла docker-compose, который я подготовил для этой статьи:

version: '3.3'

services:
keycloak:
image: quay.io/keycloak/keycloak:20.0.2
container_name: keycloak
command:
- start --auto-build --db dev-file --hostname-strict-https false --hostname-strict false --proxy edge --http-enabled true
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- 8282:8080

После того как контейнер запустился, зайдем на localhost:8282 (у вас порт может быть другой) и выполним настройку. Нам необходимо добавить нового пользователя и создать клиента для использования аутентификации в стороннем приложении.

Клиента создаем в разделе Clients → Create Client. Сначала вводим название и идентификатор клиента.

На следующем этапе нужно включить галочку Client Authentication.

После нажатия Save в разделе Credentials увидим Client Secret, который можно скопировать и использовать в приложении.

Теперь добавим пользователя в Users → Create User:

Во вкладке Credentials укажем пароль для пользователя:

Создаем проект на ASP.NET Core

Мы будем использовать ASP.NET MVC. Для начала настроим конфигурацию OIDC для одного провайдера — это делается в коде. Можно создать проект в интерфейсе IDE или выполнить команду dotnet new mvc

Структура созданного проекта будет следующей:

Для добавления аутентификации по протоколу OpenId Connect нам потребуется NuGet-пакет Microsoft.AspNetCore.Authentication.OpenIdConnect. Установить его нужно на основе вашей версии .NET. У меня .NET7, поэтому я выбрал версию 7.0.16.

Теперь добавим конфигурацию OpenId Connect в наше приложение. В файл program.cs после строки builder.Services.AddControllersWithViews(); добавим следующий код:

builder.Services
   .AddAuthentication(options =>
   {
       options.DefaultScheme =
           CookieAuthenticationDefaults
               .AuthenticationScheme; // сохранять данные аутентификации будем в cookie стандартным механизмом ASP.NET Core
       options.DefaultChallengeScheme = "testScheme"; // название схемы авторизации
   })
   .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
   .AddOpenIdConnect("testScheme", options =>
   {
       options.Authority = "http://localhost:8282/realms/master";
       options.CallbackPath = "/signin-oidc-test"; // путь для колбэка openid connect в формате signin-oidc-<что-то>
       options.ResponseType = OpenIdConnectResponseType.Code;
       options.ResponseMode = OpenIdConnectResponseMode.FormPost;
       options.ClientId = "test-client-id"; // clientId, созданный на этапе настройки Keycloak
       options.ClientSecret = "<секрет>"; // clientSecret, созданный на этапе настройки Keycloak
       options.RequireHttpsMetadata = false; // локально развернутый Keycloak работает по HTTP
       options.SaveTokens = true;
       options.NonceCookie.SameSite = SameSiteMode.Unspecified;
       options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
   });

Немного отвлечемся и посмотрим, как происходит аутентификация через внешний Identity Provider:

  1. Сначала пользователь выбирает Войти через Keycloak и происходит редирект на адрес Keycloak, где отображается форма с введением логина и пароля.
  2. Пользователь вводит свои данные и происходит редирект (с cookie) на адрес в формате /signin-oidc-*.
  3. В процессе обработки редиректа /sign-in-* дефолтная логика ASP.NET получает информацию о пользователе, создает объект Identity с клеймами и сохраняет их.
  4. Далее происходит редирект на некоторый ReturnUrl, указанный при вызове логики аутентификации. Это уже кастомный эндпойнт, в котором мы можем досталь пользователя и клеймы из HttpContext и выполнить необходимую логику.

Реализация аутентификации

Добавим следующие файлы:

  • AccountController в папку Controllers/;
  • Index.cshtml в папку Views/Account/.

Файл Index.cshtml будет небольшой — просто кнопка «Войти через Keycloak»:

<h2>Вход на сайт</h2>
<form asp-action="Login" asp-controller="Account" asp-anti-forgery="true">
   <div class="form-group">
       <input type="submit" value="Войти с помощью keycloak" class="btn btn-outline-dark"/>
   </div>
</form>

Теперь реализуем AccountController, добавим метод Index и Login:

public class AccountController : Controller
{
   public IActionResult Index()
   {
       return View();
   }
   [HttpPost]
   public IActionResult Login()
   {
       if (HttpContext.User.Identity?.IsAuthenticated is not true)
       {
           return Challenge(new OpenIdConnectChallengeProperties
           {
               RedirectUri = $"/account/callback",
           }, "testScheme");
       }
       return RedirectToAction("Index", "Home");
   }
   [HttpGet("account/callback")]
   public async Task<IActionResult> OpenIdCallback()
   {
       await HttpContext.AuthenticateAsync("testScheme");
       return RedirectToAction("Index", "Home");
   }
}

Чтобы понять, что аутентификация прошла успешно, дополним главную страницу — файл Home/Index.cshtml:

@{
   ViewData["Title"] = "Home Page";
}
<div class="text-center">
   <h1 class="display-4">Welcome</h1>
   @if (User.Identity?.IsAuthenticated is true)
   {
       <h1 class="display-6">Вы авторизованы!</h1>
       <a asp-controller="Account" asp-action="Index">Выйти</a>
       <ul class="list-unstyled">
           @foreach (var claim in User.Claims)
           {
               <li>@claim.Type: @claim.Value</li>
           }
       </ul>  
   }
   @if(User.Identity?.IsAuthenticated is not true)
   {
       <h1 class="display-5">Вы не авторизованы!</h1>
       <a asp-controller="Account" asp-action="Login">Войти</a>
   }
</div>

Теперь попробуем запустить приложение. Перед этим немного донастроим наш клиент в Keycloak — добавим информацию о доступных хостах для редиректа на основе порта нашего ASP.NET-приложения:

Вот какая страница открывается после запуска приложения:

Перейдем на адрес /Account по кнопке «Войти»:

Попробуем войти через Keycloak (сначала выполните SignOut в самом Keycloak). Можно использовать логин и пароль нашего созданного пользователя, а можно — админа.

После авторизации на главной странице видим список клеймов, полученных от Keycloak.

Мультитенантность

Мультитенантность — это особенность архитектуры ПО, которая позволяет одному приложению обслуживать несколько независимых арендаторов. В контексте SaaS (Software as a Service) это означает, что одна и та же система может изолированно и безопасно управлять данными и процессами различных клиентов. Каждый клиент работает с приложением, как будто оно было создано специально для него, хотя на самом деле они все используют общую инфраструктуру и кодовую базу.

Клиенты, которые используют такие приложения, могут требовать использования своей внутренней системы аутентификации и авторизации, построенной на базе ADFS или Keycloak. Для этого в SaaS-приложении необходимо разработать функционал динамического добавления различных провайдеров.

Добавляем хранение провайдеров в БД

Чтобы добавить поддержку множества провайдеров, а также добавлять и редактировать их без перезапуска приложения, нам нужно где-то хранить конфигурации. Для примера используем Entity Framework Core и SQLite, но в целом подойдет любая база данных.

Устанавливаем пакеты через nuget:

Далее нам необходимо описать класс с полями, которые будут использоваться в таблице-хранилище конфигураций. Добавим класс OpenIdConnectScheme и заполним его:

public class OpenIdConnectScheme
{
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get; set; }
   public string Authority { get; set; } // URL нашего identity-провайдера
   public string ClientId { get; set; } // ClientId, созданный в idp
   public string ClientSecret { get; set; } // ClientSecret, созданный в idp
}

Теперь нам нужно создать контекст, который будет «общаться» с самой БД:

public class OidcDbContext : DbContext
{
   public OidcDbContext(DbContextOptions<OidcDbContext> options)
       : base(options)
   {
   }
   public DbSet<OpenIdConnectScheme> OpenIdConnectSchemes { get; set; }
}

В файле Program.cs зарегистрируем наш контекст:

builder.Services.AddDbContext<OidcDbContext>(optionsBuilder => optionsBuilder.UseSqlite("db"));

Теперь добавим OpenIdConnectSchemeController — контроллер, который будет обрабатывать пользовательский ввод данных. В этом контроллере реализованы стандартные CRUD-операции. Итоговый код OpenIdConnectSchemeController.cs можно посмотреть в репозитории.

Теперь поработаем над интерфейсом. Добавим несколько cshtml-файлов для интерфейса CRUD-операций.

Структура проекта в папке Views:

Реализация интерфейса доступна в файлах по ссылкам ниже:

Теперь отредактируем файл Account/Index.cshtml — добавим столько кнопок «Войти», сколько у нас провайдеров. Также для корректного вызова метода Challenge нам нужно пробросить в параметры метода Login информацию о добавленных схемах.

Home/Index.cshtml

@model List<OpenIdConnectScheme>
<h2>Вход на сайт</h2>
@foreach (var scheme in Model)
{
   <form asp-action="Login" asp-route-scheme="oidc-@scheme.Id" asp-controller="Account" asp-anti-forgery="true">
       <div class="form-group">
           <input type="submit" value="Войти с помощью keycloak - @scheme.ClientId" class="btn btn-outline-dark"/>
       </div>
   </form>
}

AccountController.cs

public class AccountController : Controller
{
   private readonly OidcDbContext _dbContext;
   public AccountController(OidcDbContext dbContext)
   {
       _dbContext = dbContext;
   }
   public async Task<IActionResult> Index()
   {
       return View(await _dbContext.OpenIdConnectSchemes.ToListAsync());
   }
   [HttpPost]
   public IActionResult Login(string scheme)
   {
       if (HttpContext.User.Identity?.IsAuthenticated is not true)
       {
           return Challenge(new OpenIdConnectChallengeProperties
           {
               RedirectUri = $"/account/callback?scheme={scheme}",
           }, scheme);
       }
       return RedirectToAction("Index", "Home");
   }
   [HttpGet("account/callback")]
   public async Task<IActionResult> OpenIdCallback(string scheme)
   {
       await HttpContext.AuthenticateAsync(scheme);
       return RedirectToAction("Index", "Home");
   }
}

Динамическая регистрация провайдеров

Динамическая регистрация провайдеров аутентификации происходит с помощью следующих классов:

  • IAuthenticationSchemeProvider, который хранит список схем аутентификации;
  • IOptionsMonitorCache<OpenIdConnectOptions> — кеш для параметров OpenIdConnect;
  • OpenIdConnectPostConfigureOptions — класс для сохранения параметров OpenIdConnect.

В OpenIdConnectSchemeController их надо пробросить как зависимости в конструктор.

Далее добавим метод LoadScheme для того, чтобы при сохранении или редактировании информации об OIDC-провайдере мы добавляли информацию в сам ASP.NET Core, и реализуем его следующим образом:

private async Task LoadScheme(OpenIdConnectScheme scheme)
{
   // формируем уникальный ключ для схемы аутентификации
   var key = "oidc-" + scheme.Id;
   var options = new OpenIdConnectOptions();
   options.CallbackPath = $"/signin-{key}";
   options.Authority = scheme.Authority;
   options.ClientId = scheme.ClientId;
   options.ClientSecret = scheme.ClientSecret;
   options.UsePkce = true;
   options.RequireHttpsMetadata = false;
   options.SaveTokens = true;
   options.ResponseType = OpenIdConnectResponseType.Code;
   options.ResponseMode = OpenIdConnectResponseMode.FormPost;
   options.NonceCookie.SameSite = SameSiteMode.Unspecified;
   options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
  
   var existingScheme = await _authenticationSchemeProvider.GetSchemeAsync(key);
   // если такой схемы еще нет, значит, просто добавляем ее
   if (existingScheme is null)
   {
       AddScheme(key, options);
   }
   else
   {
       // если схема уже есть, то заменяем ее в _authenticationSchemeProvider и обновляем кеши
       _authenticationSchemeProvider.RemoveScheme(key);
       _optionsMonitorCache.TryRemove(key);
       AddScheme(key, options);
   }
}
private void AddScheme(string key, OpenIdConnectOptions options)
{
   _authenticationSchemeProvider.AddScheme(new AuthenticationScheme(key, key, typeof(OpenIdConnectHandler)));
   _openIdConnectPostConfigureOptions.PostConfigure(key, options);
   _optionsMonitorCache.TryAdd(key, options);
}

Далее в тот же контроллер OpenIdConnectSchemeController, в методы Create, Edit и Delete добавим код для управления схемами аутентификации — вызов метода LoadScheme.

Зависимости в Program.cs

Уберем добавленную ранее схему OpenIdConnect и зарегистрируем в DI класс OpenIdConnectPostConfigureOptions — вот что должно получиться:

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<OidcDbContext>(optionsBuilder => optionsBuilder.UseSqlite("Data Source=Application.db"));
builder.Services
   .AddAuthentication(options =>
   {
       options.DefaultScheme =
           CookieAuthenticationDefaults
               .AuthenticationScheme; // сохранять данные аутентификации будем в cookie стандартным механизмом ASP.NET Core
   })
   .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
builder.Services.AddSingleton<OpenIdConnectPostConfigureOptions>();

Запускаем приложение

Перейдем по пути <ваш адрес>:<порт>/openidconnectscheme:

Добавим новую конфигурацию — например, аналогичную той, на которой тестировали приложение:

По пути /Account появится кнопка для перехода:

Теперь добавим еще одного клиента в Keycloak:

Укажем данные:

Сохраним и снова перейдем на страницу входа /Account.

Попробуем войти с помощью любой схемы — получаем положительный результат.

Заключение

В этой статье мы научились динамически добавлять провайдеры аутентификации в приложение на ASP.NET Core. Весь исходный код вы найдете в моем репозитории DmitryBahtenkov/OidcArticle (github.com).

Также я веду телеграм-канал о разработке, где разбираю интересные задачи из практики, а еще публикую обзоры на книги и приложения.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники