Введение
Привет, Хабр! На связи разработчик АО АльфаСтрахование. В этой статье я хочу рассказать о мультиаутентификации в ASP.NET Core приложениях.
Но сначала немного о нас. Мы – back-office команда, которая занимается электронным документооборотом (далее ЭДО). И иногда любим делиться своим опытом: Odata, интеграция с Почтой России, распознавание подписи, интеграция с HashiCorp Vault.
Нам довольно часто нужно писать .NET Core приложения. Нередко они используются для интеграции крупных систем. А в этих системах частенько свой собственный набор учетных записей (далее УЗ) и инструментов по управлению доступа к ним.
Анализ проблемы
При управлении жизненным циклом документа в ЭДО у нас возникает необходимость проделывать с ними разные манипуляции (например, изменять их статус, перемещать в архив, назначать согласующих и т.д.) в системе за пределами вотчины нашей команды. Каждый запрос, отправляемый во внешние системы, должен быть авторизован. А значит, у нас должны быть секреты (логин/пароль или токен) от УЗ в этих системах. Ранее мы пользовались специально созданными техническими УЗ в этих системах, и нам этого хватало. Но с недавних пор появилась необходимость манипулировать 2-я и более УЗ, владельцами которых мы не являемся.
На рисунке изображена одна из стандартных ситуаций. У нас есть пользователь, который спокойно работает в своей программе. В определенный момент ему нужно созданные им документы прогнать через системы/сервисы нашей команды. Для этого у нас существует интеграция, которая вызывается из системы пользователя. В рамках этой интеграции ЭДО должен пройти через 2 системы, одна из которых - наша. В рамках такого простого взаимодействия используются 3 УЗ:
УЗ от системы нашей команды;
УЗ от системы C. Еще одна система, не наша и не пользователя, в которой нужно произвести манипуляции;
УЗ от транспортного шлюза.
С нашей системой проблем нет. Обычно мы действуем от имени наших технических УЗ, данные авторизации для которых мы можем добыть самостоятельно, прямо во время запроса. Проблемы возникают с УЗ от транспортного шлюза и системы С. Данные авторизации от этих систем знает только вызывающая нас система. Из-за чего в рамках одного запроса мы работаем с 2-я и более пользователями, у которых мы не можем самостоятельно получить данные для аутентификации и авторизации - по одному пользователю на систему.
Варианты решения
Вариант 1 – Подключить все системы к Keycloak
В мире уже давно применяется технология «Единый вход в систему (SSO)», которая позволяет, авторизовавшись один раз, получить доступ ко всем системам организации. Для реализации подобного подхода в нашей компании можно использовать корпоративную систему аутентификации и авторизации Keycloak.
В теории можно все внутренние системы интегрировать с Keycloak и использовать один единый токен для авторизации во всех системах. Но у нас возникает сразу 2 проблемы:
Время. Данное решение начало активно применятся сравнительно недавно. Хоть все и идет к тому, что мы сможем использовать единый токен, сейчас не все системы в АльфаСтрахование имеют интеграцию с Keycloak. И когда все начнут поддерживать единый токен, это вопрос открытый :)
Контур. Как правило, нам приходится создавать интеграции с участием систем за приделами контура АльфаСтрахование. Даже если бы такие системы имели интеграцию с Keycloak, то все равно, подключить их конкретно к нашей корпоративной версии - задача очень непростая.
Вариант 2 – Обогащение токена Keycloak
В рамках мозгового штурма у нас появилась еще одна безумная идея :)
В системе Keycloak можно писать скрипты. По идее, можно написать скрипт, который во время авторизации будет ходить в нужную нам систему и там проходить авторизацию, а токен, полученный из неё, встраивать в свой. Таким образом мы сможем получить токен от Keycloak, в котором будет содержаться токен от системы C.
Данная мысль быстро забылась, так как имеет существенные проблемы:
Время. За Keycloak отвечает другая команда со своим бэклогом, и когда у них руки дойдут до разработки нужного нам скрипта – неясно;
ИБ. Скорее всего, наша информационная безопасность просто не пропустит историю, в которой корпоративный Keycloak ходит во внешнюю систему (тут конечно можно покумекать, но вряд ли будет решение, которое устроит по безопасности и срокам).
Вариант 3 – Передача двух токенов в рамках интеграции
Далее, у нас родился вариант, при котором система просто передает нам 2 токена в рамках одного запроса. Спойлер: этот вариант мы в итоге и выбрали как основной.
Идея в том, что в рамках интеграции мы предоставляем метод для авторизации во внешней системе. В этом случае стартовая система делает к нам 2 запроса:
Она получает токен авторизации системы C;
Она запускает основной процесс по интеграции и предает нам 2 токена в одном запросе.
В рамках данного варианта возникает проблема мультиаутентификации. Т.е. когда запрос приходит к нам в интеграцию, нам нужно аутентифицировать 2-х и более пользователей.
Аутентификация и авторизация в ASP.NET Core системах
Прежде чем познакомить вас с нашим решением мультиаутентификации, я расскажу, как, в общих чертах, работает аутентификация и авторизация в ASP.NET Core системах по умолчанию.
При настройке своего приложения вы так же настраиваете обработчики аутентификации и политики авторизации. Я не буду объяснять как их настраивать, скорее я опишу, как они взаимодействуют.
За аутентификацию и авторизацию в ASP.NET Core приложениях отвечают 2 промежуточных слоя, через которые проходят все запросы.
Сначала, HTTP запрос попадает в слой аутентификации. Глобально там происходит запуск обработчика аутентификации (сервис, способный извлечь из переданного токена данные пользователя), который вы установили, как обработчик по умолчанию. Он будет срабатывать всегда, на все запросы.
Далее HTTP запрос попадает в слой авторизации. В этом слое нас интересуют 2 сущности: оценщик политик, слой обработки результата авторизации.
Первым запускается оценщик политик в части аутентификации. Когда вы настраиваете действие запроса (например, action метод в контроллере), вы можете указать набор обработчиков аутентификации, которые хотите к нему применить. Собственно, этот момент и реализует оценщик политик. Т.е. он запускает все настроенные обработчики аутентификации.
Далее промежуточный слой авторизации проверяет, что в рамках запроса не разрешен анонимный вход. Если такой вход разрешён, то он пускает запрос дальше без последующих проверок.
Если в запросе анонимный вход не разрешен, то запускается оценщик политик в части авторизации. При настройке действия запроса, помимо обработчиков аутентификации, вы можете указать политики авторизации (например, доступ по ролям). Здесь, собственно, и проверяется соблюдение всех этих политик.
После оценщик политик возвращает результат авторизации, который может принимать 3 значения:
Success. Если аутентификация и авторизация прошла успешно;
Challenge:
Если ни один из запрошенных обработчиков аутентификации не отработал успешно;
Если была запрошена аутентификациая по умолчанию, но её обработчик отработал неуспешно;
Forbid. Если найдена хотя бы одна нарушенная политика авторизации.
И наконец запускается слой обработки результата авторизации. Глобально у него 3 задачи:
Если получен Success - пропустить ответ дальше;
Если получен ответ Challenge - запустить Challenge операции, прервать обработку HTTP запроса и отправить HTTP ответ. Запуск таких операции обычно заканчивается StatusCode 401 и текстом ошибки;
Если получен ответ Forbid - запустить все Forbid операции, прервать обработку HTTP запроса и отправить HTTP ответ. Запуск таких операции обычно заканчивается StatusCode 403.
Реализация мультиаутентификации
На самом деле, решить нашу проблему в рамках реализации по умолчанию тоже можно.
Мы можем создать обработчик аутентификации, который оставляет после себя особую метку (например, заполняет AuthenticationType специальным значением);
Затем создать политику авторизации, которая не пропускает запрос без этой метки;
После этого в обработчике аутентификации просто переопределить метод связанный с обработкой Forbid результата.
И это даже сработало бы, но есть одна проблема – это не очень прозрачно. Чтобы такое отследить, нужно не только понимать, как это работает под капотом, но и знать про конкретную реализацию обработчика, и в связке с какой политикой он работает.
Мы посчитали такую историю неудобной, поэтому решили слегка её доработать.
Мы решили переопределить логику работы оценщика политик и добавить в него еще одну сущность. Resolver – обработчик, определяющий является ли аутентификация в целом успешной.
Для наших нужд такой resolver нам нужен только один. Он определяет наличие запрошенной аутентификации. Причем запрос конкретной аутентификации происходит с помощью атрибута.
[ApiController]
[Route("api/[controller]")]
[RequiredAuthenticate("MyAuthenticationType")]
[Authorize(AuthenticationSchemes = "scheme1")]
[Authorize(AuthenticationSchemes = "scheme2")]
public class MyController : ControllerBase
{
public IActionResult MyAction() { }
}
Результаты
В рамках проделанной работы мы поближе познакомились с механизмом аутентификации и авторизации ASP.NET Core приложений по умолчанию. Научились расширять это механизм нужным нам функционалом. Также получили инструмент для гибкой валидации аутентификации.
Данное решение хорошо себя зарекомендовало. К тому же, как показывает практика, взаимодействие с ним является интуитивно понятным. Решение используется в 2-х больших интеграциях и не требует значительных затрат для применения.
Ссылка на механизм мультиаутентификации.