Анатомия .NET Core: как мы настроили NTLM под Linux / Хабр

Linux wcf ntlm = любовь, но после ужина

Теперь дорогу преградил такой эксепшн

Мягкий путь. windows-контейнер

Первым делом мы настроили дебаг в docker-образ и локально запустили сервис в windows-контейнере.

При попытке отправки запроса в WCF-сервис получили весьма витиеватую ошибку:

Пробуем .net core под linux

Переключившись на сборку в Linux-контейнер, ради интереса убрали значение Domain — и оно работает.

Первая проблема при отправке запросов в WCF связана с SSL. Ругается так:

Прозрачная аутентификация в 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 протаскивать в хэндлер логику в зависимости от настроек.

Смотрим исходники

И вот здесь нельзя не порадоваться за новый Microsoft за их решение открыть код миру. В сорцах находим ключик CURLHANDLER_DEBUG_VERBOSE=true, который нам расcкажет, чем занимается libcurl в момент выполнения WCF-запросов.

Эпилог

WCF-клиент в .NET Core доставил нам немало хлопот.

На github уже есть обсуждение поднятых в статье проблем и вопросов:

1. Negotiate/NTLM