gRPC and Azure App Service do not talk to each other by default. App Service on Linux terminates HTTP/2 connections in a way that needs explicit configuration before a gRPC service will respond correctly, and if you skip that step you will spend an afternoon staring at “unimplemented” or connection reset errors with no obvious cause. This article walks through a secured ASP.NET Core gRPC service hosted on a Linux Kestrel Azure App Service, protected with an Azure AD app registration and the client credentials flow.
The scenario is a machine-to-machine one. A trusted client application acquires an access token using its own client ID and secret (or certificate), attaches that token to every gRPC call, and the service validates the token before executing any RPC. There is no interactive user in this flow, which makes it a good fit for backend services calling other backend services.
Why gRPC on App Service needs extra configuration
Azure App Service serves regular HTTP/1.1 traffic by default. gRPC needs HTTP/2 end to end, including trailers, which is a different code path in the App Service front end. You have to turn on HTTP/2 explicitly in the App Service configuration, and you also have to tell Kestrel which port is dedicated to HTTP/2-only traffic through an app setting called HTTP20_ONLY_PORT. Miss either half of this and calls will fail even though the code compiles and runs fine locally.
This is the single biggest gotcha with gRPC on App Service, and it is worth internalizing before you look at any code: the port number you configure in Kestrel’s ConfigureKestrel call must be the exact same number you put into the HTTP20_ONLY_PORT app setting in the Azure Portal. If these two drift apart, even by a typo, the gRPC channel will simply refuse to connect.
Solution architecture

