Прозрачная аутентификация в ASP.Net Core на Linux

от автора

Аутентификация в ASP.Net (Core) — тема довольно избитая, казалось бы, о чем тут еще можно писать. Но по какой-то причине за бортом остается небольшой кусочек — сквозная доменная аутентификация (ntlm, kerberos). Да, когда мы свое приложение хостим на IIS, все понятно — он за нас делает всю работу, а мы просто получаем пользователя из контекста. А что делать, если приложение написано под .Net Core, хостится на Linux машине за Nginx, а заказчик при этом предъявляет требования к прозрачной аутентификации для доменных пользователей? Очевидно, что IIS нам тут сильно не поможет. Ниже я расскажу, как можно данную задачу решить c минимальными трудозатратами. Написанное актуально для .Net Core версии 2.0-2.2. Скорее всего, будет работать на версии 3 и с той же вероятностью не будет работать на версии 1. Делаю оговорку на версионность, поскольку .Net Core довольно активно развивается, и частенько методы, сервисы, зависимости могут менять имена, местоположение, сигнатуры или вообще пропадать.

Что такое Kerberos, и как это работает, кратко можно прочитать в Wiki. В нашей задаче Kerberos в паре с keytab файлом дает возможность приложению на Linux сервере (на Windows, само собой, тоже), который не требуется включать в домен, пропускать сквозной аутентификацией пользователей на windows-клиентах.

Большое огорчение для того читателя, который ожидал увидеть здесь код работы с самим kerberos. Увы, его не будет. Мне повезло, полчаса поиска на github и вот она, удача — библиотека Kerberos.NET (в nuget тоже есть). Проект развивается, много чего умеет. Советую изучить ее повнимательнее. 

Поизучав исходники ASP.NET Core, а конкретно исходники реализаций популярных способов аутентификации, я решил делать поддержку Kerberos поверх уже реализованной Cookies аутентификации.

На мой взгляд, это один из самых простых и быстрых способов, поскольку это избавило меня от написания приличного объема инфраструктурного кода: работа непосредственно с Cookies, генерация внутреннего токена аутентификации, контроль за временем жизни сессии. И потом, нужные нам методы внезапно можно перегружать, как-будто Microsoft специально заложил возможность расширения возможности стандартной реализации своим кодом. 

Процесс Kerberos аутентификации состоит из нескольких шагов:

  1. Обращение неаутентифицированного клиента в web-приложение
  2. Предварительно настроенное на поддержку Kerberos приложение получает запрос от неизвестного клиента и желает опознать его. Для этого оно в ответе на определенный метод *Web API* добавляет заголовок WWW-Authenticate со значением Negotiate
  3. Браузер видит заголовок WWW-Authenticate со значением Negotiate и понимает, что приложение хочет опознать пользователя по доменной сессии
  4. Если все настроено корректно, то на приложение уходит запрос с проставленным заголовком Authorization и значением вида Negotiate {Kerberos тикет}
  5. Приложение валидирует тикет и получает информацию о пользователе из домена
  6. Profit!

С порядком действий определились, пора писать код. 

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

public class MixedAuthenticationDefaults {     public const string AuthenticationScheme = "Mixed";     public const string AuthorizationHeader = "Negotiate"; }

Назовем Mixed. Заодно рядышком положим в константу значение заголовка WWW-Authenticate

Далее по плану — сообщить браузеру клиента, что мы желаем аутентифицироваться по доменной учетной записи. 

Теперь неплохо бы добавить в Web API нашего приложения метод для запроса аутентификации:

[HttpGet("login")] public async Task<IActionResult> External() {     return Challenge(new AuthenticationProperties(), MixedAuthenticationDefaults.AuthenticationScheme); }

Касательно использования вызова метода Challenge. На мой взгляд, это самый простой способ «дописать» в заголовки ответа метода Web API нужные данные внутри своей реализации аутентификации. Приложение может конфигурироваться на несколько способов аутентификации через конфиг, и каждый из способов может добавлять к ответу что-то свое. В случае Kerberos это заголовок, а, например, для OAuth мы можем добавить redirect url. Чуть ниже по тексту, когда дойдем до обработчика, я покажу, как это будет выглядеть в коде. Теперь напишем валидатор тикета Kerberos.

Как я ранее упоминал, всю черную магию логики валидации за нас будет делать библиотека Kerberos.NET

