OAuth 2.0 and OIDC for Microservices
Learn how OAuth 2.0 and OpenID Connect provide delegated authorization and federated identity for microservices architectures.
OAuth 2.0 and OIDC for Microservices
Modern microservice architectures rarely keep authentication simple. A monolith might validate a username and password directly. Microservices face a harder problem: how do you validate identity when a request has already traveled through three services, an API gateway, and a message queue? OAuth 2.0 and OpenID Connect solve this by externalizing authentication and distributing identity verification across service boundaries.
Forget adding a login form. The real problem is building a system where services trust incoming requests, users can grant third-party apps access without sharing passwords, and your architecture scales without turning every microservice into an authentication service.
When to Use / When Not to Use
| Scenario | Use OAuth 2.0 / OIDC | Notes |
|---|---|---|
| User authentication for web or mobile apps | Yes | Authorization Code flow with PKCE is standard |
| Service-to-service calls without shared secrets | Yes | Client Credentials flow works well |
| Third-party application access to user data | Yes | OAuth’s delegated authorization shines here |
| Machine-to-machine with static API keys | Consider | May be simpler if you already have key infrastructure |
| Microservices behind an API gateway | Yes | Centralized token validation at the gateway |
| Long-lived background job authentication | Consider | Refresh token rotation adds complexity |
| Stateless serverless functions | Yes | JWT validation works well for cold starts |
| Real-time bidirectional communication (WebSocket) | Caution | Token validation on connect, re-auth on reconnect |
Trade-offs
| Aspect | API Keys / Basic Auth | OAuth 2.0 / OIDC |
|---|---|---|
| Identity verification | None or weak | Strong (via IdP verification) |
| Delegated authorization | Not supported | First-class support |
| Token lifetime | Usually long-lived | Short-lived with refresh |
| Revocation | Immediate if key rotated | Delayed (until token expires) |
| Implementation complexity | Low | Higher |
| Standards compliance | None | RFC-compliant |
| User identity in tokens | Not available | Standard in OIDC ID tokens |
| Multi-factor auth support | External to system | Built into IdP flows |
When NOT to Use OAuth 2.0 / OIDC
- Simple internal services behind strict network segmentation: If network access is tightly controlled and all clients are trusted, overhead may not justify the benefit
- High-throughput, latency-critical paths where IdP is a single point of failure: JWT self-validation removes this risk; if you cannot tolerate IdP latency, use reference tokens
- Legacy systems that cannot participate in token flows: Integration work may exceed benefit
- Public APIs where you do not control the client: OAuth flows can be gamed; rate limiting and API keys may be more practical
What OAuth 2.0 Actually Does
OAuth 2.0 is an authorization framework, not an authentication protocol. That distinction matters. OAuth 2.0 lets a user grant a client application access to their resources on a resource server, without the client ever seeing the user’s credentials.
The classic example is a mobile app accessing your Google Drive files. You do not give the app your Google password. Instead, Google issues the app a token with limited permissions that you approved. The app presents that token to Google Drive, which validates it and serves the files.
The four roles in OAuth 2.0:
- Resource owner: The user who owns the data
- Client: The application requesting access
- Authorization server: The system that authenticates the user and issues tokens
- Resource server: The API that holds the protected resources
Authorization Code Flow
The Authorization Code flow is the standard for server-side web apps and SPAs with a backend. It involves more steps, but it keeps tokens away from browsers where they could be intercepted.
sequenceDiagram
participant User
participant Client as Client App
participant Auth as Authorization Server
participant Resource as Resource Server
User->>Client: Click "Login with Google"
Client->>User: Redirect to Authorization Server
User->>Auth: Enter credentials, approve access
Auth->>Client: Redirect with authorization code
Client->>Auth: Exchange code for tokens
Auth->>Client: Return access token + refresh token
Client->>Resource: Request with access token
Resource->>Client: Protected resource
The flow works like this: the client redirects the user to the authorization server with a client_id, redirect_uri, and requested scopes. The user authenticates and approves the request. The authorization server redirects back to the client with a short-lived authorization code. The client exchanges this code for tokens by calling the token endpoint directly, passing the code and a client_secret. The authorization server validates the code and returns an access token and optionally a refresh token.
The authorization code never appears in browser logs or server-side access logs. The token exchange happens on a secure channel between client and authorization server.
Client Credentials Flow
Client Credentials flow handles machine-to-machine communication where there is no user. A service needs to call another service’s API, and both services have their own credentials with the authorization server.
sequenceDiagram
participant ServiceA as Service A
participant Auth as Authorization Server
participant ServiceB as Service B
ServiceA->>Auth: Request token with client_id + client_secret
Auth->>ServiceA: Return access token
ServiceA->>ServiceB: Request with access token
ServiceB->>ServiceA: Protected resource
In this flow, Service A authenticates directly with the authorization server using its own credentials (client_id and client_secret). There is no user interaction. The authorization server returns a token that Service A uses to call Service B. Service B validates the token and serves the request.
This is how microservices talk to each other without sharing passwords or API keys. Each service has its own identity, and the authorization server tracks who can call what.
Refresh Token Flow
Access tokens are short-lived. JWTs typically expire in 5 to 15 minutes. Refresh tokens live longer and let clients obtain new access tokens without re-authenticating the user.
sequenceDiagram
participant Client
participant Auth as Authorization Server
Client->>Auth: Exchange refresh token for new access token
Auth->>Client: Return new access token + new refresh token
The client sends the refresh token to the authorization server. If the refresh token is valid and not revoked, the server issues a new access token and optionally a new refresh token. The old refresh token is invalidated.
This rotation prevents replay attacks where a stolen refresh token becomes useless after use. Most production systems rotate refresh tokens on every use.
OpenID Connect: Adding Identity to OAuth 2.0
OAuth 2.0 tells you whether a request is authorized. It does not tell you who the user is. OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0, standardized as OIDC in 2014.
OIDC introduces the ID token, a JWT that contains claims about the user. Where an OAuth 2.0 access token is opaque to the client, an ID token is self-contained and verifiable.
ID Token vs Access Token
Here is the key distinction.
An access token is a bearer token that grants access to a resource server. The resource server validates the token and decides whether to serve the request. The client cannot read the access token; it is opaque to the client.
An ID token is a signed JWT that contains user information. The client can read the ID token directly. It proves the user authenticated with the authorization server and contains claims like their email, name, and unique identifier.
sequenceDiagram
participant User
participant Client
participant Auth as Auth Server
User->>Client: Initiates login
Client->>Auth: Authentication request
Auth->>User: Login prompt
User->>Auth: Credentials
Auth->>Client: ID token + Access token
Client->>Client: Decode and read ID token
Note over Client: User info: sub, email, name
Client->>Auth: Use access token for API calls
For web applications, OIDC flows are similar to OAuth 2.0 flows but with an additional scope (openid) that signals the request is for identity information. The authorization server returns both an ID token and an access token.
Standard OIDC Scopes and Claims
OIDC defines standard scopes that map to sets of claims:
| Scope | Claims |
|---|---|
openid | sub (user identifier) |
profile | name, family_name, given_name, preferred_username, picture |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
The sub claim is the unique identifier for the user at the authorization server. It is the only required claim. Applications typically use the sub as the primary key for user records in their own databases.
JWT Tokens and Claims
JSON Web Tokens (JWTs) are the dominant token format for both access tokens and ID tokens in modern systems. A JWT is a base64-encoded, signed JSON object.
A JWT has three parts:
- Header: Algorithm and token type
- Payload: Claims (the data)
- Signature: Verifies the token was issued by the expected party
// Decoded JWT payload example
{
"iss": "https://auth.example.com",
"sub": "user_12345",
"aud": ["api.example.com", "mobile-app"],
"exp": 1710930000,
"iat": 1710926400,
"scope": "read:profile read:orders",
"email": "user@example.com",
"name": "Jane Developer"
}
The claims above include the issuer (iss), subject (sub), audience (aud), expiration time (exp), issued-at time (iat), scopes, and user information. The signature allows any party with the public key to verify the token was issued by the expected authorization server.
Why JWTs Work Well for Microservices
JWTs are self-contained. A service can validate a JWT without calling back to the authorization server for every request. The signature proves authenticity. The claims are visible to the service. The expiration time is enforced locally.
In microservices, network calls have latency. If every service has to call an authorization server to validate a token, you have created a bottleneck. JWT validation is local and fast.
Token Validation in Microservices
Validating a JWT is straightforward but has several steps that must all pass.
Signature Validation
The token was signed by the authorization server. To verify this, you need the public key corresponding to the private key that signed the token.
In practice, authorization servers publish their public keys at a well-known URL called the JWKS endpoint. Services cache these keys and refresh them periodically. When a token arrives, the service finds the matching key by the key ID (kid) in the token header and verifies the signature.
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json"),
);
async function validateToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "api.example.com",
});
return payload;
}
Expiration and Time Validation
Tokens carry an exp claim. Your service must reject tokens where exp is in the past. Clock skew between services can cause valid tokens to appear expired, so most systems allow a small tolerance (usually 30 to 60 seconds) when validating expiration.
Audience Validation
The aud claim specifies who the token is intended for. A token issued for mobile-app should not grant access to api.example.com. Your service must verify the audience claim matches its own identifier.
Scope and Permission Validation
Tokens may contain a scope or scp claim listing what the token allows. Before performing an action, your service should verify the token includes the required scope.
function requireScope(payload, requiredScope) {
const scopes = (payload.scope || "").split(" ");
if (!scopes.includes(requiredScope)) {
throw new Error(`Missing required scope: ${requiredScope}`);
}
}
Federated Identity Providers
Most organizations do not build their own authorization servers. They use identity providers (IdPs) that implement OAuth 2.0 and OIDC.
Keycloak
Keycloak is an open-source identity and access management solution. You deploy it as a service, define realms (tenant separation), clients, and users. It supports standard protocols and integrates with LDAP and Active Directory.
For microservice architectures, Keycloak can act as the authorization server, issuing tokens for your services and enforcing realm-level policies.
Auth0
Auth0 is a managed identity platform. It handles the infrastructure, handles edge cases like brute force protection and credential stuffing detection, and provides SDKs for every platform. You configure connections to social identity providers (Google, GitHub) and enterprise providers (SAML, OIDC).
Auth0 abstracts the complexity of multi-factor authentication, password policies, and credential storage.
Okta
Okta is an enterprise identity platform with similar capabilities. It focuses on workforce identity (employees accessing corporate applications) but also supports customer identity scenarios.
All three work with microservices architectures. The choice depends on whether you want self-hosted or managed, budget, and enterprise integration requirements.
Scope-Based Authorization
OAuth 2.0 scopes are coarse-grained permission labels. read:orders means you can read orders. The resource server decides what that means.
In practice, scope validation happens at two levels. The API gateway validates scopes before forwarding requests to services. Individual services validate scopes for fine-grained operations.
GET /api/orders/12345
Authorization: Bearer eyJhbGc...
// Service-level scope check
app.get("/api/orders/:id", async (req, res) => {
const token = req.token;
// Gateway already validated this scope exists
if (!token.scope.includes("read:orders")) {
return res.status(403).json({ error: "Forbidden" });
}
// Service enforces that user owns the order
const order = await db.getOrder(req.params.id);
if (order.userId !== token.sub) {
return res.status(403).json({ error: "Forbidden" });
}
res.json(order);
});
The gateway handles the coarse check. The service handles the fine-grained check. This separation keeps the gateway simple while allowing services to enforce their own business rules.
API Gateway Integration
The API gateway is the natural place to validate tokens before they reach your services. This centralizes authentication logic and keeps services focused on business logic.
graph TD
A[Client] --> B[API Gateway]
B --> C{Token Valid?}
C -->|No| D[401 Unauthorized]
C -->|Yes| E{Scope OK?}
E -->|No| F[403 Forbidden]
E -->|Yes| G[Route to Service]
G --> H[Order Service]
G --> I[Product Service]
H --> J[Return Response]
I --> J
When a request arrives, the gateway extracts the bearer token, validates it (signature, expiration, audience), checks the required scope, and either rejects the request or forwards it with the token claims attached as headers.
// Gateway middleware
async function authenticate(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
return res.status(401).json({ error: "Missing token" });
}
try {
const payload = await validateToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: "Invalid token" });
}
}
Services behind the gateway receive requests with a verified user context. They trust the gateway because the gateway is inside the network boundary and the token has already been validated.
See API Gateway for a comprehensive overview of gateway patterns including authentication, rate limiting, and failure handling.
Machine-to-Machine Authentication
Microservices talking to each other need identity too. The Client Credentials flow handles this.
Each service has its own client_id and client_secret. To call another service, it authenticates with the authorization server, receives a token, and uses that token for the call.
// Service-to-service token request
async function getServiceToken() {
const response = await fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.SERVICE_CLIENT_ID,
client_secret: process.env.SERVICE_CLIENT_SECRET,
scope: "read:inventory write:orders",
}),
});
const { access_token, expires_in } = await response.json();
return { token: access_token, expiresIn: expires_in };
}
// Cached token with refresh before expiry
const tokenCache = { token: null, expiresAt: 0 };
async function getValidToken() {
if (Date.now() >= tokenCache.expiresAt - 60000) {
const { token, expiresIn } = await getServiceToken();
tokenCache = { token, expiresAt: Date.now() + expiresIn * 1000 };
}
return tokenCache.token;
}
Services should cache tokens and refresh them before expiry. Calling the authorization server on every request adds latency and load. A short buffer before expiration prevents race conditions where a token expires mid-request.
Security Considerations
Token-based authentication introduces security concerns that password-based systems do not have.
Token storage: Access tokens in browser localStorage are vulnerable to XSS attacks. HttpOnly cookies are safer for browser-based applications. Service-to-service tokens should be stored in memory or secure vaults.
Token revocation: JWTs cannot be revoked before expiration. If a token is stolen, it remains valid until it expires. This is why short expiration times matter. For high-security applications, consider using reference tokens (opaque tokens that require a validation call) instead of JWTs.
Scope creep: Request only the scopes you need. A compromised token with admin:* is far more dangerous than one with read:profile.
Client secrets: Treat client secrets like passwords. Rotate them regularly. Do not commit them to source control. Use secret management systems for production deployments.
For more on securing your infrastructure, see Rate Limiting for protecting APIs from abuse and Service Mesh for mutual TLS between services.
Common Pitfalls
Treating access tokens as ID tokens: Access tokens are opaque to clients. Do not try to decode them for user information unless you control the authorization server and know the format.
Skipping audience validation: A token meant for one API should not work on another. Always validate the audience claim.
Long-lived tokens: Access tokens should be short (5 to 15 minutes). Refresh tokens handle session persistence. Long-lived access tokens are an unacceptable risk if leaked.
No token refresh handling: Clients must handle token expiration gracefully. Unhandled expiration mid-session produces confusing 401 errors for users.
Verifying only the signature: Signature validation alone is insufficient. Check expiration, audience, and issuer too.
Production Runbook
Failure Scenarios and Mitigations
Scenario: Authorization Server Unavailable
Symptoms: All requests start returning 401/403. Services cannot obtain tokens. Users cannot log in.
Diagnosis:
# Check authorization server health
curl -s https://auth.example.com/.well-known/openid-configuration | jq '.issuer'
# Check token endpoint availability
curl -s -X POST https://auth.example.com/oauth/token -d "grant_type=client_credentials" -d "client_id=test"
# Check JWKS endpoint
curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys | length'
Mitigation:
- If using JWTs with local validation, services should continue working until their cached JWKS expires
- If using reference tokens requiring validation calls, switch to allowlist mode temporarily
- Scale the authorization server if it is overloaded
- Check network policies and DNS resolution
Prevention:
- Run authorization server with high availability (multiple replicas)
- Use JWTs with local validation to eliminate single point of failure
- Cache JWKS keys with appropriate TTL (typically 1-24 hours)
- Monitor authorization server health and set alerts
Scenario: Client Credentials Leaked
Symptoms: Unauthorized usage detected in logs. Unexpected API calls from unknown sources. Unusual patterns in access logs.
Diagnosis:
# Review token issuance logs
# Look for tokens issued to your client_id at unusual times or from unexpected IPs
# Check token introspection for recent tokens
curl -s -X POST https://auth.example.com/oauth/introspect \
-d "token=<suspicious_token>" -d "client_id=<your_client_id>"
# Audit client usage
# Check which scopes were requested vs what your application normally uses
Mitigation:
- Immediately revoke the client secret: rotate credentials in IdP admin console
- Invalidate all existing tokens for that client (if IdP supports bulk revocation)
- Audit which resources were accessed with the compromised credentials
- Review logs for data exfiltration or unauthorized actions
- If tokens are short-lived, you may need to wait for expiry rather than revoke
Prevention:
- Store client secrets in Vault or a secrets manager, never in environment variables or code
- Use mTLS for service-to-service communication instead of client credentials where possible
- Set alerts for unusual token issuance patterns
- Implement IP allowlisting for client credential flows if IdP supports it
Scenario: JWT Validation Bypass via Algorithm Confusion
Symptoms: Security audit finds endpoints accepting tokens with “none” algorithm. Vulnerability scanners detect algorithm confusion attacks.
Diagnosis:
# Test your token validation with a tampered token
# RS256 token sent to HS256 endpoint can leak secret if misconfigured
# Check which algorithms your validation library accepts
# Most should only accept the expected algorithm (RS256, ES256, etc.)
Mitigation:
- Update token validation to explicitly specify and check the expected algorithm
- Reject tokens with “none” algorithm
- Do not accept different key types than expected (e.g., symmetric keys for asymmetric algorithms)
- Rotate signing keys if compromise is suspected
Prevention:
- Always specify expected algorithm explicitly in validation code
- Use a validation library that rejects algorithm confusion attacks
- Include algorithm in your token validation checks alongside signature verification
- Run security scans against your token validation endpoints
Scenario: Refresh Token Leak
Symptoms: Users report being logged out unexpectedly. Concurrent session anomalies. Access from unexpected locations.
Diagnosis:
# Check IdP for refresh token usage logs
# Look for refresh tokens being used from multiple IPs simultaneously
# Check active sessions for affected users
# In Keycloak: ./kcadm.sh get sessions <user-id>
# In Auth0: Check breach detection dashboard
Mitigation:
- If IdP supports per-user token revocation, revoke all refresh tokens for affected users
- Force re-authentication for affected users
- If using Auth0 or similar managed IdP, enable anomaly detection to auto-revoke on suspicious activity
- Notify affected users of the security event
Prevention:
- Use refresh token rotation (new refresh token on each use) to limit exposure
- Store refresh tokens in HttpOnly cookies, not localStorage
- Implement refresh token binding (token-bound references)
- Enable IdP anomaly detection (impossible travel, new device detection)
Observability Hooks
Metrics to Capture
| Metric | What It Tells You | Alert Threshold |
|---|---|---|
token_issuance_total | Token issuance rate by grant type | Unexpected grant type volume |
token_validation_failure_total | Validation failures by reason | >1% failure rate |
token_validation_duration_seconds | Token validation latency | p99 > 100ms |
jwks_cache_miss_total | JWKS fetches from IdP | >10% miss rate indicates cache misconfiguration |
active_sessions_total | Currently active refresh tokens | Sudden drop indicates revocation event |
oauth_error_total | OAuth errors by error code | Any increase in invalid_grant |
Logs to Collect
From API Gateway (structured logging):
{
"event": "token_validated",
"trace_id": "abc123",
"client_id": "my-service",
"grant_type": "client_credentials",
"scopes": ["read:orders", "write:inventory"],
"validation_result": "success|failure",
"failure_reason": "expired|invalid_signature|wrong_audience",
"auth_server": "keycloak",
"duration_ms": 5
}
{
"event": "token_issued",
"client_id": "my-service",
"grant_type": "authorization_code",
"user_id": "user_12345",
"scopes": ["openid", "profile", "email"],
"token_type": "access|refresh|id",
"expires_in": 3600,
"auth_server": "keycloak"
}
Key log fields: client_id, user_id (if applicable), grant_type, scopes, validation result, failure reason, auth server, duration.
Traces to Capture
Enable tracing in API gateway and authorization server. Key span attributes:
oauth.client_id: Client identifieroauth.grant_type: authorization_code, client_credentials, refresh_tokenoauth.scopes: Array of requested scopesoauth.validation.result: success, failureoauth.failure.reason: expired, invalid_signature, invalid_audience, insufficient_scope
Dashboards to Build
- OAuth/OIDC Health: Token issuance rate, validation success/failure ratio, error breakdown
- Token Lifecycle: Average token lifetime, refresh rate, revocation events
- Client Activity: Token usage by client, scope distribution, unusual client behavior
- Authorization Server: Request latency, error rate, JWKS cache hit ratio
Alerting Rules
# Token validation failures
- alert: TokenValidationFailures
expr: rate(token_validation_failure_total[5m]) > 0.01
labels:
severity: warning
annotations:
summary: "Token validation failure rate above 1%"
# Authorization server down
- alert: AuthorizationServerDown
expr: up{job="auth-server"} == 0
labels:
severity: critical
annotations:
summary: "Authorization server is unavailable"
# JWKS cache misses
- alert: JWKSMissRateHigh
expr: rate(jwks_cache_miss_total[5m]) / rate(jwks_cache_request_total[5m]) > 0.1
labels:
severity: warning
annotations:
summary: "JWKS cache miss rate above 10%"
# Unusual token issuance
- alert: UnusualTokenIssuance
expr: rate(token_issuance_total{grant_type="client_credentials"}[15m]) > 10 * avg(rate(token_issuance_total{grant_type="client_credentials"}[1h]))
labels:
severity: warning
annotations:
summary: "Unusual spike in client credential token issuance"
Quick Recap
- OAuth 2.0 is an authorization framework; OIDC adds identity on top with standardized user claims in ID tokens
- Authorization Code flow with PKCE is the standard for user-facing apps; Client Credentials flow is for service-to-service
- JWT validation must check signature, expiration, audience, and issuer; skipping any of these creates security gaps
- JWKS caching eliminates authorization server as a per-request bottleneck; cache keys appropriately (1-24h typical)
- API gateway centralizes token validation so services can trust incoming user context without re-validation
- Authorization server unavailability is a single point of failure; use JWT self-validation to make services resilient
- Algorithm confusion attacks (RS256 vs HS256) are a real vulnerability; always specify expected algorithm explicitly
- Refresh token rotation limits exposure if a token is stolen; enable rotation and store tokens in HttpOnly cookies
- Federated identity providers (Keycloak, Auth0, Okta) handle the complexity so you do not build auth yourself
Related Concepts
For understanding how centralized validation impacts system latency and reliability, see Distributed Caching for strategies to cache validation results effectively.
For a broader view of how microservices handle inter-service communication, the Microservices Roadmap covers architecture patterns, communication styles, and operational concerns.
When designing APIs that handle authentication and authorization, follow the guidelines in RESTful API Design to ensure consistent, secure interfaces.
Category
Related Posts
mTLS: Mutual TLS for Service-to-Service Authentication
Learn how mutual TLS secures communication between microservices, how to implement it, and how service meshes simplify mTLS management.
Secrets Management: Vault, Kubernetes Secrets, and Env Vars
Learn how to securely manage secrets, API keys, and credentials across microservices using HashiCorp Vault, Kubernetes Secrets, and best practices.
Service Identity: SPIFFE and Workload Identity in Microservices
Understand how SPIFFE provides cryptographic identity for microservices workloads and how to implement workload identity at scale.