Autenticación en .NET Core gRpc con JWT

En este artículo hablaré sobre las características de la autenticación API en los servicios gRpc que usan JWT. Supongo que está familiarizado con los encabezados JWT y HTTP que los usan en .NET Core WebAPI, por lo que no discutiré estos detalles. Cuando intenté implementar la autenticación en gRpc, me encontré con el hecho de que la mayoría de los ejemplos están escritos usando aplicaciones de consola. Esto está muy lejos de la realidad en la que, en mi opinión, viven los desarrolladores. Por ejemplo, no quiero crear un canal cada vez que quiero llamar a un método de servicio. Tampoco quiero preocuparme por enviar un token y la información del usuario con cada solicitud. En cambio, quiero tener un nivel de infraestructura que se encargue de todo esto por mí. Si este tema es interesante para usted, entonces habrá más bajo el corte. Todos los ejemplos en este artículo son válidos para .NET Core 3.1.

Ejemplo utilizado


Antes de profundizar en el tema, vale la pena describir el ejemplo que se utiliza en el artículo. La solución completa consta de dos aplicaciones: un sitio web y un servicio gRpc (en adelante, API). Ambos están escritos en .NET Core 3.1. El usuario puede iniciar sesión y ver algunos datos si está autorizado para ello. El sitio web no almacena datos de usuario y se basa en una API en el proceso de autenticación. Para comunicarse con el servicio gRpc, el sitio web debe tener un token JWT válido, pero este token no se relaciona con la autenticación del usuario en la aplicación. La aplicación web utiliza cookies de su lado. Para que la API sepa qué usuario realiza la solicitud al servicio, se envía información sobre esto junto con el token JWT, pero no en el token en sí, sino con un encabezado HTTP adicional. La figura a continuación muestra un esquema de ejemplo del sistema del que acabo de hablar:


Aquí debo tener en cuenta que cuando hice este ejemplo, no tenía el objetivo de implementar el método de autenticación más correcto para la API. Si desea ver algunas de las mejores prácticas, consulte la especificación OpenID Connect . Aunque, a veces me parece que la solución más correcta puede ser redundante en comparación con lo que puede resolver el problema y ahorrar tiempo y dinero.

Habilite la autenticación JWT en el servicio gRpc


La configuración del servicio gRpc no es diferente de la configuración habitual que requiere .NET Core API. Una ventaja adicional es que no es diferente para los protocolos HTTP y HTTPS. Brevemente, debe agregar servicios estándar de autenticación y autorización, así como los archivos intermedios en el archivo Startup.cs . El lugar donde agrega middleware es importante: debe agregarlo exactamente entre el enrutamiento y los puntos ( falta algún código ):

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

Pero el lugar donde se registran los servicios no es tan importante, solo agregue el método ConfigureServices () al método . Pero aquí debe configurar la comprobación del token JWT. Se puede definir aquí mismo, pero recomiendo incluirlo en una clase separada. Por lo tanto, el código puede verse así:

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

La clase JwtTokenValidator es aquella en la que definirá la lógica de validación. Debe crear la clase TokenValidationParameters con la configuración correcta y hará el resto del trabajo de validación JWT. Como beneficio adicional, puede agregar una capa adicional de seguridad aquí. Puede ser necesario porque JWT es un formato bien conocido. Si tiene JWT, puede ir a jwt.io y ver información. Prefiero agregar cifrado adicional al JWT, lo que hace que el descifrado sea más difícil. Así es como se vería un 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;
}

Y eso es todo lo que necesita el lado API. El historial de configuración del cliente es un poco más largo y diferente según el protocolo HTTP o HTTPS seleccionado.

Envío de encabezados HTTP con cada solicitud al servicio gRpc


Es posible que conozca esto por la documentación oficial, que de hecho no puede usar en ningún otro lugar, excepto en un programa de consola tonto. Por ejemplo, puedes verlo aquí .

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 esto en un proyecto real, aún necesitamos tener una configuración centralizada y DI, que casi no se consideran. Aquí está lo que tú necesitas hacer. Primero, necesitamos agregar los paquetes NuGet necesarios a nuestro proyecto. El paquete Grpc.Tools lo ayudará a crear prototipos al construir un proyecto, y Grpc.Net.ClientFactory lo ayudará a configurar DI. Cuando trabaje con gRpc, si necesita implementar su procesamiento en algún lugar en el medio de la cadena de solicitud-respuesta, debe usar clases heredadas de Interceptor , que es parte de gRpc.Core . Si necesita acceder a HttpContext.User.Identity dentro de sus servicios, puede agregar la interfaz IHttpContextAccessor

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



a su servicio (esto requiere un registro adicional en los servicios). Debe agregar lo siguiente a su archivo 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);

La clase AuthHeadersInterceptor es nuestra propia clase, derivada de la clase Interceptor . Utiliza IHttpContextAccessor y el registro .AddHttpContextAccessor () le permite hacer esto.

Características de configuración para HTTP


Puede observar la siguiente configuración:

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

Es necesario para trabajar a través de HTTP, pero esto no es suficiente. También debe excluir esta línea del método Configure ().

app.UseHttpsRedirection();

Y aún necesita bailar para establecer un entorno especial antes de crear cualquier canal gRpc. Esto solo se puede hacer una vez durante el inicio de la aplicación. Por lo tanto, lo agregué en casi la misma posición que la línea eliminada mencionada anteriormente. Esto solo debería llamarse para HTTP.

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

Características de configuración para HTTPS


Existen algunas dificultades para trabajar con SSL en Windows y Linux. Puede suceder que desarrolle en una computadora con Windows e implemente en Docker / Kubernetes utilizando imágenes basadas en Linux. En este caso, la configuración no es tan simple como se describe en muchas publicaciones. Describiré esta configuración en otro artículo, y aquí solo tocaré el código.

Necesitamos reconfigurar el canal gRpc para usar credenciales SSL. Si implementa en Docker y crea imágenes basadas en Linux, es posible que también deba configurar HttpClient para permitir certificados no válidos. HttpClient se crea 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;
});

Agregar encabezados HTTP


Los encabezados se agregan en la clase interceptor (sucesor de Interceptor ). gRpc utiliza el concepto de metadatos, que se envía junto con las solicitudes como encabezados. La clase interceptor debe agregar metadatos para el contexto de la llamada.

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 el escenario, cuando solo llama al servicio gRpc, solo necesita anular el método AsyncUnaryCall . Por supuesto, el token JWT se puede guardar en archivos de configuración.

Y es todo. Más adelante agregaré un enlace al código con un ejemplo simple del caso de uso descrito. Si tiene más preguntas, escríbame. Intentaré responder

All Articles