Authenticate gRPC service in ASP.NET Core

ASP.NET Core supports creation of RPC style services using gRPC. Once created you might also want to secure them by authenticating and authorizing the service calls. To that end this article discusses a possible approach to implementing authentication in gRPC services.

To work through the example discussed in this article you need to be familiar with gRPC in ASP.NET Core and IdentityServer. I am not going to cover basics of these technologies here. If you are new to these topics I suggest you read my article series about gRPC (Part 1, Part 2, Part 3, Part 4) and IdentityServer (Part 1, Part 2, Part 3, Part 4, Part 5, Part 6). I am going to use the same Employees gRPC service that we developed in the mentioned article series. There are three projects involved - gRPC service project, gRPC client project, and IdentityServer server project.

The Solution explorer containing  these projects is shown below: 

Before going any further arrange your Visual Studio solution as shown above. Read the articles mentioned above and you will have everything required to write the code discussed in this article.

First of all we will make some modifications to the IdentityServer server project. Especially we need to setup an ApiResource for the gRPC service and also make some changes to the MVC app client configuration.

So, open ServerConfiguration class from the IdentityServer server project and go to the ApiResources and ApiScopes properties.

public static List<ApiScope> ApiScopes
{
    get
    {
        List<ApiScope> apiScopes = new List<ApiScope>();
        apiScopes.Add(new ApiScope("employeesGrpcService", 
"Employees gRPC Service"));
        return apiScopes;
    }
}

public static List<ApiResource> ApiResources
{
    get
    {
        ApiResource apiResource1 = new ApiResource
("employeesGrpcServiceResource", "Employees gRPC Service")
        {
            Scopes = { "employeesGrpcService" },
            UserClaims = { "role",
                            "given_name",
                            "family_name"
                            }
        };

        List<ApiResource> apiResources = 
new List<ApiResource>();
        apiResources.Add(apiResource1);

        return apiResources;
    }
}

As you can see we created an ApiScope and ApiResource for our gRPC service.

Then we need to modify the client definition for the MVC web app as shown below:

public static List<Client> Clients
{
    get
    {
        Client client = new Client
        {
            ClientName = "Client 2",
            ClientId = "client2",
            AllowedGrantTypes = GrantTypes.Hybrid,
            RedirectUris = new List<string> { 
                "https://localhost:5011/signin-oidc" 
            },
            RequirePkce = false,
            AllowedScopes = {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                "employeesGrpcService",
                "roles"
            },
            ClientSecrets = { 
                new Secret("client_secret_code".Sha512()) 
            },
            PostLogoutRedirectUris = new List<string> { 
                "https://localhost:5011/signout-callback-oidc" 
            },
            RequireConsent = true
        };
        List<Client> clients = new List<Client>();
        clients.Add(client);
        return clients;
    }
}

This completes the modifications required for the IdentityServer server project. Now, let's move on to the gRPC service project. This is the project that contains our Employees gRPC service.

Open the Startup class of the gRPC service project and enable JWT based authentication as shown below:

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(options =>
    {
        options.EnableDetailedErrors = true;
    });
            
    services.AddDbContext<AppDbContext>
(options => options.UseSqlServer
(this.config.GetConnectionString("AppDb")));

    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap
.Clear();
    services.AddAuthorization();

    services.AddAuthentication("Bearer")
            .AddJwtBearer(o =>
            {
                o.Authority = "https://localhost:5001";
                o.RequireHttpsMetadata = false;
                o.Audience = "employeesGrpcServiceResource";
                o.TokenValidationParameters =
new TokenValidationParameters
{
    RoleClaimType = "role",
};
});
}

Notice the AddJwtBearer() method. Note that the Authority property must point to the URL of the IdentityServer server application.

Go to the Configure() method and add this code :

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(...)

Make sure to place the UserAuthentication() and UseAuthorization() calls in between UseRouting() and UseEndpoints().

Now open the EmployeeCRUDService class from the Services folder and decorate it with [Authorize] attribute like this : 

[Authorize(Roles = "Admin")]
public class EmployeeCRUDService : 
EmployeeCRUD.EmployeeCRUDBase
{
  ...
  ...
}

We set the Roles property of [Authorize] to Admin. Recollect that IdentityServer's TestUser user1 has role configured to Admin (see ServerConfiguration class for more details).

The EmployeeCRUDService is now protected using JWT authentication. In order to invoke any of its methods you need to supply a valid JWT with the call. This JWT will be provided by the IdentityServer server application.

Now let's move on to the third part of the solution - the MVC client application.

