How Does Service-to-Service Authentication Work in Microservices?
Service-to-service authentication (S2S authentication) is the mechanism by which one microservice proves its own identity when calling another microservice.
For example:
Order Service
|
| "I am Order Service"
v
Payment Service
Payment Service should not trust the request merely because it came from the internal network. It should establish:
Who is calling me?
↓
Is this really Order Service?
↓
Is Order Service allowed to call me?
↓
Is it allowed to perform THIS operation?
A very common standards-based approach is OAuth 2.0 Client Credentials, where the calling service authenticates to an authorization server and obtains an access token representing the application itself, not an end user. OAuth 2.0 defines Client Credentials specifically for confidential clients acting on their own behalf; Microsoft likewise documents it as a standard server-to-server scenario. RFC Editor
1. What Is Service-to-Service Authentication?
Consider these microservices:
Order Service
Payment Service
Inventory Service
Notification Service
When Order Service creates an order, it may need to call Payment Service:
Client
|
v
Order Service
|
| POST /api/payments
v
Payment Service
The question is:
How does Payment Service know the request actually came from an authorized Order Service?
That's the purpose of service-to-service authentication.
Without authentication:
Order Service --------+
|
Inventory Service ----+----> Payment Service
|
Attacker -------------+
Payment Service cannot safely assume:
Internal network = trusted
Instead:
Caller
|
| Credential / Identity
v
Payment Service
|
Authenticate caller
|
Authorize caller
v
Execute operation
2. Why Do We Need It?
Imagine Payment Service exposes:
POST /api/payments
Order Service legitimately calls it:
Order Service
|
v
Payment Service
But if there is no authentication, another compromised workload might also call it:
Compromised Service
|
| POST /api/payments
v
Payment Service
The receiving service needs a cryptographically verifiable identity.
Service-to-service authentication helps prevent:
- unauthorized internal calls
- impersonation
- lateral movement
- gateway bypass
- unrestricted access between workloads
- anonymous access to internal APIs
3. Authentication vs Authorization
This distinction remains important for service identities.
Authentication
Payment Service asks:
Who is calling me?
Result:
Caller = Order Service
Authorization
Payment Service then asks:
What is Order Service allowed to do?
For example:
Order Service
Allowed:
payments.create
Not Allowed:
payments.refund
payments.admin
Therefore:
Service Token
|
v
Authentication
|
v
Caller = Order Service
|
v
Authorization
|
| Has payments.create?
v
Payment Controller
4. Common S2S Authentication Methods
There are several approaches:
| Method | Typical use |
|---|---|
| OAuth 2.0 Client Credentials | General service-to-service authentication |
| Managed Identity | Azure-hosted services |
| Workload Identity | Kubernetes/cloud-native environments |
| mTLS | Strong certificate-based mutual authentication |
| API Key | Simpler integrations; less expressive |
| Service Mesh identity | Kubernetes/service-mesh environments |
For ASP.NET Core enterprise microservices, a very common model is:
OAuth 2.0
+
Client Credentials
+
JWT Access Token
Microsoft documents Client Credentials as an application-identity flow used for server-to-server interactions without user interaction. Microsoft Learn
5. Client Credentials Flow
Suppose:
Order Service
|
v
Payment Service
Order Service does not simply send:
Username = OrderService
Password = abc123
to Payment Service.
Instead, an authorization server is introduced:
Authorization Server
/ Identity Provider
^
|
Client Credentials
|
Order Service
|
Access Token
|
v
Payment Service
Examples of authorization servers/identity providers include:
- Microsoft Entra ID
- Keycloak
- Auth0
- Duende IdentityServer
6. Complete Flow
The complete flow is:
Order Service
|
| 1. Authenticate itself
| Client ID + credential
v
Authorization Server
|
| 2. Validate service identity
|
| 3. Issue access token
v
Order Service
|
| 4. Authorization:
| Bearer <access_token>
v
Payment Service
|
| 5. Validate access token
|
| 6. Authenticate caller
|
| 7. Check permission
v
PaymentController
|
v
Process Payment
RFC 6749 defines essentially this flow: the confidential client authenticates to the authorization server, requests an access token using grant_type=client_credentials, and receives an access token if authentication and authorization succeed. RFC Editor
7. Step 1 — Register the Services
Conceptually, register Order Service as a client:
Client ID:
order-service
and give it a credential.
For example:
Client ID:
order-service
Client Secret:
**************
Payment API might be registered as:
payment-api
with an application permission such as:
payments.process
8. Client ID vs Client Secret
Think of:
Client ID
=
Service username / public identifier
Client Secret
=
Service password / confidential credential
For example:
ClientId:
order-service
ClientSecret:
7fK9**********
However, long-lived client secrets are not the strongest option.
Where supported, prefer:
Managed Identity
Certificate
Workload Identity
over manually managed shared secrets. Microsoft notes that confidential clients can authenticate using secrets or certificates, and managed identities avoid exposing underlying credentials to application administrators/code. Microsoft Learn
9. Step 2 — Order Service Requests a Token
Conceptually:
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
client_id=order-service
client_secret=********
scope=payments.process
The authorization server validates:
Does order-service exist?
↓
Is credential valid?
↓
Can order-service request
payments.process?
↓
YES
↓
Generate access token
The OAuth standard requires the token endpoint to authenticate the confidential client, and credentials must be protected with TLS. RFC Editor
10. Token Response
Conceptually:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}
The access token represents:
Order Service
not:
Syed / End User
That's the critical difference.
11. Example Service Token
Conceptually, a JWT access token could contain claims like:
{
"sub": "order-service",
"aud": "payment-api",
"scope": "payments.process",
"iss": "https://identity.example.com",
"exp": 1790690000
}
Meaning:
Subject
= Order Service
Audience
= Payment API
Permission
= payments.process
Issuer
= Trusted Authorization Server
Expiration
= Token expiry
12. Step 3 — Call Payment Service
Order Service sends:
POST /api/payments
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json
{
"orderId": 1001,
"amount": 75000
}
Flow:
Order Service
|
| Bearer Service Token
v
Payment Service
13. Step 4 — Payment Service Validates the Token
Payment Service validates:
Signature
↓
Issuer
↓
Audience
↓
Expiration
↓
Required permission
For example:
aud = payment-api
scope = payments.process
If everything is correct:
Authenticated caller
=
Order Service
ASP.NET Core protected APIs normally validate incoming access tokens using authentication middleware/handlers, then authorization policies restrict access to the appropriate caller permissions. Microsoft Learn
14. ASP.NET Core Example Architecture
Let's create:
MicroservicesDemo
│
├── OrderService
│
└── PaymentService
The real authorization server is external:
Identity Provider
^
|
Get Token
|
OrderService
|
Bearer Token
|
v
PaymentService
15. Required NuGet Packages
Order Service
If using Microsoft Entra ID/MSAL for acquiring application tokens:
dotnet add package Microsoft.Identity.Client
Payment Service
For JWT bearer validation:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
The exact acquisition library depends on your identity provider.
16. Order Service — appsettings.json
For demonstration:
{
"Identity": {
"TenantId": "YOUR-TENANT-ID",
"ClientId": "ORDER-SERVICE-CLIENT-ID",
"ClientSecret": "ORDER-SERVICE-CLIENT-SECRET",
"PaymentApiScope": "api://PAYMENT-API-CLIENT-ID/.default"
},
"Services": {
"PaymentService": "https://localhost:7201"
}
}
Important
Do not store the real production secret in committed appsettings.json.
Use:
Local development
-> .NET User Secrets
Azure
-> Managed Identity / Key Vault
Kubernetes
-> Workload Identity / secured secret management
17. TokenService
Create:
Services/TokenService.cs
using Microsoft.Identity.Client;
namespace OrderService.Services;
public class TokenService
{
private readonly IConfiguration _configuration;
private readonly IConfidentialClientApplication _app;
public TokenService(IConfiguration configuration)
{
_configuration = configuration;
var tenantId =
configuration["Identity:TenantId"];
var clientId =
configuration["Identity:ClientId"];
var clientSecret =
configuration["Identity:ClientSecret"];
_app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(
$"https://login.microsoftonline.com/{tenantId}")
.Build();
}
public async Task<string> GetPaymentTokenAsync()
{
var scope =
_configuration["Identity:PaymentApiScope"]!;
var result =
await _app
.AcquireTokenForClient(
new[] { scope })
.ExecuteAsync();
return result.AccessToken;
}
}
The important line is:
AcquireTokenForClient(...)
It means:
Acquire an access token for the application itself, not for an interactive user.
MSAL supports this client-credential acquisition for confidential applications. Microsoft Learn
18. PaymentClient
Create:
Services/PaymentClient.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace OrderService.Services;
public class PaymentClient
{
private readonly HttpClient _httpClient;
private readonly TokenService _tokenService;
public PaymentClient(
HttpClient httpClient,
TokenService tokenService)
{
_httpClient = httpClient;
_tokenService = tokenService;
}
public async Task<HttpResponseMessage> ProcessPaymentAsync(
int orderId,
decimal amount)
{
var accessToken =
await _tokenService.GetPaymentTokenAsync();
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue(
"Bearer",
accessToken);
var request = new
{
OrderId = orderId,
Amount = amount
};
return await _httpClient.PostAsJsonAsync(
"/api/payments",
request);
}
}
The important part is:
Authorization: Bearer <service-token>
19. Order Service — Program.cs
using OrderService.Services;
var builder =
WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddSingleton<TokenService>();
builder.Services.AddHttpClient<PaymentClient>(
client =>
{
client.BaseAddress =
new Uri(
builder.Configuration[
"Services:PaymentService"]!);
});
var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();
20. OrdersController
using Microsoft.AspNetCore.Mvc;
using OrderService.Services;
namespace OrderService.Controllers;
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly PaymentClient _paymentClient;
public OrdersController(
PaymentClient paymentClient)
{
_paymentClient = paymentClient;
}
[HttpPost]
public async Task<IActionResult> CreateOrder()
{
var orderId = 1001;
var amount = 75000m;
var response =
await _paymentClient
.ProcessPaymentAsync(
orderId,
amount);
if (!response.IsSuccessStatusCode)
{
return StatusCode(
500,
"Payment failed");
}
return Ok(new
{
OrderId = orderId,
Message =
"Order and payment completed"
});
}
}
21. Payment Service — appsettings.json
{
"Authentication": {
"Authority": "https://login.microsoftonline.com/YOUR-TENANT-ID/v2.0",
"Audience": "PAYMENT-API-CLIENT-ID"
}
}
Your actual audience value must match how the Payment API is registered/configured with the identity provider.
22. Payment Service — Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder =
WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services
.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority =
builder.Configuration[
"Authentication:Authority"];
options.Audience =
builder.Configuration[
"Authentication:Audience"];
options.RequireHttpsMetadata = true;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"ProcessPayment",
policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(context =>
{
var scopes =
context.User.FindFirst("scp")?.Value
?? context.User.FindFirst("scope")?.Value;
var roles =
context.User.FindAll("roles")
.Select(x => x.Value);
return
roles.Contains("payments.process")
||
(scopes?.Split(' ')
.Contains("payments.process")
?? false);
});
});
});
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
For Microsoft Entra application-only tokens, application permissions/app roles are normally the relevant authorization mechanism; delegated scp scopes apply to user-delegated scenarios. Microsoft documents protected APIs as supporting both delegated permissions and application permissions. Microsoft Learn
23. PaymentRequest
namespace PaymentService.Models;
public class PaymentRequest
{
public int OrderId { get; set; }
public decimal Amount { get; set; }
}
24. PaymentsController
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PaymentService.Models;
namespace PaymentService.Controllers;
[ApiController]
[Route("api/payments")]
[Authorize]
public class PaymentsController : ControllerBase
{
[HttpPost]
[Authorize(Policy = "ProcessPayment")]
public IActionResult ProcessPayment(
PaymentRequest request)
{
return Ok(new
{
request.OrderId,
request.Amount,
Message =
"Payment processed successfully"
});
}
}
Now Payment Service does not simply trust Order Service.
It requires:
Valid Access Token
+
Correct Issuer
+
Correct Audience
+
Valid Lifetime
+
Required Application Permission
25. Complete Execution Flow
When the client creates an order:
Client
|
| POST /api/orders
v
Order Service
Order Service needs Payment Service.
It first obtains a service token:
Order Service
|
| Client ID
| Client Credential
v
Microsoft Entra ID
|
| Authenticate Order Service
|
v
Access Token
Then:
Order Service
|
| POST /api/payments
|
| Authorization:
| Bearer <access-token>
v
Payment Service
Payment Service:
JWT received
|
v
Validate Signature
|
v
Validate Issuer
|
v
Validate Audience
|
v
Validate Expiration
|
v
Authenticate Caller
|
v
Caller = Order Service
|
v
Check payments.process
|
+------ No ------> 403 Forbidden
|
Yes
|
v
PaymentsController
|
v
Process Payment
26. What Happens If No Token Is Sent?
Order Service
|
| POST /api/payments
| No Authorization header
v
Payment Service
Result:
401 Unauthorized
Because Payment Service cannot authenticate the caller.
27. What Happens If Token Is Valid but Permission Is Missing?
Suppose:
Caller = Inventory Service
Permission:
inventory.read
but Payment Service requires:
payments.process
Result:
403 Forbidden
Remember:
401
=
Authentication problem
403
=
Authorization problem
28. Client Credentials vs User Token
This is one of the most important distinctions.
User authentication
User
|
| Login
v
Identity Provider
|
| User Access Token
v
Order Service
The token represents:
User
Service authentication
Order Service
|
| Client Credentials
v
Identity Provider
|
| Application Access Token
v
Payment Service
The token represents:
Order Service
Microsoft specifically describes Client Credentials as allowing a web service to use its own identity instead of impersonating a user. Microsoft Learn
29. What If Payment Service Needs the User Identity?
Now consider:
User
|
| User Token
v
Order Service
|
| ???
v
Payment Service
Payment Service might need to know:
Which user initiated this operation?
This is different from pure application authentication.
One standards-based option in Microsoft Entra environments is the On-Behalf-Of (OBO) flow:
User
|
| User Token for Order API
v
Order Service
|
| Exchange user assertion
v
Identity Provider
|
| Downstream token
| for Payment API
v
Order Service
|
| Downstream token
v
Payment Service
Microsoft distinguishes these two scenarios:
- Client Credentials → application acts as itself.
- On-Behalf-Of → API calls another API on behalf of the authenticated user. Microsoft Learn
30. Don't Blindly Forward the Original JWT
Avoid assuming this is always correct:
User JWT
|
v
Order Service
|
| SAME JWT
v
Payment Service
Why?
The original token might have:
{
"aud": "order-api"
}
That means it was intended for:
Order API
not necessarily:
Payment API
Payment Service should reject a token whose audience is not appropriate for Payment Service.
Instead use the identity architecture appropriate to the scenario:
Service acting itself
↓
Client Credentials
Service acting for user
↓
On-Behalf-Of / delegation
31. Client Secret vs Certificate vs Managed Identity
There are multiple ways for Order Service to prove its identity.
Option 1 — Client Secret
Order Service
|
Client ID
Client Secret
|
v
Identity Provider
Simple, but you must securely manage and rotate the secret.
Option 2 — Certificate
Order Service
|
Private Key / Certificate
|
v
Identity Provider
This avoids a simple shared password and can provide stronger assurance when managed correctly. Microsoft explicitly supports certificates for confidential client authentication. Microsoft Learn
Option 3 — Managed Identity
In Azure:
Azure-hosted
Order Service
|
| Managed Identity
v
Microsoft Entra ID
|
| Access Token
v
Payment API
No application-managed client secret is required.
Microsoft notes that with managed identities, Azure manages the underlying credentials so applications don't need to expose them. Microsoft Learn
For Azure-hosted services, this is often preferable when the target resource/API and architecture support it.
32. mTLS Alternative
Service-to-service authentication does not have to use JWT.
Another approach is mutual TLS:
Order Service
|
| Client Certificate
|
| mTLS
v
Payment Service
|
| Server Certificate
Both sides authenticate each other:
Order verifies Payment
+
Payment verifies Order
This is especially common with:
Kubernetes
Service Mesh
Istio
Linkerd
Internal high-security systems
33. JWT vs mTLS
| Feature | JWT/OAuth | mTLS |
|---|---|---|
| Service identity | Yes | Yes |
| Permissions/scopes | Strong support | Usually separate policy |
| Certificate required | Not necessarily | Yes |
| Works well with APIs | Yes | Yes |
| Token expiration | Yes | N/A in same sense |
| Certificate lifecycle | Optional | Required |
| User delegation | Supported by OAuth flows | Not inherently |
They can also be combined:
mTLS
+
OAuth Access Token
where mTLS authenticates the connection/workload and the token carries application authorization information.
34. API Key Alternative
Simpler systems sometimes use:
X-API-Key: abc123
Flow:
Order Service
|
| API Key
v
Payment Service
This can work for simpler controlled integrations, but compared with OAuth it usually provides weaker identity/authorization semantics and more manual:
Key distribution
Rotation
Revocation
Permission management
Auditing
For larger microservices systems, workload identity or OAuth-based S2S authentication is generally more scalable.
35. Should Every Microservice Have Different Credentials?
Generally, yes.
Avoid:
All Microservices
ClientId = common-service
Secret = same-secret
Because if one service is compromised, the attacker may impersonate all services.
Prefer:
Order Service
identity = order-service
Inventory Service
identity = inventory-service
Notification Service
identity = notification-service
Then authorization can be precise:
Order Service
-> payments.process
Inventory Service
-> inventory.update
Notification Service
-> notifications.send
This follows least privilege.
36. Token Caching
Do not unnecessarily request a new token for every API call.
Bad:
Request #1 -> Get token
Request #2 -> Get token
Request #3 -> Get token
Request #4 -> Get token
Request #5 -> Get token
Prefer token acquisition libraries that cache/reuse valid access tokens:
Get Token
|
v
Cache
|
+--> API Call #1
+--> API Call #2
+--> API Call #3
When approaching expiration:
Acquire new token
MSAL provides token caching behavior around its token acquisition APIs, so applications should use the library rather than implementing an ad-hoc token request for every outgoing request. Microsoft Learn
37. Advantages
Strong service identity
The receiving API knows which application is calling.
No user required
Background services can communicate independently.
Fine-grained authorization
You can define:
orders.read
payments.process
inventory.update
Short-lived credentials
Access tokens expire automatically.
Centralized identity
An authorization server manages application identities and permissions.
Scalable
Works across many services and instances.
Zero-Trust friendly
Internal network location is not treated as sufficient authentication.
38. Disadvantages
Additional infrastructure
You need an authorization/identity system.
Credential management
Client secrets/certificates require secure handling unless workload/managed identities eliminate this burden.
Token acquisition complexity
Services must obtain and cache tokens.
Configuration complexity
You must correctly configure:
Client ID
Credential
Audience
Permissions
Issuer
Token validation
Identity provider dependency
Token acquisition can depend on the authorization server, although already-issued locally verifiable tokens can often continue to be validated until they expire.
39. Best Practices
For production microservices:
- Give each microservice its own identity.
- Prefer OAuth Client Credentials for application-only calls where appropriate.
- Prefer Managed/Workload Identity when the platform supports it.
- Prefer certificates over long-lived shared secrets where appropriate.
- Never commit client secrets to source control.
- Store unavoidable secrets in Key Vault or another secret manager.
- Use HTTPS/TLS.
- Use short-lived access tokens.
- Validate issuer.
- Validate audience.
- Validate signature.
- Validate token lifetime.
- Authorize using application permissions/roles.
- Apply least privilege.
- Cache access tokens appropriately.
- Don't blindly forward user access tokens.
- Use OBO/delegation when downstream processing genuinely needs user context.
- Restrict network access in addition to authentication.
- Never log access tokens or client secrets.
- Rotate credentials/certificates according to policy.
40. Key Points
The most important flow to remember is:
Service A
|
| Client Credential /
| Workload Identity
v
Authorization Server
|
| Access Token
v
Service A
|
| Bearer Access Token
v
Service B
|
+--> Validate Token
|
+--> Identify Service A
|
+--> Check Permission
|
v
Execute Request
And remember the distinction:
USER AUTHENTICATION
User
↓
Identity Provider
↓
User Token
↓
Microservice
SERVICE AUTHENTICATION
Microservice A
↓
Authorization Server
↓
Application Token
↓
Microservice B
41. Interview Questions and Answers
Q1. How does service-to-service authentication work?
Answer:
One common approach is OAuth 2.0 Client Credentials. The calling microservice authenticates itself to an authorization server using its application identity, receives a short-lived access token, and sends that token to the target microservice. The target service validates the token and then authorizes the caller using application permissions or roles.
Q2. Which OAuth flow is used for service-to-service communication?
For an application acting as itself:
Client Credentials flow.
OAuth defines it for confidential clients acting on their own behalf. RFC Editor
Q3. Does Client Credentials authenticate a user?
No.
It authenticates:
Application / Microservice
not:
End User
Q4. How does Payment Service know Order Service called it?
Order Service presents an access token representing its application identity. Payment Service validates the token and checks the caller's application permission, for example:
payments.process
Q5. Should all Microservices share the same client secret?
No.
Each service should generally have its own identity and only the permissions it requires.
Q6. Where should client secrets be stored?
Production secrets should be stored in a secure secret-management system such as Azure Key Vault rather than committed into appsettings.json.
When possible, eliminate manually managed secrets through Managed Identity or Workload Identity.
Q7. Client Credentials vs On-Behalf-Of?
Client Credentials
=
Service acts as itself
On-Behalf-Of
=
Service calls downstream API
for an authenticated user
Microsoft supports both scenarios and distinguishes application-only Client Credentials from delegated OBO calls. Microsoft Learn
Q8. Can we use mTLS instead of OAuth?
Yes.
mTLS authenticates services through certificates. OAuth tokens are particularly useful when you also need application-level permissions, audiences and centralized authorization.
They can also be used together.
Q9. Should we get a new access token for every request?
Normally, no. Use a token acquisition library/cache and reuse a valid access token until it needs renewal. Microsoft Learn
Q10. What happens if Service A has a valid token but lacks permission?
Authentication succeeds but authorization fails:
403 Forbidden
Interview Summary
A strong interview answer is:
For service-to-service authentication, each microservice should have its own workload/application identity. A common implementation uses OAuth 2.0 Client Credentials: Service A authenticates to the authorization server using its application credential or workload identity, obtains a short-lived access token targeted at Service B, and sends it in the Bearer header. Service B validates the token's signature, issuer, audience and lifetime, identifies Service A, and checks its application permissions before processing the request. In Azure, I prefer Managed Identity or workload identity where possible so that long-lived client secrets don't need to be stored in the application. If the downstream call must preserve user context, I would consider a delegated flow such as On-Behalf-Of rather than Client Credentials.
The easiest way to remember it is:
Order Service
|
| "Give me permission
| to call Payment API"
v
Identity Provider
|
| Service Access Token
v
Order Service
|
| Bearer Token
v
Payment Service
|
| Who are you?
| -> Order Service
|
| What can you do?
| -> payments.process
v
Process Payment
Service-to-service authentication proves the identity of the calling microservice; authorization determines what that microservice is permitted to do.