The design here is straightforward once you see it end to end. A confidential client application authenticates against Azure AD using the client credentials grant, receives an access token scoped to the gRPC service, and sends that token as a bearer token in the gRPC call metadata. The service, hosted on Azure App Service using the Linux Kestrel stack, validates the token using Microsoft.Identity.Web before letting the request reach the actual RPC implementation.
NuGet packages you need
Two packages do the heavy lifting on the service side. Grpc.AspNetCore brings in the gRPC server implementation and code generation tooling for ASP.NET Core, and Microsoft.Identity.Web handles the JWT bearer validation against Azure AD.
- Grpc.AspNetCore
- Microsoft.Identity.Web
Before writing any code, register an application in Azure AD for the daemon client following the standard client credentials flow setup (Microsoft’s daemon app scenario documentation is the reference for this). You will end up with a client ID, a client secret or certificate, and a scope that the service exposes and the client requests.
Configuring authentication and Kestrel
The Program.cs setup does two separate jobs: wiring up JWT bearer authentication against Azure AD, and configuring Kestrel to listen on the HTTP/2-only port that App Service expects. The authorization policy below goes a step further than a plain “require authenticated user” check. It inspects two specific claims on the access token, azp and azpacr, to confirm exactly which client application requested the token and how that client authenticated.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
{
// Validate id of application for which the token was created
// In this case the CC client application
validateAccessTokenPolicy.RequireClaim("azp", "b178f3a5-7588-492a-924f-72d7887b7e48");
// only allow tokens which used "Private key JWT Client authentication"
// https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
// Indicates how the client was authenticated. For a public client, the value is "0".
// If client ID and client secret are used, the value is "1".
// If a client certificate was used for authentication, the value is "2".
validateAccessTokenPolicy.RequireClaim("azpacr", "1");
});
});
builder.Services.AddGrpc();
// Configure Kestrel to listen on a specific HTTP port
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080);
// port must match the Azure App Service setup HTTP20_ONLY_PORT
options.ListenAnyIP(7179, listenOptions =>
{
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2;
});
});
The azp claim identifies the application ID of the client that requested the token, and pinning it to a specific GUID means only that one registered client application can call this service, even if other clients somehow obtained a technically valid Azure AD token. The azpacr claim tells you how the client authenticated itself: a value of 1 means client secret was used, 2 means a certificate was used. Requiring azpacr equal to 1 here rejects tokens obtained through weaker authentication paths. Port 8080 stays open for plain HTTP health checks and routing, while port 7179 is locked to HTTP/2 only and is the one that must match HTTP20_ONLY_PORT in the App Service configuration.
Wiring up the middleware pipeline
Once authentication and Kestrel are configured, the middleware pipeline itself looks like any other secured ASP.NET Core API. The only gRPC-specific addition is the MapGrpcService call inside UseEndpoints, and a plain HTTP GET endpoint that returns a simple text response, which is handy for confirming the App Service is actually running before you start debugging the gRPC channel itself.
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("GRPC service running...");
});
});
app.Run();
The order here matters, same as any ASP.NET Core pipeline: routing before authentication, authentication before authorization, and both before the endpoints are mapped. If you ever see a gRPC call getting an authentication challenge you did not expect, this is usually the first place to check, since a misordered pipeline is a common source of confusing 401s that have nothing to do with the token itself.
Defining the proto contract
The service needs a proto3 definition describing the RPC methods and message types. This example reuses the standard Greeter service shape that ships in Microsoft’s own gRPC samples, which keeps the focus on the security plumbing rather than on gRPC contract design.
syntax = "proto3";
option csharp_namespace = "GrpcAzureAppServiceAppAuth";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}
The .NET gRPC tooling generates the base classes and message types from this file at build time. In a real service you would replace this with your own domain-specific RPCs, but the authentication and hosting concerns covered here apply regardless of what the actual proto contract looks like.
Protecting the gRPC service implementation
With the proto generating a GreeterBase class, the actual implementation is a small C# class. The Authorize attribute ties the class to the policy defined earlier and to the JWT bearer scheme, so every call to SayHello goes through the claim checks before this method body ever runs.
using Grpc.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
namespace GrpcAzureAppServiceAppAuth;
[Authorize(Policy = "ValidateAccessTokenPolicy",
AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
If a caller sends a request without a bearer token, or with a token that fails either claim check, ASP.NET Core rejects the call before SayHello executes and the client receives an authentication failure rather than a gRPC-level error. That separation is useful when you are debugging, since it tells you immediately whether the problem is token validation or actual business logic.
Deploying the gRPC service to Azure App Service on Linux
Once the service code is ready, deploy it to an Azure App Service configured for Linux, following Microsoft’s own gRPC-on-App-Service documentation for the platform-level settings. Two things need to be turned on in the Azure Portal beyond a normal App Service deployment: HTTP/2 support, and the HTTP/2 proxy.

Both of these live under the Configuration and General settings blades for the App Service. Skipping the HTTP/2 proxy setting is a common mistake, because the app will deploy and start without any errors, and the plain HTTP GET endpoint will even respond correctly, but every gRPC call will fail since the front end is not proxying HTTP/2 frames through to Kestrel.

The HTTP20_ONLY_PORT application setting is where the port number from ConfigureKestrel gets wired into App Service. In this example the value is 7179, matching the ListenAnyIP call in Program.cs exactly. Get this number wrong and you will see connection failures that look like a networking or firewall problem, when the actual cause is a simple mismatch between the app setting and the code.
Building a client credentials test client
A minimal console client is enough to confirm the whole flow works end to end. On the client side you need Microsoft.Identity.Client for the token acquisition, plus Google.Protobuf, Grpc.Net.Client and Grpc.Tools for the gRPC plumbing itself.
- Microsoft.Identity.Client
- Google.Protobuf
- Grpc.Net.Client
- Grpc.Tools
The client credentials flow only works for confidential clients, meaning applications that can safely hold a secret or certificate, such as a backend service or a daemon. It is not appropriate for anything running on an end user’s device, since a secret embedded in a mobile app or single-page application is not actually secret. The example below uses a client secret for simplicity, but a client certificate with client assertions is the safer option for production, and switching between them is a small change to the ConfidentialClientApplicationBuilder call.
using Grpc.Net.Client;
using GrpcAzureAppServiceAppAuth;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
using Grpc.Core;
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets("0464abbd-c57d-4048-873d-d16355586e50")
.AddJsonFile("appsettings.json");
var configuration = builder.Build();
// 1. Client client credentials client
var app = ConfidentialClientApplicationBuilder
.Create(configuration["AzureADServiceApi:ClientId"])
.WithClientSecret(configuration["AzureADServiceApi:ClientSecret"])
.WithAuthority(configuration["AzureADServiceApi:Authority"])
.Build();
var scopes = new[] { configuration["AzureADServiceApi:Scope"] };
// 2. Get access token
var authResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
if (authResult == null)
{
Console.WriteLine("no auth result... ");
}
else
{
Console.WriteLine(authResult.AccessToken);
// 2. Use access token & service
var tokenValue = "Bearer " + authResult.AccessToken;
var metadata = new Metadata
{
{ "Authorization", tokenValue }
};
var handler = new HttpClientHandler();
var channel = GrpcChannel.ForAddress(
configuration["AzureADServiceApi:ApiBaseAddress"],
new GrpcChannelOptions
{
HttpClient = new HttpClient(handler)
});
CallOptions callOptions = new CallOptions(metadata);
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" }, callOptions);
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
The important part here is the Metadata object carrying the Authorization header, since gRPC calls do not use HttpClient headers the way a REST call would. The token is fetched once through AcquireTokenForClient, which MSAL caches internally, so repeated calls within the token’s lifetime reuse the cached token instead of hitting Azure AD every time. On a successful call, you should see the access token printed to the console followed by “Greeting: Hello GreeterClient”, confirming the round trip worked and the claim checks passed on the service side.
Production considerations and trade-offs
A few things are worth knowing before you take this pattern into production rather than a demo. First, App Service’s HTTP/2 support for gRPC has historically been less mature than what you get from Azure Container Apps or AKS, both of which are built around HTTP/2 and gRPC as first-class citizens rather than something bolted on afterward. If your workload is gRPC-heavy with high throughput requirements, it is worth benchmarking App Service against Container Apps before committing, since the HTTP/2 proxy layer on App Service adds overhead that a container-native platform avoids.
Second, anything sitting in front of App Service, such as Application Gateway or Azure Front Door, needs to support end-to-end HTTP/2 as well, or your gRPC calls will fail at that layer even though App Service itself is configured correctly. Not every load balancer or WAF product handles HTTP/2 trailers correctly, so this is worth testing explicitly rather than assuming it works because plain HTTPS works.
Third, the azp and azpacr claim checks shown here are a reasonable extra layer on top of standard token validation, but they only work reliably with the Microsoft identity platform’s v2.0 tokens, since claim names and behavior can differ across token versions and identity providers. If you swap Azure AD for another OpenID Connect provider, verify what equivalent claims that provider issues before reusing this exact policy.
Finally, client secrets are the easiest way to get started, but they expire, and rotating them without downtime needs planning, typically by registering two credentials on the app registration and rotating one at a time. A client certificate avoids the expiry-and-rotation headache at the cost of slightly more setup work, and is generally the better choice once this pattern moves past a proof of concept.
Leave a Reply