OAuth 2.0 and OIDC for Microservices
Learn how OAuth 2.0 and OpenID Connect enable delegated authorization and federated identity in 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.
Introduction
| 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}`);
}
}
SSO Patterns in Microservices
SSO means you log in once and then access multiple services without being asked for credentials again. OAuth 2.0 and OIDC make this work by sharing authentication through a common IdP.
How SSO Works with OIDC
The IdP holds the user’s session. When you redirect to the IdP from any client app, it checks for an existing session cookie first:
sequenceDiagram
participant User
participant ClientA as Client App A
participant ClientB as Client App B
participant IdP as Identity Provider
User->>ClientA: Access App A (no session)
ClientA->>User: Redirect to IdP login
User->>IdP: Authenticate
IdP->>User: Set session cookie
IdP->>ClientA: Redirect with code
ClientA->>IdP: Exchange code for tokens
IdP->>ClientA: ID token + Access token
ClientA->>User: Logged in to App A
User->>ClientB: Access App B (no session)
ClientB->>User: Redirect to IdP
User->>IdP: Already authenticated (cookie)
IdP->>User: Redirect with code (no login)
ClientB->>IdP: Exchange code for tokens
IdP->>ClientB: ID token + Access token
ClientB->>User: Logged in to App B
That session cookie is what makes the second login disappear. The IdP sees it and skips the prompt.
Session State Considerations
You can manage session state a few different ways:
IdP-managed sessions: The IdP owns the session completely. RPs use short-lived tokens and do not track their own user sessions. This is the standard OAuth/OIDC approach.
Shared session store: Applications keep local sessions but subscribe to logout events through a shared store or the IdP’s back-channel logout endpoint. This gets you faster local session invalidation than waiting for tokens to expire.
// IdP-managed session validation
async function validateUserSession(token) {
// Token validation is local (JWT) or via introspection (reference token)
const payload = await validateToken(token);
// Check if token is still valid (not revoked, not expired)
// IdP session state is NOT checked here - that's the IdP's job
return payload;
}
Same-Site Cookie Considerations
Browsers restrict how cookies work across sites, which directly impacts OAuth redirects:
- SameSite=Strict: Cookies only go to the same site that set them. OAuth redirects break because the IdP redirect crosses site boundaries.
- SameSite=Lax: Cookies travel on top-level navigations from other sites. Works for most OAuth flows but silently fails for iframe-based silent auth.
- SameSite=None; Secure: Cookies work everywhere. Required for cross-site OAuth but introduces CSRF risk.
Chrome 80 and later default to Lax. SPAs work around this by keeping redirect URIs on the same origin, or by using the storage access API for cross-origin token storage.
Identity Platform SSO Features
The major platforms layer additional features on top of the basic protocol:
Session management: Auth0, Okta, and Keycloak give administrators consoles to view active sessions, force logout, set lifetime policies, and distinguish passive from active authentication requirements.
Application integration: IdPs track metadata (redirect URIs, logout URIs, PKCE requirements) for every registered client and enforce consistent security policies across all of them.
Multi-IdP support: Large enterprises sometimes need to authenticate against multiple IdPs simultaneously (corporate IdP plus social providers, for instance). Platforms like Auth0 handle this at the connection level rather than the application level.
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.
Token Security Analysis: Common Attack Vectors
Understanding how tokens get compromised changes how you think about design decisions.
Authorization Code Interception: In poorly configured systems, authorization codes can be intercepted via browser history, referrer headers, or server logs. Always use PKCE for public clients and ensure code_verifier complexity meets security requirements.
Token Replay Attacks: Refresh tokens intercepted in transit can be replayed to obtain new access tokens. Mitigation includes token rotation, binding refresh tokens to device fingerprints, and monitoring for impossible travel patterns at the IdP level.
Scope Manipulation: Malicious clients may attempt to escalate privileges by modifying the scope parameter in token exchange requests. Always validate that granted scopes match what was originally requested, and reject responses containing scopes you did not ask for.
Client Impersonation: Without proper client authentication, a malicious actor could craft token requests that appear to come from your registered application. Use client_secret for confidential clients, mTLS certificates for service-to-service flows, and client_id binding in token responses.
MFA Implementation Patterns via OIDC
OIDC lets you layer additional authentication factors without building MFA logic into your application code.
Major identity providers (Keycloak, Auth0, Okta) support MFA configuration at the authorization server level. When MFA is enabled, the IdP enforces it before issuing tokens — your services just receive the result.
The amr (Authentication Methods References) claim in ID tokens tells you which factors were used:
// Example ID token payload with MFA information
{
"sub": "user_12345",
"amr": ["pwd", "otp"], // Password + One-Time Password (TOTP)
"auth_time": 1710930000,
"acr": "http://schemas.openid.net/claims/acr/values/authenticator"
}
Step-up Authentication: For sensitive operations, services can require tokens with specific acr (Authentication Context Class Reference) values indicating higher assurance. Users who authenticated with only password get a lower-privilege token; those with MFA get tokens that can access sensitive endpoints.
Patterns for MFA Integration:
- Configure MFA policies at the IdP (not in your application code)
- Use
amrclaims to audit MFA usage in your services - Implement step-up authentication for sensitive operations by validating
acrvalues - For APIs called by automated systems, use mTLS client certificates instead of MFA (MFA is designed for human users)
Common Pitfalls / Anti-Patterns
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 Failure Scenarios
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
Interview Questions
Expected answer points:
- OAuth 2.0 is an authorization framework for delegated access; OIDC is an identity layer on top of OAuth 2.0
- OAuth 2.0 answers "what can this client access?" while OIDC answers "who is the user?"
- OIDC adds ID tokens (signed JWTs) containing user claims like email, name, and sub identifier
- Use OAuth 2.0 when you only need authorization (e.g., service-to-service calls with Client Credentials)
- Use OIDC when you need user identity information for your application (user-facing apps needing authentication)
- Both can coexist; an OIDC flow returns both access tokens and ID tokens
Expected answer points:
- PKCE (Proof Key for Code Exchange) adds a cryptographic verifier to prevent authorization code interception attacks
- Public clients cannot safely store client secrets, making them vulnerable to code interception
- PKCE works by having the client generate a random code_verifier, send its hash (code_challenge) with the auth request, and prove possession of the verifier when exchanging the code
- An intercepted authorization code alone cannot be exchanged without knowing the original code_verifier
- RFC 7636 standardizes PKCE and it is now recommended for ALL OAuth flows, not just public clients
Expected answer points:
- Extract the token from the Authorization header and parse the JWT structure (header, payload, signature)
- Signature validation: fetch public keys from the IdP's JWKS endpoint, match by kid, verify cryptographic signature using the matching key
- Expiration check (exp claim): reject tokens where exp is in the past, accounting for clock skew tolerance (30-60 seconds)
- Issuer validation (iss claim): verify the token was issued by your expected authorization server
- Audience validation (aud claim): confirm the token is intended for your service, not another API
- Optional but recommended: scope validation against required permissions before performing actions
Expected answer points:
- JWTs are self-contained: validation is local, no network call needed, better latency and availability
- Reference tokens are opaque: require a call to the authorization server for validation, adding latency and a dependency
- JWT drawback: cannot be revoked before expiration; if compromised, you must wait for expiry
- Reference token advantage: can be revoked immediately, useful for high-security scenarios
- Hybrid approach: short-lived JWTs for normal operations, reference tokens for high-privilege actions requiring immediate revocation capability
- For most microservices, JWTs with short expiration (5-15 minutes) balance performance and security adequately
Expected answer points:
- localStorage is accessible via JavaScript, making it vulnerable to XSS attacks that can exfiltrate tokens
- HttpOnly cookies are inaccessible to JavaScript, protecting tokens from XSS-based theft
- HttpOnly cookies are also protected against CSRF by using `SameSite` attribute and CSRF tokens
- localStorage persists across browser sessions until explicitly cleared; cookie expiration is controlled
- For browser-based applications, HttpOnly cookies with Secure flag (HTTPS-only) is the recommended approach
- Refresh tokens should always use HttpOnly cookies; access tokens can use memory (not stored) for highest security
Expected answer points:
- On each refresh token exchange, the IdP issues a new access token AND a new refresh token
- The old refresh token is invalidated immediately after the exchange
- If a stolen refresh token is used: the legitimate refresh also occurs, causing a token mismatch the IdP can detect
- Detection triggers revocation of all refresh tokens for that user session, limiting exposure
- Rotation limits the window of opportunity: a stolen refresh token becomes useless after its first use by the attacker
- Must implement proper token storage (HttpOnly cookies) and handle the case where rotation fails (token reuse detection)
Expected answer points:
- An attacker crafts a token signed with a symmetric algorithm (e.g., HS256) using the public key as the secret
- If the server accepts both asymmetric algorithms (RS256) and symmetric (HS256), the attacker can forge tokens
- The server's public key (meant for RS256 verification) becomes the HMAC secret for HS256 validation
- Prevention: explicitly specify the expected algorithm in validation code (e.g., only accept "RS256")
- Reject tokens with "none" algorithm entirely
- Use a validation library that defaults to rejecting algorithm confusion rather than accepting it
- Rotate signing keys immediately if compromise is suspected
Expected answer points:
- Client Credentials is for service-to-service (machine-to-machine) communication where no user is involved
- Authorization Code is for user-facing applications where a user grants access to their resources
- Client Credentials example: a background job service calling an inventory API to update stock levels
- Client Credentials example: microservice A calling microservice B's internal API
- Authorization Code example: a web app accessing Google Drive on behalf of a logged-in user
- Client Credentials cannot be used when you need to know which user is making the request; it only identifies the client
Expected answer points:
- JWKS (JSON Web Key Set) is a JSON document published by the IdP at a well-known endpoint containing public keys
- Services use these public keys to verify JWT signatures locally without calling the IdP
- Each key has a "kid" (key ID) that appears in token headers, allowing services to select the correct key
- Caching: services cache JWKS documents and refresh periodically (TTL typically 1-24 hours) to avoid per-request fetches
- Benefit: token validation remains fast and the IdP is not a bottleneck or single point of failure
- Risk: if a signing key rotates and cache is too long, validation failures occur until cache expires
- Recommendation: set appropriate TTL, have fallback fresh fetch on validation failure, monitor cache hit rates
Expected answer points:
- Problem: multiple concurrent requests all see an expired access token and attempt to refresh simultaneously
- Solution 1: token cache with expiry timestamp; check if refresh is needed before using the token
- Solution 2: use a mutex or lock at the process level so only one refresh happens at a time
- Solution 3: refresh early (before actual expiry with a buffer, e.g., expiry - 60 seconds) to avoid concurrent expiration
- Implement graceful degradation: if refresh fails and token is not yet expired, allow the request to proceed with the existing token
- Centralize token management in a service/utility rather than having each component implement its own refresh logic
- Handle refresh failures with circuit breaker pattern to prevent thundering herd on IdP
Expected answer points:
- Nonce is a random string included in the authentication request that gets returned in the ID token
- The client generates a nonce, includes it in the auth request (as a hash in code flow with PKCE, or directly in implicit/hybrid flows)
- The authorization server includes the nonce hash in the ID token
- The client verifies the nonce in the returned ID token matches what it sent
- If an attacker intercepts the ID token and tries to replay it, the nonce will not match or already be used
- Nonce binding prevents attackers from intercepting and replaying ID tokens to impersonate users
Expected answer points:
- Standard Authorization Code flow uses a client_secret to authenticate when exchanging the code for tokens
- PKCE (Proof Key for Code Exchange) replaces the static client_secret with a dynamic code_verifier
- PKCE flow: client generates random code_verifier, sends SHA256 hash (code_challenge) with auth request, sends original verifier when exchanging code
- PKCE protects against authorization code interception attacks where an attacker steals the code from a redirect URI
- PKCE is REQUIRED for public clients (SPAs, mobile apps) that cannot safely store client_secret
- PKCE is now RECOMMENDED for all OAuth flows including confidential clients as defense in depth
Expected answer points:
- acr indicates the authentication context class, representing the LEVEL of assurance (e.g., phising-resistant, MFA present)
- amr indicates the specific AUTHENTICATION METHOD used (e.g., password, OTP, biometric)
- acr values are often defined by the IdP or standards like NIST AAL levels; they signal whether step-up authentication occurred
- amr values are defined by OIDC spec (pwd, otp, swk,hwk, etc.) and tell you WHAT factors were used
- Use acr for authorization decisions (does this token meet our assurance requirements?)
- Use amr for audit/compliance (log what authentication methods were used for this session)
Expected answer points:
- Silent authentication uses a stored refresh token to obtain new tokens without user interaction (no login prompt)
- The IdP checks for an existing session cookie or silent refresh grant type
- If session is valid and not expired, new tokens are returned without user interaction
- Use cases: keeping a web app session alive, preloading user data on page load
- Benefits: better UX (no login interruption), reduces authentication load on IdP
- Caveats: requires refresh token storage, may be blocked by browsers with strict cookie policies (ITP, Firefox Total Cookie Protection)
Expected answer points:
- Client-side logout: clear local tokens, session cookies, and any stored state
- RP-initiated logout: call the IdP's logout endpoint to invalidate the IdP session
- IdP can then trigger Single Logout (SLO) by calling logout endpoints on other RPs that were accessed in the same session
- Token revocation: call the authorization server's token revocation endpoint to invalidate refresh tokens
- Front-channel logout: IdP redirects to RP logout URLs with iframes (less secure, cache issues)
- Back-channel logout: IdP makes direct POST calls to RP logout endpoints (more secure, requires RPs to expose endpoints)
- Session invalidation: clear local sessions across all services, not just the originating one
Expected answer points:
- State is an opaque random string the client generates and includes in the initial authorization request
- The authorization server returns state unchanged in the redirect, allowing the client to verify the redirect came from the expected request
- State prevents Cross-Site Request Forgery (CSRF) attacks: an attacker could trick a user into authorizing an authorization code grant with the attacker's client_id
- Without state validation, the attacker could intercept the authorization code and exchange it before the legitimate user
- State should be: cryptographically random, stored server-side in session, validated on redirect before code exchange
- State is REQUIRED for OAuth 2.0 security; never process an authorization response without validating state
Expected answer points:
- Coarse-grained: `read:orders` grants read access to all orders
- Fine-grained: combine scopes with attributes (e.g., `read:orders:own` or resource-based policies)
- Resource indicators (RFC 8707) allow specifying which resource API the token is for when calling multiple APIs
- Scope hierarchy: define scope groups like `orders:read` that implies `orders:read:basic`
- Entitlement systems: externalize authorization decisions to a policy engine (Open Policy Agent) that evaluates token claims against resource attributes
- Example: `read:orders` scope + order.userId == token.sub claim = authorized to read specific order
- Avoid scope explosion: use resource prefixes, group related permissions, consider ABAC for complex scenarios
Expected answer points:
- auth_time records when the user last actively authenticated with the IdP (seconds since Unix epoch)
- Clients use auth_time to detect if a new authentication has occurred since the last ID token was issued
- If auth_time is newer than expected, the client may need to re-authenticate for sensitive operations
- auth_time enables "passive authentication" refresh: check if auth_time has changed to know if session is still valid
- For step-up authentication scenarios, clients can check if auth_time is recent enough for the required assurance level
- Note: auth_time tells when authentication happened, not when the current ID token was issued (iat claim)
Expected answer points:
- Token binding ties tokens to a cryptographic key or device that the client proves possession of
- Access token binding: bind token to TLS channel, client certificate, or asymmetric key pair
- If a token is stolen and used from a different context (different IP, device, TLS fingerprint), the binding check fails
- For browser-based apps: token binding to TLS channel state (Channel ID, Certificate Bound Access Tokens)
- Refresh token rotation already provides some binding via device fingerprinting, but explicit binding is stronger
- Trade-offs: adds complexity, requires IdP support, may break in certain network configurations
Expected answer points:
- Service mesh (Linkerd, Istio) provides mTLS for cryptographically authenticating which service is making the request
- mTLS alone identifies the SERVICE identity but does not carry USER identity across service-to-service calls
- OIDC tokens carry user identity: which user initiated the request, what scopes they were granted
- Combined model: mTLS handles service-to-service authentication, OIDC tokens propagate user context
- For external requests: API gateway validates OIDC token (who is the user?) and attaches identity headers
- For internal requests: sidecar proxy validates mTLS certificates (which service is calling?)
- Services can trust the combined identity: "service X called us on behalf of user Y" with cryptographic proof for both
- Zero-trust means: verify both service identity (mTLS) AND user identity (OIDC) on every hop, regardless of network position
Further Reading
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.
Conclusion
OAuth 2.0 and OpenID Connect form the foundation of modern delegated authorization and federated identity in microservices architectures. OAuth 2.0 solves the problem of granting limited access to resources without sharing credentials, while OIDC extends it to add identity layer capabilities on top of OAuth 2.0.
Key takeaways: always use short-lived access tokens backed by refresh token rotation, validate tokens at the resource server with cryptographic signature verification rather than relying solely on opaque token introspection, and prefer explicit algorithm specification in JWT validation to prevent algorithm confusion attacks. When designing authorization scopes, follow the principle of least privilege and avoid overly broad scopes that grant more access than necessary.
For microservices communicating across service mesh or API gateway boundaries, mTLS provides cryptographic service-level authentication, while OAuth 2.0/OIDC handles user and application identity. These two layers work together to enable a zero-trust security model where every request is authenticated and authorized regardless of network position.
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.