Open the gRPC MVC client web application and go to its Startup class. We have protected the gRPC service using JWT. We would also like to protect the client app using OIDC. Add the following code to the ConfigureServices() method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    string connStr = this.config.
GetConnectionString("AppDb");


    services.AddAuthentication(o => {
        o.DefaultScheme = "Cookies";
        o.DefaultChallengeScheme = "oidc";
    })
            .AddCookie("Cookies", o =>
            {
                o.AccessDeniedPath = "/Account/AccessDenied";
            })
            .AddOpenIdConnect("oidc", o =>
            {
                o.ClientId = "client2";
                o.ClientSecret = "client_secret_code";
                o.SignInScheme = "Cookies";
                o.Authority = "https://localhost:5001";
                o.RequireHttpsMetadata = false;
                o.ResponseType = "code id_token";
                o.SaveTokens = true;
                o.GetClaimsFromUserInfoEndpoint = true;
                o.Scope.Add("employeesGrpcService");
                o.Scope.Add("roles");
                o.ClaimActions.MapUniqueJsonKey("role", "role");
                o.TokenValidationParameters = new
    TokenValidationParameters
                {
                    RoleClaimType = "role"
                };
            });
}

If you read my IdentityServer article series, this configuration should look familiar to you. Make sure to use the correct ClientId and ClientSecret as per your ServerConfiguration values. Also make sure to have scope for employeesGrpcService.

The MVC gRPC client application contains the HomeController class. Decorate it with [Authorize] as shown below:

[Authorize(Roles = "Admin")]
public class HomeController : Controller
{
...
}

Let's quickly test the configuration so far.

Run the IdentityServer server project, gRPC service project, and gRPC client project in the same sequence. If all goes well you will be shown a login page like this:

If you supply user1 credentials you will be able to log into the gRPC MVC client app but the actual call to the gRPC service will fail because we aren't passing a valid JWT to the gRPC service yet.

There are two ways to pass a JWT to the gRPC service. You can pass a JWT with each and every call made to the service OR you can set a JWT at the time of creating the gRPC channel.

Let's see the first possibility.

public IActionResult Index()
{
    var accessToken = HttpContext.GetTokenAsync
(OpenIdConnectParameterNames.AccessToken).Result;

    var channel = GrpcChannel.ForAddress
("https://localhost:5021");

    var client = new EmployeeCRUD.
EmployeeCRUDClient(channel);

    var headerMetadata = new Metadata();
    headerMetadata.Add("Authorization", 
$"Bearer {accessToken}");
    CallOptions callOptions = new CallOptions
(headerMetadata);

    Empty response1 = client.Insert(new Employee()
    {
        FirstName = "Tom",
        LastName = "Jerry"
    }, callOptions);

    Employees employees = client.SelectAll(new Empty(), 
callOptions);

    return View(employees);
}

Notice the code shown in bold letters. First, we get the JWT token by calling GetTokenAsync() on the HttpContext. We then create a Metadata object that holds the Authorization header information. The Authorization header value is set to Bearer followed by the JWT token. Then we create a CallOptions object based on the Metadata.

Then we call Insert() method to insert a new Employee. Notice the second parameter of Insert(). It's the CallOptions object we just created. To verify whether the new employee got added or not we call SelectAll() method (again we pass the CallOptions object as its second parameter).

The following figure shows a list of employees rendered on the Index view.

As you can see, you need to pass CallOptions object with each and every call to the gRPC service. You can also set the JWT while creating the gRPC channel. That way you need not explicitly pass CallOptions with each call. Let's see how that can be done. Take a look at the following modified Index() action.

public IActionResult Index()
{
    var accessToken = HttpContext.GetTokenAsync
(OpenIdConnectParameterNames.AccessToken).Result;

    var callCredentials = CallCredentials.
FromInterceptor((context, metadata) =>
    {
        metadata.Add("Authorization", 
$"Bearer {accessToken}");
        return Task.CompletedTask;
    });

    var channelOptions = new GrpcChannelOptions();
    channelOptions.Credentials = 
ChannelCredentials.Create(new SslCredentials(), 
callCredentials);


    var channel = GrpcChannel.ForAddress
("https://localhost:5021", channelOptions);

    var client = new EmployeeCRUD.EmployeeCRUDlient(channel);

    Empty response1 = client.Insert(new Employee()
    {
        FirstName = "Tom",
        LastName = "Jerry"
    });

    Employees employees = client.SelectAll(new Empty());

    return View(employees);
}

This  time we created CallCredentials object by adding Authorization header. We then created ChannelOptions object based on the CallCredentials just created. Finally we pass the ChannelCredentials object as the second parameter of ForAddress() method.

Make sure to remove the CallOptions parameter from Insert() and SelectAll() methods since we are now passing the JWT through CallCredentials and ChannelCredentials objects.

To know more about authentication and authorization in gRPC go here.

That's it for now! Keep coding!!


Bipin Joshi is an independent software consultant and trainer by profession specializing in Microsoft web development technologies. Having embraced the Yoga way of life he is also a meditation teacher and spiritual guide to his students. He is a prolific author and writes regularly about software development and yoga on his websites. He is programming, meditating, writing, and teaching for over 27 years. To know more about his ASP.NET online courses go here. More details about his Kriya and Meditation online course are available here.

Posted On : 25 October 2021


Tags : ASP.NET ASP.NET Core Data Access .NET Framework C# Visual Studio