Autenticação no .NET Core gRpc com JWT

Neste artigo, falarei sobre os recursos de autenticação de API nos serviços gRpc usando JWT. Suponho que você esteja familiarizado com os cabeçalhos JWT e HTTP usando-os no .NET Core WebAPI, então não discutirei esses detalhes. Quando tentei implementar a autenticação no gRpc, deparei-me com o fato de que a maioria dos exemplos é escrita usando aplicativos de console. Isso está muito longe da realidade em que, na minha opinião, os desenvolvedores vivem. Por exemplo, não quero criar um canal sempre que quiser chamar um método de serviço. Também não quero me preocupar em enviar um token e informações do usuário a cada solicitação. Em vez disso, quero ter um nível de infraestrutura que cuide de tudo isso para mim. Se este tópico for interessante para você, haverá mais detalhes. Todos os exemplos neste artigo são válidos para o .NET Core 3.1.

Exemplo usado


Antes de se aprofundar no tópico, vale a pena descrever o exemplo usado no artigo. Toda a solução consiste em dois aplicativos: um site e um serviço gRpc (a seguir denominada API). Ambos são escritos no .NET Core 3.1. O usuário pode efetuar login e ver alguns dados se estiver autorizado para isso. O site não armazena dados do usuário e depende de uma API no processo de autenticação. Para se comunicar com o serviço gRpc, o site precisa ter um token JWT válido, mas esse token não se aplica à autenticação do usuário no aplicativo. O aplicativo da web usa cookies do seu lado. Para que a API saiba qual usuário faz a solicitação ao serviço, informações sobre isso são enviadas junto com o token JWT, mas não no próprio token, mas com um cabeçalho HTTP adicional. A figura abaixo mostra um esquema de exemplo do sistema sobre o qual acabei de falar:


Aqui, devo observar que, quando fiz esse exemplo, não tinha o objetivo de implementar o método de autenticação mais correto para a API. Se você quiser ver algumas práticas recomendadas, consulte a especificação do OpenID Connect . Embora, às vezes me pareça que a solução mais correta possa ser redundante em comparação com o que pode resolver o problema e economizar tempo e dinheiro.

Habilitar autenticação JWT no serviço gRpc


A configuração do serviço gRpc não é diferente da configuração usual exigida pela API do .NET Core. Uma vantagem adicional é que não é diferente para os protocolos HTTP e HTTPS. Resumidamente, você precisa adicionar serviços de autenticação e autorização padrão, bem como a parte intermediária do arquivo Startup.cs . O local onde você adiciona o middleware é importante: você precisa adicioná-lo exatamente entre o roteamento e os pontos ( falta algum código ):

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

Mas o local em que os serviços estão registrados não é tão importante, basta adicionar o método ConfigureServices () ao método . Mas aqui você precisa configurar a verificação do token JWT. Pode ser definido aqui, mas eu recomendo puxá-lo para uma classe separada. Assim, o código pode ficar assim:

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

A classe JwtTokenValidator é aquela em que você definirá a lógica de validação. Você precisa criar a classe TokenValidationParameters com as configurações corretas e ele fará o restante do trabalho de validação do JWT. Como bônus, você pode adicionar uma camada extra de segurança aqui. Pode ser necessário porque o JWT é um formato conhecido. Se você possui o JWT, pode acessar o jwt.io e ver algumas informações. Prefiro adicionar criptografia extra ao JWT, o que dificulta a descriptografia. Aqui está a aparência de um validador:

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;
}

E isso é tudo o que a API precisa. O histórico de configuração do cliente é um pouco mais longo e um pouco diferente, dependendo do protocolo HTTP ou HTTPS selecionado.

Enviando cabeçalhos HTTP com todas as solicitações para o serviço gRpc


Você pode conhecê-lo na documentação oficial, que na verdade não pode ser usada em nenhum lugar, exceto em um programa de console idiota. Por exemplo, você pode vê- lo aqui .

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);

Para usar isso em um projeto real, ainda precisamos ter uma configuração centralizada e DI, que quase não são consideradas. Aqui está o que você precisa fazer. Primeiro, precisamos adicionar os pacotes NuGet necessários ao nosso projeto. O pacote Grpc.Tools ajudará você a criar protótipos ao criar um projeto, e o Grpc.Net.ClientFactory o ajudará a configurar o DI. Ao trabalhar com o gRpc, se você precisar implementar seu processamento em algum lugar no meio da cadeia de solicitação-resposta, precisará usar as classes herdadas do Interceptor , que faz parte do gRpc.Core . Se você precisar acessar o HttpContext.User.Identity em seus serviços, poderá adicionar a interface IHttpContextAccessor

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



ao seu serviço (isso requer registro adicional nos serviços). Você precisa adicionar o seguinte ao seu arquivo 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);

A classe AuthHeadersInterceptor é nossa própria classe, derivada da classe Interceptor . Ele usa IHttpContextAccessor e o registro .AddHttpContextAccessor () permite que você faça isso.

Recursos de configuração para HTTP


Você pode observar a seguinte configuração:

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

É necessário trabalhar com HTTP, mas isso não é suficiente. Você também precisa excluir esta linha do método Configure ().

app.UseHttpsRedirection();

E você ainda precisa dançar para estabelecer uma configuração especial antes de criar qualquer canal gRpc. Isso pode ser feito apenas uma vez durante o lançamento do aplicativo. Portanto, eu o adicionei quase na mesma posição da linha excluída mencionada acima. Isso deve ser chamado apenas para HTTP.

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

Recursos de configuração para HTTPS


Existem algumas dificuldades em trabalhar com SSL no Windows e Linux. Pode acontecer que você desenvolva em um computador Windows e implante no Docker / Kubernetes usando imagens baseadas em Linux. Nesse caso, a configuração não é tão simples como descrito em várias postagens. Descreverei essa configuração em outro artigo e aqui tocarei apenas no código.

Precisamos reconfigurar o canal gRpc para usar credenciais SSL. Se você implantar no Docker e criar imagens baseadas em Linux, também poderá ser necessário configurar o HttpClient para permitir certificados inválidos. HttpClient é criado para cada canal.

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;
});

Adicionando cabeçalhos HTTP


Cabeçalhos são adicionados na classe interceptador (sucessor do Interceptor ). O gRpc usa o conceito de metadados, que é enviado junto com as solicitações como cabeçalhos. A classe interceptora deve adicionar metadados para o contexto da chamada.

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);
    }
}

Para o cenário, quando você acabou de chamar o serviço gRpc, precisa substituir o método AsyncUnaryCall . Obviamente, o token JWT pode ser salvo nos arquivos de configuração.

E é tudo. Posteriormente, adicionarei um link ao código com um exemplo simples do caso de uso descrito. Se você tiver mais perguntas, escreva-me. Vou tentar responder.

All Articles