Service-to-Service Authentication in Microservices: A Practical Architecture Guide
By Aakash Ahuja Category: Practical Microservices Field Manual Reading time: 24–30 minutes Published: June 20, 2026
Service-to-service authentication in microservices fails when teams assume that an internal API call is safe because it came from inside the network. That assumption is wrong.
A microservice should not trust another microservice just because it is deployed in the same VPC, cluster, subnet, namespace, or backend environment. Every internal call should carry a verifiable service identity. If the call is made on behalf of a user, it should also carry user context. The receiving service should verify identity, derive tenant context from trusted claims, enforce authorization, and log the decision.
The practical rule is:
Internal network location is not identity. A service must prove who it is, what it is allowed to do, and which user or tenant context it is acting under.
This article is part of the Practical Microservices Field Manual. It follows the earlier articles on service boundaries, API contracts, and database ownership. Those topics matter because authentication is not a separate feature. It is part of every service boundary.

Table of contents
- What is service-to-service authentication in microservices?
- Why internal services should not be trusted by default
- Service authentication vs user authentication vs authorization
- What should a service-to-service request contain?
- How should Auth, UMS, and RBAC responsibilities be separated?
- Should every service verify JWTs locally or call the Auth service?
- How should tenant context be derived in service-to-service calls?
- What should go inside a service token?
- How should RBAC work across microservices?
- JWT vs mTLS vs service mesh: which pattern should you use?
- How should service secrets and keys be managed?
- What usually fails in service-to-service authentication?
- A practical implementation model for Python/FastAPI microservices
- Service-to-service authentication checklist
- Frequently Asked Questions About Service-to-Service Authentication in Microservices
- Key Takeaways
What is service-to-service authentication in microservices?
Service-to-service authentication is the process by which one microservice proves its identity to another microservice before the receiving service accepts the request.In a monolith, a function call happens inside one application boundary. In microservices, the same business operation may cross multiple services:
Frontend
→ Orders Service
→ Pricing Service
→ Availability Service
→ Wallet Service
→ Billing Service
→ Payments ServiceEvery hop is now a network call. Every network call is a trust boundary.
A service-to-service authentication model must answer:
- Which service is calling?
- Is that service active?
- Is that service allowed to call this endpoint?
- Is the call made on behalf of a user?
- Which user initiated the action?
- Which tenant does this action belong to?
- What permission is required?
- What scopes does the calling service have?
- What should be logged?
- What should happen if the token is expired, revoked, malformed, or issued for the wrong audience?
That distinction is non-negotiable.
Why internal services should not be trusted by default
The phrase "internal API" creates a false sense of safety.A request can come from inside the network and still be unsafe because:
- a service token may be leaked;
- a developer may call an internal endpoint manually;
- a compromised service may call another service;
- a staging or debug pathway may bypass checks;
- a worker may reuse a broad service credential;
- an SSRF flaw may reach internal endpoints;
- a misconfigured gateway may expose internal routes;
- a service may pass tenant IDs or user IDs that were never verified.
Being inside the network is not enough. Every request should be authenticated and authorized before accessing a resource.
For microservices, this means internal services need identity, scoped permissions, verification, and audit.
What "do not trust internal services by default" means
It does not mean every service should distrust every other service operationally.
It means every service should enforce a clear contract:
I will accept this request only if:
- the calling service proves its identity;
- the token is valid and intended for me or for this internal audience;
- the service has the required scope;
- the user context is valid if the action is user-scoped;
- the tenant context is derived from trusted claims;
- RBAC or policy allows the operation;
- the request is logged with enough traceability.
That is service-to-service authentication in practice.
Service authentication vs user authentication vs authorization
Most microservice authentication designs fail because they mix three different questions.1. Service authentication
Service authentication answers:
Which service is calling?
Example:
orders-service is calling pricing-service.This can be proven through:
- service JWT,
- mTLS certificate,
- workload identity,
- service mesh identity,
- signed internal token,
- API gateway/sidecar-attested identity.
2. User authentication
User authentication answers:
Which human user initiated the request?
Example:
user_123 initiated an order confirmation.This usually comes from:
- user JWT,
- session token,
- identity provider token,
- internal user token created by the Auth service.
3. Authorization
Authorization answers:
Is this service/user allowed to perform this operation on this resource in this tenant?
Example:
Can user_123, through orders-service, confirm order_456 for tenant_789?Authorization depends on:
- user role,
- service scope,
- tenant membership,
- object ownership,
- endpoint permission,
- product/module access,
- workflow state,
- business rule.
Comparison table
| Question | Answered by | Example |
|---|---|---|
| Who is calling? | Service authentication | orders-service |
| On whose behalf? | User authentication/delegation | user_123 |
| In which tenant? | Tenant context | tenant_789 |
| What can they do? | RBAC/policy | order.confirm |
| On which object? | Object-level authorization | order_456 |
| What happened? | Audit log | confirmed / denied / failed |
A service token proves service identity. It should not automatically grant every user action.
What should a service-to-service request contain?
A service-to-service request should carry service identity, optional user context, correlation context, and enough metadata for authorization and audit.A practical internal HTTP request may look like this:
Authorization: Bearer <user-token>
X-Service-Token: <service-token>
X-Correlation-Id: corr_abc123
X-Request-Id: req_456Or, if the call is purely system-initiated:
X-Service-Token: <service-token>
X-Correlation-Id: corr_abc123
X-Request-Id: req_456Recommended header roles
| Header | Purpose |
|---|---|
Authorization: Bearer | Human/user context, when the call is on behalf of a user |
X-Service-Token | Calling service identity |
X-Correlation-Id | Trace the workflow across services |
X-Request-Id | Trace a specific request attempt |
Idempotency-Key | Required for state-changing operations where retry can duplicate effects |
Service token and user token together
In many real systems, both are needed.
Example:
User clicks "Confirm Order"
Frontend → Orders Service
Orders Service → Wallet Service
Orders Service → Billing Service
Orders Service → Payments ServiceThe downstream service needs to know:
- the calling service is legitimate;
- the user is legitimate;
- the tenant is legitimate;
- the user has permission;
- the service is allowed to perform this internal call;
- the action is allowed in this workflow state.
X-Service-Token: orders-service identity
Authorization: Bearer user identityThis pattern avoids two bad extremes:
- trusting only the user token and ignoring which service is calling;
- trusting only the service token and losing user accountability.
Service-only calls
Some calls are not user-initiated:
- nightly reconciliation;
- webhook processing;
- background jobs;
- scheduled report generation;
- retry workers;
- inventory sync;
- payment status polling.
A background worker should not get admin-level access to every internal API.
How should Auth, UMS, and RBAC responsibilities be separated?
A clean service-to-service authentication design separates Auth, UMS, and RBAC.These services are related, but they should not become one vague "user service."
Recommended responsibility split
| Service | Owns | Should not own |
|---|---|---|
| Auth Service | Login identity, credential mapping, token issuance, token verification, service accounts, public keys, token blacklist, auth audit | Business permissions, user profile fields, product rules |
| UMS / User Management | Users, organizations, tenant memberships, profile attributes, user status, module access | Passwords, service token secrets, final permission decisions |
| RBAC Service | Roles, permissions, assignments, activity keys, policy checks | Credentials, profile attributes, business records |
| Business Services | Endpoint-level enforcement, tenant-scoped business rules, object-level checks, audit events | Token issuance, credential verification |
Why this separation matters
If every service validates external identity-provider tokens directly, authentication logic spreads everywhere.
If every service stores user roles independently, authorization becomes inconsistent.
If every service trusts tenant_id from the request body, tenant isolation becomes fragile.
A cleaner model:
Auth verifies identity.
UMS resolves user/tenant membership.
RBAC evaluates permission.
Business service enforces business rules and object-level checks.
Audit records the decision.A grounded design pattern
A practical pattern that keeps authentication logic from scattering across services looks like this:
- The Auth service owns token verification.
- Services do not talk directly to the external identity provider.
- Services use a shared SDK/middleware.
- Incoming requests are normalized into a standard
service_identityanduser_identityon the request context. - Service-to-service calls carry
X-Service-Token. - User-scoped service-to-service calls also carry
X-User-TokenorAuthorization. - RBAC checks are applied through a standard dependency/decorator such as
require(activity_key). tenant_idis derived from verified Auth/UMS context, not from frontend input.
Should every service verify JWTs locally or call the Auth service?
There are two common validation models.Model 1: Central verification call
Each service calls the Auth service:
Receiving Service → Auth Service /verifyPros:
- simple mental model;
- easy revocation checks;
- one place to change token validation logic;
- good early-stage pattern;
- easier to implement with an existing Auth SDK.
- the Auth service becomes a runtime dependency;
- higher latency;
- cascading failure risk if Auth is down;
- every endpoint may add auth network calls;
- needs caching and resilience.
Model 2: Local JWT verification using public keys
Each service verifies JWTs locally using JWKS/public keys:
Receiving Service → local JWT verificationPros:
- lower latency;
- fewer auth-service calls;
- better resilience;
- scales better for high request volume;
- avoids auth bottleneck.
- key rotation must be handled correctly;
- revocation/blacklist checks are harder;
- services need correct validation logic;
- token validation library mistakes become widespread if not centralized in an SDK.
Practical recommendation
Use a shared SDK/middleware either way.
Do not let every team write its own token validation code.
A practical maturity path:
| Stage | Pattern |
|---|---|
| Early stage | Services call the Auth service /verify through a shared SDK |
| Growth stage | SDK validates service tokens locally using JWKS and calls Auth only where needed |
| Mature stage | Service mesh / workload identity / mTLS for service identity plus token-based user context |
| High-security stage | Short-lived workload identity, mTLS, policy engine, strong audit, automated key rotation |
How should tenant context be derived in service-to-service calls?
Tenant context should come from verified identity and membership claims, not from untrusted request payloads.This rule matters more in multi-tenant systems than almost anything else:
Do not trust tenant_id from the frontend as the source of truth.A request body can contain tenant_id, project_id, org_id, or another tenant-like field for routing or compatibility reasons. But the service should validate that value against authenticated context.
Bad pattern
{
"tenant_id": "tenant_123",
"order_id": "order_456"
}Then service code does:
tenant_id = request.body.tenant_idThis is unsafe because the caller controls the body.
Better pattern
tenant_id = verified_user_identity.tenant_idOr:
tenant_id = Auth/UMS membership resolution for actor + requested contextIf the payload includes tenant_id, treat it as a requested context, not an authority.
Service-to-service tenant rule
For user-scoped calls:
tenant_id = derived from verified user token / membershipFor service-only calls:
tenant_id = explicitly scoped in service token, job context, or approved workflow stateFor cross-tenant platform jobs:
tenant scope must be explicit, audited, and tightly permissionedTenant context checklist
| Question | Good answer |
|---|---|
| Does every tenant-scoped operation have tenant context? | Yes |
| Is tenant context derived from verified identity? | Yes |
Is frontend tenant_id treated as authority? | No |
| Can service-only jobs act across all tenants? | Only with explicit privileged scope |
| Is tenant context logged? | Yes |
| Are object-level checks tenant-scoped? | Yes |
What should go inside a service token?
A service token should prove service identity and carry only the minimum claims needed for safe authorization.A typical internal service JWT could contain:
{
"iss": "auth-service",
"sub": "orders-service",
"type": "service",
"aud": "internal-services",
"scopes": [
"pricing.resolve",
"availability.check",
"billing.create_invoice"
],
"iat": 1760000000,
"exp": 1760000300,
"jti": "token_unique_id"
}Recommended service-token claims
| Claim | Purpose |
|---|---|
iss | Issuer, usually the Auth service |
sub | Service identity |
type | service, to distinguish from human/user token |
aud | Intended audience |
scopes | What the service can call |
iat | Issued time |
exp | Expiry |
jti | Unique token ID for trace/revocation |
tenant_id | Only if token is tenant-scoped |
env | Optional: prod/staging/dev isolation |
kid | Key ID in JWT header for rotation |
What should not be inside a service token
Avoid putting these in service tokens:
- service secrets;
- raw credentials;
- database passwords;
- secret-store ARNs;
- private keys;
- large permission objects;
- unbounded tenant access unless required;
- user profile data;
- sensitive business payload.
iss, sub, aud, exp, and iat are defined in RFC 7519.Service account database record
The Auth service may store a service-account record such as:
service_id
service_name
allowed_scopes
status
created_at
updated_atBut service secrets should be stored in a secret manager, not directly in the Auth database.
The database can store metadata. The secret manager stores secrets.
How should RBAC work across microservices?
RBAC should be enforced at the endpoint or operation boundary, not only at login.A user being authenticated does not mean they can perform every action.
A service being authenticated does not mean it can call every internal endpoint.
RBAC needs both user and service context
Example:
POST /orders/{id}:confirmThe receiving service should check:
| Check | Question |
|---|---|
| Service identity | Is the caller a trusted service? |
| Service scope | Can this service call order.confirm or this internal dependency? |
| User identity | Which user initiated the action? |
| Tenant context | Does this user belong to this tenant? |
| RBAC activity | Does the user have order.confirm? |
| Object-level rule | Can this user confirm this order? |
| Workflow state | Is this order in a confirmable state? |
Standard activity-key pattern
A clean implementation uses activity keys:
catalog.category.read
catalog.category.create
order.price
order.confirm
invoice.issue
payment.mark_received
rbac.role.assignThen each endpoint declares what it needs:
@router.post("/orders/{order_id}:confirm")
async def confirm_order(
order_id: str,
ctx = Depends(require("order.confirm"))
):
...The require("order.confirm") guard should:
- verify the service token;
- verify the user token if required;
- derive tenant context;
- call/check RBAC;
- attach identity context to the request state;
- deny if permission is absent;
- log the decision.
Do not replicate RBAC manually in every service
Bad pattern:
Each service has its own role-checking logic.
Each endpoint checks different fields.
Some endpoints check tenant_id.
Some endpoints do not.
Some endpoints skip RBAC for internal calls.Better pattern:
One shared middleware/dependency.
One standard identity context.
One standard activity key.
One standard audit shape.Consistency is the point. Broken object-level authorization and broken function-level authorization are among the most common API risks documented in the OWASP API Security Top 10, which is exactly what consistent per-endpoint enforcement is meant to prevent.
JWT vs mTLS vs service mesh: which pattern should you use?
JWT, mTLS, and service mesh solve related but different problems.JWT service tokens
JWT service tokens are useful when services need portable, signed, inspectable claims.
Use JWT service tokens when:
- you need scopes;
- you need audience validation;
- you need service identity in application logic;
- services are HTTP/API based;
- you want a simple starting point;
- you do not yet have a service mesh.
- bearer tokens can be replayed if stolen;
- token lifetime and key rotation matter;
- validation must be implemented correctly;
- secret handling still matters.
mTLS
mTLS authenticates both sides of a TLS connection using certificates.
Use mTLS when:
- you need transport-level service identity;
- you want to reduce bearer-token replay risk;
- you can manage certificates and rotation;
- you need stronger service-to-service channel authentication.
- mTLS proves workload/channel identity, not necessarily user intent;
- application authorization is still needed;
- certificate lifecycle must be managed;
- tenant/user context still needs a separate model.
Service mesh
A service mesh can move service identity, mTLS, policy enforcement, routing, retries, telemetry, and some authorization concerns into sidecars/proxies or infrastructure. NIST's guidance on service-mesh-based microservices describes how identity, mutual authentication, and policy can be standardized at the infrastructure layer (NIST SP 800-204A).
Use a service mesh when:
- the number of services is growing;
- services are in Kubernetes or similar orchestrated environments;
- you need uniform mTLS;
- you need centralized traffic policy;
- you want consistent telemetry and access control;
- platform team maturity exists.
- operational complexity;
- debugging can become harder;
- not a replacement for application-level RBAC;
- not a shortcut for bad API contracts.
Workload identity / SPIFFE-style identity
Workload identity gives services cryptographically verifiable identities tied to runtime/workload characteristics rather than long-lived static secrets.
Use workload identity when:
- static service secrets are becoming risky;
- workloads are dynamic;
- services are spread across clusters/clouds;
- automated identity issuance and rotation matter;
- zero-trust service identity is a priority.
Decision table
| Situation | Reasonable pattern |
|---|---|
| Small number of services | Service JWT through a shared SDK |
| Python/FastAPI services with central Auth | X-Service-Token + user token + middleware |
| High request volume | Local JWT verification with JWKS |
| Strong network-level service identity needed | mTLS |
| Kubernetes platform with many services | Service mesh + mTLS |
| Dynamic workloads / high security | SPIFFE/SPIRE-style workload identity |
| User-scoped business actions | Service identity + user context + RBAC |
| Background jobs | Scoped service identity + explicit tenant/job context |
| External APIs | API gateway + external auth + internal service identity |
Choose based on blast radius, team maturity, service count, latency, compliance, and operations capability.
How should service secrets and keys be managed?
Service secrets should not live in source code, environment dumps, logs, or ordinary database rows.Use a secret manager.
For example, a service may have:
SERVICE_ID=orders-service
SERVICE_SECRET=stored in a secret manager
AUTH_BASE_URL=https://auth.internalThe service can exchange its secret for a short-lived service token.
Recommended secret rules
| Rule | Reason |
|---|---|
| Store service secrets in a secret manager | Avoid secrets in DB/code |
| Rotate secrets | Limit blast radius |
| Use short-lived tokens | Reduce replay window |
| Never log raw tokens | Prevent log-based credential leaks |
| Never put tokens in URLs | URLs leak through logs/proxies |
| Scope service credentials | Limit what a compromised service can do |
| Separate prod/staging/dev secrets | Prevent environment crossover |
| Avoid shared super-service credentials | Preserve accountability |
| Audit token issuance | Track service activity |
| Revoke/disable service accounts | Support incident response |
Logging rule
Do not log:
- access tokens;
- refresh tokens;
- service secrets;
- passwords;
- private keys;
- raw authorization headers;
- secret-store ARNs where sensitive;
- full auth request bodies;
- SQL params containing sensitive values.
What usually fails in service-to-service authentication?
Failure 1: "Internal means trusted"
Teams skip auth for internal endpoints.
That creates hidden admin surfaces.
A better rule:
No internal endpoint without authentication unless it is explicitly public, harmless, and documented.Health checks may be exceptions, but even health checks should be carefully scoped.
Failure 2: Services trust frontend tenant IDs
The frontend sends:
{
"tenant_id": "tenant_123"
}The service accepts it.
This is dangerous. Tenant IDs from request bodies should be validated against verified identity context.
Failure 3: User token is forwarded without service identity
Downstream service sees the user but not which service is calling.
This loses service accountability.
A compromised or unintended service can reuse the user token path.
Failure 4: Service token is used without user accountability
Downstream service sees only:
orders-serviceBut does not know:
which human user initiated the actionThis breaks audit and user-level authorization for user-scoped actions.
Failure 5: Broad service scopes
A service token says:
scope: *This is usually a bad idea.
Services should get narrow scopes such as:
pricing.resolve
billing.invoice.create
inventory.availability.checkFailure 6: Every service decodes JWTs differently
One service checks audience. Another does not. One checks expiry. Another accepts expired tokens. One checks type=service. Another forgets.
This is why token validation belongs in a shared SDK/middleware.
Failure 7: RBAC is skipped for internal calls
The team says:
RBAC is checked at the frontend.That is not enough.
Every service boundary should enforce the permission needed for that operation.
Failure 8: Tokens are logged
Tokens leak through logs, traces, exception payloads, HTTP debugging, or APM tools.
Mask them aggressively.
Failure 9: Service accounts never expire or rotate
Long-lived service secrets become permanent blast-radius problems.
Use short-lived tokens and secret rotation.
A practical implementation model for Python/FastAPI microservices
This model fits a Python microservice system where the Auth service is the authority and services call each other frequently.Design goal
Every incoming request should be normalized into a standard identity context:
request.state.service_identity
request.state.user_identity
request.state.tenant_context
request.state.correlation_idThen endpoint code should not manually parse tokens.
Request types
| Request type | Required identity |
|---|---|
| External user request | User token |
| Internal user-scoped service call | Service token + user token |
| Internal system job | Service token |
| Webhook | Provider signature + tenant resolution after verification |
| Admin/platform operation | Strong service/admin identity + explicit scope |
Incoming middleware responsibilities
Middleware should:
- Read
X-Service-Tokenif present. - Read
AuthorizationorX-User-Tokenif present. - Verify the service token through the Auth SDK or local JWKS validation.
- Verify the user token through the Auth SDK or local validation.
- Reject requests with no valid identity unless the endpoint is explicitly public.
- Attach identities to
request.state. - Extract or resolve tenant context from verified identity.
- Attach a correlation ID.
- Mask sensitive headers in logs.
- Allow an endpoint-level RBAC guard to enforce activity permission.
Outgoing SDK responsibilities
A service client SDK should:
- Attach
X-Service-Token. - Forward the user token only when the downstream call is user-scoped.
- Attach a correlation ID.
- Attach an idempotency key where needed.
- Avoid logging raw headers.
- Retry only safe/idempotent calls.
- Fail closed when a service token cannot be obtained.
- Use internal base URLs only for internal services.
Endpoint RBAC guard
Each protected endpoint should declare the permission:
@router.post("/categories")
async def create_category(
payload: CategoryCreate,
ctx = Depends(require("catalog.category.create"))
):
...The guard should check:
- service identity;
- user identity where required;
- tenant context;
- activity permission;
- object-level rule where needed;
- module/product access where needed.
Example flow: user calls Orders, Orders calls Pricing
1. User sends request to Orders Service with user token.
- Orders middleware verifies user token.
- Orders derives tenant context.
- Orders checks RBAC: order.price or order.confirm.
- Orders calls Pricing Service with:
- X-Service-Token: orders-service
- Authorization/X-User-Token: user token
- X-Correlation-Id
- Pricing verifies the orders-service token.
- Pricing verifies or trusts validated user context according to policy.
- Pricing derives tenant context from verified token/context.
- Pricing checks whether orders-service can call pricing.resolve.
- Pricing returns the result.
- Both services log the request, decision, and correlation ID.
Example flow: payment webhook
Webhook flows are different.
A payment gateway webhook should not rely on normal user tokens.
Safe pattern:
1. Payment service receives the webhook.
- Payment service reads the raw body.
- Payment service verifies the provider signature.
- Only after signature verification does it resolve tenant/payment gateway config.
- Payment service records the provider event ID.
- Payment service deduplicates the event.
- Payment service updates payment state through owned logic.
- Payment service emits/audits the event.
Do not resolve tenant-scoped database access before verifying the webhook signature.
Service-to-service authentication checklist
Use this before adding or approving an internal API.Identity checklist
- [ ] Does every internal endpoint require service identity?
- [ ] Is there a standard service token format?
- [ ] Does the token identify the calling service?
- [ ] Does the token have
type=serviceor equivalent? - [ ] Does the token have expiry?
- [ ] Does the token have audience validation?
- [ ] Are service accounts active/inactive in the Auth service?
- [ ] Can service tokens be revoked or blacklisted?
User context checklist
- [ ] Does user-scoped work carry user identity downstream?
- [ ] Is service identity still present when user identity is forwarded?
- [ ] Can the system distinguish human action from a system job?
- [ ] Are user tokens validated through a standard SDK/middleware?
- [ ] Is user identity logged without logging raw tokens?
Tenant checklist
- [ ] Is
tenant_idderived from verified identity/membership? - [ ] Is frontend
tenant_idignored or validated? - [ ] Does every tenant-scoped query include tenant context?
- [ ] Are service-only jobs explicitly tenant-scoped?
- [ ] Are cross-tenant admin jobs separately permissioned and audited?
Authorization checklist
- [ ] Does every endpoint declare a required activity key?
- [ ] Is RBAC enforced for internal calls?
- [ ] Are service scopes checked?
- [ ] Are object-level authorization checks applied?
- [ ] Are product/module gates enforced where needed?
- [ ] Are permission denials audited?
Secrets checklist
- [ ] Are service secrets stored in a secret manager?
- [ ] Are secrets absent from source code and DB tables?
- [ ] Are tokens short-lived?
- [ ] Are keys rotated?
- [ ] Are tokens masked in logs?
- [ ] Are prod/staging/dev credentials separated?
Operational checklist
- [ ] Are correlation IDs propagated?
- [ ] Are auth failures logged safely?
- [ ] Are service-token validation failures observable?
- [ ] Are internal-only endpoints kept private/VPC-only if used?
- [ ] Are health/debug endpoints scoped and protected?
- [ ] Are auth dependencies resilient and monitored?
Frequently Asked Questions About Service-to-Service Authentication in Microservices
What is service-to-service authentication in microservices?
Service-to-service authentication is how one microservice proves its identity to another microservice before the receiving service accepts the request. It prevents internal APIs from trusting requests only because they came from the same network or cluster.
Is a user JWT enough for service-to-service calls?
Usually no. A user JWT identifies the human actor, but it does not identify which internal service is calling. For user-scoped internal calls, pass both service identity and user context.
Is a service token enough for user actions?
No. A service token identifies the calling service, but it does not explain which user initiated the action. For user-scoped business operations, keep user context so RBAC, audit, and object-level authorization can work.
Should microservices verify JWTs locally?
They can, if there is a shared SDK/middleware, JWKS/key rotation, audience validation, expiry checks, and consistent claim handling. Early systems often start with a central Auth service verification call and later move toward local verification for scale and resilience.
What is the difference between authentication and authorization?
Authentication proves identity. Authorization decides what that identity can do. A service can be authenticated and still not be authorized to call a specific endpoint or perform a specific action.
Should tenant ID come from the request body?
Tenant ID should not be trusted directly from the request body. It should be derived from verified user/service context or validated against authenticated membership and scope.
Is mTLS better than JWT for service-to-service authentication?
mTLS and JWT solve different problems. mTLS proves service identity at the transport layer, while JWT can carry application-level claims such as issuer, subject, audience, expiry, and scopes. Many mature systems use both.
Do I need a service mesh for service-to-service authentication?
Not always. A small or early-stage system can use service JWTs, shared middleware, and RBAC guards. A service mesh becomes more useful when service count, traffic policy, mTLS, observability, and platform maturity justify the operational overhead.
Key Takeaways
- Internal network location is not identity.
- Every internal service call should carry verifiable service identity.
- User-scoped calls should carry both service identity and user context.
- Authentication and authorization are different controls.
- Tenant context must be derived from verified identity, not blindly trusted from request bodies.
- RBAC should be enforced at endpoint boundaries, including internal calls.
- Service secrets belong in a secret manager, not source code or ordinary database rows.
- JWT, mTLS, service mesh, and workload identity are complementary patterns, not interchangeable magic fixes.
- A shared SDK/middleware is the practical way to keep service authentication consistent across microservices.
Continue learning: Practical Microservices Field Manual
This article is part of the Practical Microservices Field Manual.Recommended next reads:
- Microservices Architecture Design
- API Contracts in Microservices
- Database Ownership in Microservices
- Testing Microservices Without Fooling Yourself (coming soon)
Part of the series
Designing Scalable Microservices- 1.Microservices Architecture Design: Why Services Fail, When to Avoid Them, and How to Draw Boundaries That Survive Production
- 2.API Contracts in Microservices: How to Design Interfaces That Survive Production
- 3.Database Ownership in Microservices: Beyond the Database-per-Service Rule
- 4.Service-to-Service Authentication in Microservices: A Practical Architecture Guide← you are here
- 5.Testing Microservices Without Fooling Yourselfcoming soon
- 6.Observability for Microservices Before Productioncoming soon
