DEV Community

Dinesh Dunukedeniya
Dinesh Dunukedeniya

Posted on

Microservice-to-Microservice Authentication in .NET Using IdentityServer and JWT

In a microservices architecture, secure communication between services is just as important as securing external API calls. One of the most common and robust ways to handle this in .NET is by using IdentityServer, JWT tokens, and the Client Credentials flow.

In this article, you’ll learn how to:

  • Authenticate microservices using Client Credentials
  • Request and validate JWT tokens
  • Pass tokens between services securely
  • Optimize token management with caching
  • Apply best practices for real-world deployments.

Why Client Credentials Flow?

The Client Credentials flow is designed for machine-to-machine communication. Unlike user-based authentication (e.g., Authorization Code flow), there’s no interactive login. Instead, each service identifies itself using a ClientId and ClientSecret, and IdentityServer issues an access token that can be used to call other services.

When to use it:

  • Service A needs to call Service B directly.
  • No user context is involved.
  • Both services are trusted and run in a secure environment.

Trusted Services and Secure Environment

When we say "both services are trusted and run in a secure environment", it means:
Trusted Services

  • Both microservices are owned and controlled by the same organization (or within a trusted partner ecosystem).
  • Each service is registered in IdentityServer with its own ClientId and ClientSecret.
  • Services rely on IdentityServer as the trust broker — if a service presents a valid JWT from IdentityServer, other services trust it.

Example:
A Billing API (Service A) calls a Payment Processor API (Service B) within your company's internal infrastructure. Both are part of your platform and use IdentityServer for token issuance.

Secure Environment

  • Services communicate over a private and encrypted network (e.g., AWS VPC, Azure VNet, Kubernetes internal network).
  • All traffic is secured with TLS (HTTPS) to prevent token interception.
  • Secrets (e.g., ClientSecret) are stored securely in tools like Azure Key Vault or AWS Secrets Manager.
  • IdentityServer is either private or protected with firewalls, API gateways, and access controls.

Example:
Your services are deployed in Kubernetes and communicate over an internal service mesh (like Istio), where mTLS enforces encryption and mutual authentication.

⚠️ If your services are NOT in a trusted or secure environment (e.g., exposed to the public internet or involve third-party APIs), you should:

  • Validate the audience in the JWT token strictly.
  • Use an API Gateway with rate limiting, throttling, and authentication enforcement.
  • Consider mTLS in addition to JWT for an extra security layer.
  • Implement scopes and claims-based authorization to enforce least privilege.

Token Flow Diagram

Here’s how authentication works between two microservices:

+-----------------+           +---------------------+          +-----------------+
| Service A       |           | IdentityServer      |          | Service B       |
| (Caller)        |           | (Auth Provider)     |          | (Resource API)  |
+-----------------+           +---------------------+          +-----------------+
       |                              |                               |
       |   1. Request Token           |                               |
       |----------------------------->|                               |
       |                              |                               |
       |         2. JWT Token         |                               |
       |<-----------------------------|                               |
       |                              |                               |
       |  3. Call API with Token      |                               |
       |------------------------------------------------------------->|
       |                              |                               |
       |          4. Validate Token                                   |
       |                              |                               |
       |<-------------------------------------------------------------|

Enter fullscreen mode Exit fullscreen mode

IdentityServer Setup

First, configure a client in IdentityServer for service-to-service communication:

new Client
{
    ClientId = "service-a",
    ClientSecrets = { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.ClientCredentials,
    AllowedScopes = { "service-b-api" }
}

Enter fullscreen mode Exit fullscreen mode

This allows Service A to request a token for the scope service-b-api and then use it when calling Service B.


Requesting a Token in .NET

Service A will use the IdentityModel library to request a token:

using IdentityModel.Client;
using System.Net.Http;

public class TokenService
{
    private readonly HttpClient _httpClient;

    public TokenService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetTokenAsync()
    {
        var disco = await _httpClient.GetDiscoveryDocumentAsync("https://identityserver-url");
        if (disco.IsError) throw new Exception(disco.Error);

        var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = "service-a",
            ClientSecret = "secret",
            Scope = "service-b-api"
        });

        if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);

        return tokenResponse.AccessToken;
    }
}

Enter fullscreen mode Exit fullscreen mode

Calling Another Microservice with JWT

Once we have a token, we can attach it to the Authorization header:

using IdentityModel.Client;

public class ServiceBClient
{
    private readonly HttpClient _httpClient;
    private readonly TokenService _tokenService;

    public ServiceBClient(HttpClient httpClient, TokenService tokenService)
    {
        _httpClient = httpClient;
        _tokenService = tokenService;
    }

    public async Task<string> GetDataAsync()
    {
        var token = await _tokenService.GetTokenAsync();
        _httpClient.SetBearerToken(token);

        var response = await _httpClient.GetAsync("https://service-b/api/data");
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }
}

Enter fullscreen mode Exit fullscreen mode

Validating JWT in the Receiving Service

Service B must validate the JWT token. Add JWT Bearer authentication:

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://identityserver-url";
        options.RequireHttpsMetadata = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false
        };
    });

builder.Services.AddAuthorization();

Enter fullscreen mode Exit fullscreen mode

Apply authentication and authorization middleware:

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

Enter fullscreen mode Exit fullscreen mode

Optimizing with Token Caching

You should avoid requesting a token for every call. Instead, cache it until it expires:

public class CachedTokenService
{
    private readonly TokenService _tokenService;
    private string _cachedToken;
    private DateTime _expiresAt;

    public CachedTokenService(TokenService tokenService)
    {
        _tokenService = tokenService;
    }

    public async Task<string> GetTokenAsync()
    {
        if (!string.IsNullOrEmpty(_cachedToken) && DateTime.UtcNow < _expiresAt)
            return _cachedToken;

        var token = await _tokenService.GetTokenAsync();
        _cachedToken = token;
        _expiresAt = DateTime.UtcNow.AddMinutes(50); // assuming 1-hour expiry

        return _cachedToken;
    }
}

Enter fullscreen mode Exit fullscreen mode

Automating Token Injection with DelegatingHandler

You can also create a custom DelegatingHandler to automatically add tokens:

public class TokenHandler : DelegatingHandler
{
    private readonly CachedTokenService _tokenService;

    public TokenHandler(CachedTokenService tokenService)
    {
        _tokenService = tokenService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await _tokenService.GetTokenAsync();
        request.SetBearerToken(token);
        return await base.SendAsync(request, cancellationToken);
    }
}

Enter fullscreen mode Exit fullscreen mode

Register it in Program.cs:

builder.Services.AddHttpClient<ServiceBClient>()
    .AddHttpMessageHandler<TokenHandler>();

Enter fullscreen mode Exit fullscreen mode

Now, every call to Service B will automatically include a valid JWT.


Best Practices for Production

🔒 Secure Secrets: Store ClientSecret in a secret manager (e.g., Azure Key Vault or AWS Secrets Manager).

📜 Logging & Monitoring: Log token acquisition failures and monitor IdentityServer availability.

🚀 Use Delegating Handlers: Automate token injection for all service-to-service calls.


Summary

Use IdentityServer and Client Credentials flow for secure microservice-to-microservice communication.

  • Retrieve JWT tokens using IdentityModel.
  • Validate tokens in each service using JwtBearer.
  • Optimize with token caching and DelegatingHandler.
  • Follow security best practices for secret management and logging.

By implementing this approach, you ensure that your microservices communicate securely and efficiently in a production environment.

Top comments (0)