
В статье я расскажу как подружить net.core версии 2 и выше с доменной NTLM-авторизацей так, чтобы все пользователи (читать как сотрудники организации) имели доступ к приложению и для каждого пользователя формировалась сессия на основе его доменной учетки
Вводная
Выполнять NTLM-авторизацию (т.е. проверку прав пользователя; не путать с аутентификацией, её выполняет контроллер домена предпрития проверяя логин и пароль пользователя при входе в систему) в контексте .Net Core версии 2.2 (или выше). И, если пользователь авторизован – формировать валидный JWT-токен с логином, ID пользователя и списком ролей
Проблемы
Обычно приложения не net.core работает с собственным веб-сервером kestrel. Kestrel - это очень скромный и шустрый веб-сервер, который, увы, не умеет NTLM. Отсюда вытекает решение – использовать kestrel совместно с IIS. Такое себе решение, но даже если так – то разработчику все равно не получить доменный логин пользователя. Ведь все, что сделает IIS, это добавит заголовки на запрос NTLM-аутентификации.
Выход
Использование драйвера http.sys в контексте net.core приложения. При этом не требуется размещение приложения в IIS, равно как и сам Internet Information Service.
Что такое http.sys
Http.sys – это низкоуровневый драйвер прослушивающий tcp-порты на предмет http-сессий и умеющий работать с ними. Является одним из главных компонентов IIS и, фактически, это самостоятельный http-сервер умеющий в NTLM. То что нужно!
Как использовать http.sys в .net core
Нужно просто указать приложению что мы будем использовать не собственную реализацию веб-сервера kestrel, а низкоуровневый виндовый http.sys драйвер. Выглядит это так
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Threading;
namespace Common.Auth.Api
{
public class Program
{
public static void Main(string[] args)
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environment}.json", optional: true)
.Build();
CreateWebHostBuilder(args)
.Build()
.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
var webHostBuilder = WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseHttpSys(options =>
{
options.Authentication.Schemes = Microsoft.AspNetCore.Server.HttpSys.AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = false;
});
return webHostBuilder;
}
}
}
На строке 27 мы объявляем UseHttpSys - мы будем хостить наше приложение c помощью http.sys драйвера. Ниже мы производим настройки веб-сервера - указываем, что все пользователи должны быть авторизованы с помощью NTLM-механизма. Готово.
Как получить ActiveDirectory логин пользователя
Еще проще, чем использовать http.sys вместо kestrel. В нужном место просто обращаемся к http-контексту, вот так
[HttpGet("auth")]
public IActionResult Auth()
{
var userLogin = HttpContext?.User?.Identity?.Name;
return Ok(userLogin);
}
Если вы хотите получать пользователя из сервиса, который ничего не знает о Controller или HttpContext - то вполне можно использовать встроенный IHttpContextAccessor и его реализацию HttpContextAccessor. Только не забудьте добавить зависимость в Startup
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Далее просто объявляете в конструкторе сервиса IHttpContextAccessor и готово, вы можете получить доступ к контексту запроса и вытащить из него AD login пользователя.
Как формировать JWT
Описание формирование JWT-токена тянет на отдельную статью. Но буду краток, ведь задача очень атомарна – нужно взять доменный логин пользователя, проверить его в собственной базе данных и, если все ок, сформировать валидный JWT-токен. А если пользователя нет в бд, то можно
плюнуть ему в лицо вернуть 403 ответ.
Метод генерации JWT-токена
private string GenerateAccessToken(int userId, string userLogin, string[] userRoles)
{
if (userId == 0 ||
string.IsNullOrEmpty(userLogin))
{
return null;
}
var claims = new List<Claim> {
new Claim("id", userId.ToString()),
new Claim("login", userLogin)
};
if (userRoles != null &&
userRoles.Any())
{
foreach (var r in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, r));
}
}
var SECRET_JWT_KEY = "mysecretkey";
var symmetricKey = new SymmetricSecurityKey(System.Text.Encoding.UTF32.GetBytes(SECRET_JWT_KEY));
var claimsIdentity = new ClaimsIdentity(claims, "Token", JwtClaims.USER_LOGIN, ClaimTypes.Role);
var utcNow = DateTime.UtcNow;
var jwt = new JwtSecurityToken(
issuer: "my_super_app_name",
audience: "_anyone_who_supports_LDAP-auth",
notBefore: utcNow,
claims: claimsIdentity.Claims,
expires: utcNow.Add(TimeSpan.FromMinutes(1)),
signingCredentials: new SigningCredentials(symmetricKey, SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
Метод GenerateAccessToken надо вызывать с параметрами userId, userLogin и userRoles. Все эти параметры, обычно, лежат в БД приложения.
Таким образом процесс генерации JWT-токена на основе доменного логина пользователя выглядит так: мы получили NTLM-логин пользователя (читать как логин от доменной учетки), далее пошли в БД нашего приложения, нашли там пользователя по строгому совпадению логина и получили его ID и список ролей. Все это добро мы запихиваем в JWT токен (это основные составляющие JWT-токена).
Далее мы передаем сгенерированный токен в приложение-потребитель и подписываем им необходимые запросы к серверу. Уже на сервере мы проверяем валидность JWT-токена.
That is all, folks!