public class KerberosAuthTicketValidator {     public async Task<ClaimsIdentity> IsValid(string ticket, string keytabPath)     {         if (!string.IsNullOrEmpty(keytabPath) || !string.IsNullOrEmpty(ticket))             {                 var kerberosAuth = new KerberosAuthenticator(new KeyTable(File.ReadAllBytes(_kerberosConfiguration.KeytabPath)));                 var identity = await kerberosAuth.Authenticate(kerberosCredentials.Ticket);                 return identity;               }         return null;      } }

Как видно по коду, метод валидации тикета KerberosAuthenticator.Authenticate() возвращает ClaimsIdentity, что весьма удобно. И в общем-то это весь код для валидации. Хорошо, когда есть добрые люди, которые делают сложные вещи и делятся ими на github. 

Пришло время для самого интересного — хэндлера (обработчика запросов) аутентификации.

В начале я упоминал, что свою реализацию делал на основе уже готовой Cookie Authentication. Класс хэндлера этой аутентификации называется CookieAuthenticationHandler. Просто наследуем свой обработчик от него:

public class MixedAuthenticationHandler : CookieAuthenticationHandler{}

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

Перегрузим методы:

protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {     var authResult = await base.HandleAuthenticateAsync(); // Проверяем, может мы уже //аутентифицированы     if (!authResult.Succeeded) // Если нет, то пытаемся         {             string authorizationHeader = Request.Headers["Authorization"];                 if (string.IsNullOrEmpty(authorizationHeader))                      {                         return AuthenticateResult.Fail(”Не получилось”);                     }      // не забываем, что в заголовке приходит не чистый тикет - в начале идет “Negotiate”. //Поэтому отрежем лишнее      var ticket = authorizationHeader.Substring(MixedAuthenticationDefaults.AuthorizationHeader.Length);     //теперь у нас есть тикет без лишнего мусора     var kerberosAuthTicketValidator = new KerberosAuthTicketValidator();     var kerberosIdentity = await kerberosAuthTicketValidator.IsValid(new KerberosAuthorizeCredentials(ticket));     if (kerberosIdentity != null)         {             //собираем ClaimsPrincipal             var principal = new ClaimsPrincipal(kerberosIdentity);             //создаем тикет аутентификации             var authTicket= new AuthenticationTicket(principal, MixedAuthenticationDefaults.AuthenticationScheme);                  if (ticket != null)                     {                         //если создался, то вызываем базовый метод, чтобы вся кухня хранения аутентификации в cookie сработала                         await base.HandleSignInAsync(principal, ticket.Properties);                         //возвращаем успешный результат                         return AuthenticateResult.Success(ticket);                     }         }         }      return authResult;  }

HandleAuthenticateAsync() — точка входа аутентификации в приложении. Именно он содержит логику, пропускать запрос дальше к методам контроллеров или нет. Теперь HandleChallengeAsync(). Именно он вызывается после того, как выше в статье в контроллере мы обращались к методу Challenge(). Как раз тут есть возможность использовать разную логику для разных способов аутентификаций. Например, добавлять redirect url для oauth.

В нашем случае нужно добавить только заголовок и поставить статус код:

protected override Task HandleChallengeAsync(AuthenticationProperties properties) {     Response.StatusCode = 401; //статус код “Unauthorized”     Response.Headers.Append(HeaderNames.WWWAuthenticate, MixedAuthenticationDefaults.AuthorizationHeader);     return Task.CompletedTask; }

И последнее. Чтобы регистрировать нашу самописную аутентификацию так же удобно, как и встроенную,

public void ConfigureServices(IServiceCollection services) {        .....     //наша "донорская" схема аутентификации     services.AddAuthentication().AddCookie();     .... }

необходимо сделать метод расширения:

public static class MixedAuthenticationExtensions {    public static AuthenticationBuilder AddMixed(this AuthenticationBuilder builder)    {        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());        return builder.AddScheme<CookieAuthenticationOptions, MixedAuthenticationHandler>(MixedAuthenticationDefaults.AuthenticationScheme, String.Empty, null);    } }

Теперь можно писать так:

public void ConfigureServices(IServiceCollection services) {        ...     //идентично встроенной     services.AddAuthentication(MixedAuthenticationDefaults.AuthenticationScheme).AddMixed();     ... }

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

ссылка на оригинал статьи https://habr.com/ru/company/avanpost/blog/489852/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *