1 May

Аутентификация в .NET Core gRpc с помощью JWT

Website development.NET
В этой статье я расскажу об особенностях аутентификации API в gRpc сервисах с помощью JWT. Я предполагаю, что вы знакомы с JWT и заголовками HTTP, с их использованием в .NET Core WebAPI, поэтому не буду обсуждать эти детали. Когда я пытался реализовать аутентификацию в gRpc, я столкнулся с тем, что большинство примеров написаны с использованием консольных приложений. Это слишком далеко от реальности, в которой, на мой взгляд, живут разработчики. Например, я не хочу создавать канал каждый раз, когда я хочу вызвать метод сервиса. Еще я не хочу заботиться об отправке токена и пользовательской информации с каждым запросом. Вместо этого я хочу иметь инфраструктурный уровень, который будет заботиться обо всём этом за меня. Если эта тема вам интересна, то под катом будет больше. Все примеры в статье справедливы для .NET Core 3.1.

Используемый пример


Перед тем, как углубиться в тему, стоит описать пример, который используется в статье. Всё решение состоит из двух приложений: веб-сайта и gRpc сервиса (далее API). Оба написаны на .NET Core 3.1. Пользователь может залогиниться и посмотреть некоторые данные, если он авторизован для этого. Веб-сайт не сохраняет данные пользователя и в процессе аутентификации полагается на API. Чтобы общаться с gRpc сервисом, веб-сайту необходимо иметь валидный токен JWT, но этот токен ни как не относится к аутентификации пользователя в приложении. Веб-приложение используетс куки на своей стороне. Чтобы API знал, какой именно пользователь делает запрос к сервису, информация об этом отправляется вместе с токеном JWT, но не в самом токене, а дополнительным HTTP заголовком. На рисунке ниже показана примерная схепа системы, о которой я только что рассказал:


Здесь я должен отметить, что когда я делал этот пример, у меня не было цели реализовать наиболее правильный способ аутентификации для API. Если хотите увидеть какие-то best practices, то посмотрите спецификацию OpenID Connect. Хотя, иногда мне кажется, что самое правильное решение может ыть избыточно по сравнению с тем, что может решить проблему и сэкономить время и деньги.

Включение аутенификации с помощью JWT в gRpc сервисе


Конфигурация службы gRpc не отличается от обычной конфигурации, которая требуется .NET Core API. Дополнительным плюсом является то, что она не отличается для HTTP и HTTPS протоколов. Коротко, вам нужно добавить стандартные службы аутентификации и авторизации, а также middlewere в файле Startup.cs. Место куда вы добавляете middleware важно: его нужно добавить точно между маршрутизацией и едпоинтами (некоторый код пропущен):

public void Configure(...) {
    app.UseRouting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.UseEndpoints(...
}

А вот место где регистрируются службы не так важно, просто добавьте в метод ConfigureServices(). Но тут необходимо настроить проверку токена JWT. Это можно определить прямо тут, но я рекомендую вытащить это в отдельный класс. Таким образом, код может выглядеть следующим образом:

public void ConfigureServices(...) {
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(o => {
            var validator = new JwtTokenValidator(...);
            o.SecurityTokenValidators.Add(validator);
        });
    services.AddAuthorization();
}

Класс JwtTokenValidator — это тот, где вы будете определять логику проверки. Надо создать класс TokenValidationParameters с правильными настройками и он сделает всю остальную работу по проверке JWT. Как бонус, вы можете добавить дополнительный уровень безопасности здесь. Он может понадобиться, потому что JWT — это широко известный формат. Если у вас есть JWT, вы можете перейти на jwt.io и посмотреть некоторую информацию. Я предпочитаю добавить дополнительное шифрование в JWT, что усложняет расшифровку. Вот как может выглядеть валидатор:

public class JwtTokenValidator : ISecurityTokenValidator
{
    public bool CanReadToken(string securityToken) => true;
    
    public ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        var handler = new JwtSecurityTokenHandler();
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "your string",
            ValidAudience = "your string",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secrete code"))
        };
        
        var claimsPrincipal = handler.ValidateToken(token, tokenValidationParameters, out validatedToken);
        return claimsPrincipal;
    }
    
    public bool CanValidateToken { get; } = true;
    public int MaximumTokenSizeInBytes { get; set; } = int.MaxValue;
}

И это всё, что нужно на стороне API. История настройки клиента немного длиннее и немного отличается в зависимости выбранного протокола HTTP или HTTPS.

Отправка HTTP заголовков с каждым запросом к gRpc сервису


Вы, возможно, знаете этот из официальной документации, который фактически вы не можете использовать нигде, кроме как в тупой консольной программе. Например, вы его можете видеть вот тут.

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });
Console.WriteLine(response.Message);

Чтобы использовать это в реальном проекте, нам нужно еще иметь централизованную конфигурацию и DI, которые практически не рассматриваютсяю. Вот что вам нужно сделать. Для начала нам нужно добавить необходимые пакеты NuGet в наш проект.

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
dotnet add package Grpc.Net.ClientFactory

Пакет Grpc.Tools поможет создавать прототипы при сборке проекта, а Grpc.Net.ClientFactory поможет настроить DI.

Работая с gRpc, если вам нужно внедрить свою обработку где-то по середине цепочки запрос-ответ, вам нужно использовать классы унаследованные от Interceptor, который является частью gRpc.Core. Если вам нужно получить доступ к HttpContext.User.Identity внутри ваших сервисов, вы можете добавить интерфейс IHttpContextAccessor в ваш сервис (для этого требуется дополнительная регистрация в сервисах). Вам необходимо добавить следующее в ваш файл Startup.cs.

services.AddTransient<AuthHeadersInterceptor>();
services.AddHttpContextAccessor();

var httpClientBuilder = services.AddGrpcClient<MygRpcService.MygRpcServiceClient>(o => { o.Address = new Uri("grpc-endpoint-url"); });
httpClientBuilder.AddInterceptor<AuthHeadersInterceptor>();              
httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

Класс AuthHeadersInterceptor — это наш собственный класс, производный от класса Interceptor. Он использует IHttpContextAccessor и регистрация .AddHttpContextAccessor () позволяет сделать это.

Особенности конфигурации для HTTP


Вы можете заметить следующую конфигурацию:

httpClientBuilder.ConfigureChannel(o => o.Credentials = ChannelCredentials.Insecure);

Она необходима для работы через HTTP, но этого недостаточно. Вам также необходимо исключить эту строку из метода Configure ().

app.UseHttpsRedirection();

И ещё вам нужно потанцевать установить специальный сеттинг перед созданием любого канала gRpc. Это может быть выполнено только один раз во время запуска приложения. Поэтому я добавил его почти в ту же позицию, что и удаленная строка, упомянутая выше. Это должно быть вызвано только для HTTP.

AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

Особенности конфигурации для HTTPS


Есть некоторые сложности работы с SSL в Windows и Linux. Может случиться так, что вы разрабатываете на компьютере Windows и развертываете в Docker/Kubernetes с использованием образов на основе Linux. В таком случае конфигурация не является такой простой, как описывается во многих постах. Я опишу эту конфигурацию в другой статье, а тут я затрону только код.

Нам нужно изменить конфигурацию канала gRpc, чтобы использовать учетные данные SSL. Если вы деплоите в Docker и делаете Linux-based имеджи, вам также может понадобиться настроить HttpClient для разрешения невалидных сертификатов. HttpClient создается для каждого канала.

httpClientBuilder.ConfigureChannel(o =>
{
    // add SSL credentials
    o.Credentials = new SslCredentials();
    // allow invalid/untrusted certificates
    var httpClientHandler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    };
    var httpClient = new HttpClient(httpClientHandler);
    o.HttpClient = httpClient;
});

Добавление HTTP заголовков


Заголовки добавляются в классе перехватчика (наследнике от Interceptor). gRpc использует концепцию метаданных, которые отправляются вместе с запросами в качестве заголовков. Класс перехватчика должен добавить метаданные для контекста вызова.

public class AuthHeadersInterceptor : Interceptor
{
    public AuthHeadersInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var metadata = new Metadata
        {
            {HttpHeaderNames.Authorization, $"Bearer <JWT_TOKEN>"}
        };
        var userIdentity = _httpContextAccessor.HttpContext.User.Identity;
        if (userIdentity.IsAuthenticated)
        {
            metadata.Add(HttpHeaderNames.User, userIdentity.Name);
        }
        var callOption = context.Options.WithHeaders(metadata);
        context = new ClientInterceptorContext<TRequest, TResponse>(context.Method, context.Host, callOption);
        
        return base.AsyncUnaryCall(request, context, continuation);
    }
}

Для сценария, когда вы просто вызываете сервис gRpc, вам нужно переопределить только метод AsyncUnaryCall. Конечно, токен JWT может быть сохранен в конфигурационных файлах.

И это всё. Позже я добавлю ссылку на код с простым примером описанного варианта использования. Если у вас есть дополнительные вопросы, пожалуйста, напишите мне. Я постараюсь ответить.
Tags:grpcauthenticationjwtdotnetcore
Hubs: Website development .NET
+8
5.1k 54
Leave a comment