Authentication in .NET Core gRpc with JWT

In this article I will talk about the features of API authentication in gRpc services using JWT. I assume that you are familiar with JWT and HTTP headers using them in .NET Core WebAPI, so I won’t discuss these details. When I tried to implement authentication in gRpc, I came across the fact that most of the examples are written using console applications. This is too far from the reality in which, in my opinion, developers live. For example, I do not want to create a channel every time I want to call a service method. I also do not want to worry about sending a token and user information with each request. Instead, I want to have an infrastructure level that will take care of all this for me. If this topic is interesting to you, then there will be more under the cut. All examples in this article are valid for .NET Core 3.1.

Used example


Before delving into the topic, it is worth describing the example that is used in the article. The whole solution consists of two applications: a website and gRpc service (hereinafter API). Both are written in .NET Core 3.1. The user can log in and see some data if he is authorized for this. The website does not store user data and relies on an API in the authentication process. To communicate with the gRpc service, the website needs to have a valid JWT token, but this token does not relate to user authentication in the application. The web application uses cookies on its side. In order for the API to know which user makes the request to the service, information about this is sent along with the JWT token, but not in the token itself, but with an additional HTTP header. The figure below shows an example schema of the system that I just talked about:


Here I should note that when I did this example, I did not have the goal of implementing the most correct authentication method for the API. If you want to see some best practices, then look at the OpenID Connect specification . Although, sometimes it seems to me that the most correct solution can be redundant in comparison with what can solve the problem and save time and money.

Enable JWT Authentication in gRpc Service


The configuration of the gRpc service is no different from the usual configuration that the .NET Core API requires. An additional plus is that it is no different for HTTP and HTTPS protocols. Briefly, you need to add standard authentication and authorization services, as well as middlewere in the Startup.cs file . The place where you add middleware is important: you need to add it exactly between routing and points ( some code is missing ):

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

But the place where the services are registered is not so important, just add the ConfigureServices () method to the method . But here you need to configure the JWT token check. It can be defined right here, but I recommend pulling it into a separate class. Thus, the code may look like this:

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

The JwtTokenValidator class is the one where you will define the validation logic. You need to create the TokenValidationParameters class with the correct settings and it will do the rest of the JWT validation work. As a bonus, you can add an extra layer of security here. It may be needed because JWT is a well-known format. If you have JWT, you can go to jwt.io and see some information. I prefer to add extra encryption to the JWT, which makes decryption more difficult. Here's what a validator might look like:

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

And that’s all the API side needs. The client setup history is slightly longer and slightly different depending on the selected HTTP or HTTPS protocol.

Sending HTTP headers with every request to gRpc service


You may know this one from the official documentation, which in fact you cannot use anywhere except in a dumb console program. For example, you can see it here .

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

To use this in a real project, we still need to have a centralized configuration and DI, which are almost not considered. Here is what you need to do. First, we need to add the necessary NuGet packages to our project. The Grpc.Tools package will help you create prototypes when building a project, and Grpc.Net.ClientFactory will help you set up DI. When working with gRpc, if you need to implement your processing somewhere in the middle of the request-response chain, you need to use classes inherited from Interceptor , which is part of gRpc.Core . If you need to access HttpContext.User.Identity inside your services, you can add the IHttpContextAccessor interface

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



to your service (this requires additional registration in the services). You need to add the following to your Startup.cs file.

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

The AuthHeadersInterceptor class is our own class, derived from the Interceptor class . It uses IHttpContextAccessor and registering .AddHttpContextAccessor () allows you to do this.

Configuration Features for HTTP


You may notice the following configuration:

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

It is necessary for working through HTTP, but this is not enough. You also need to exclude this line from the Configure () method.

app.UseHttpsRedirection();

And still you need to dance to establish a special setting before creating any gRpc channel. This can be done only once during application launch. Therefore, I added it at almost the same position as the deleted line mentioned above. This should only be called for HTTP.

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

Configuration Features for HTTPS


There are some difficulties with working with SSL on Windows and Linux. It may happen that you develop on a Windows computer and deploy to Docker / Kubernetes using Linux-based images. In this case, the configuration is not as simple as described in many posts. I will describe this configuration in another article, and here I will only touch on the code.

We need to reconfigure the gRpc channel to use SSL credentials. If you deploy to Docker and make Linux-based images, you may also need to configure HttpClient to allow invalid certificates. HttpClient is created for each channel.

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

Adding HTTP Headers


Headers are added in the interceptor class (successor from Interceptor ). gRpc uses the concept of metadata, which is sent along with requests as headers. The interceptor class should add metadata for the call context.

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

For the scenario, when you just call the gRpc service, you only need to override the AsyncUnaryCall method . Of course, the JWT token can be saved in configuration files.

And it's all. Later I will add a link to the code with a simple example of the described use case. If you have further questions, please write to me. I will try to answer.

All Articles