Анатомия .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 тоже есть). Проект развивается, много чего умеет. Советую изучить ее повнимательнее. 

Похожее:  🕸 Веб-аутентификация: файлы cookies или токены?

Поизучав исходники 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

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (1 оценок, среднее: 4,00 из 5)
Загрузка...

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

Ваш адрес email не будет опубликован.

Adblock
detector