OAuth 2.0 and OIDC for Microservices

Learn how OAuth 2.0 and OpenID Connect provide delegated authorization and federated identity for microservices architectures.

published: reading time: 21 min read

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

ScenarioUse OAuth 2.0 / OIDCNotes
User authentication for web or mobile appsYesAuthorization Code flow with PKCE is standard
Service-to-service calls without shared secretsYesClient Credentials flow works well
Third-party application access to user dataYesOAuth’s delegated authorization shines here
Machine-to-machine with static API keysConsiderMay be simpler if you already have key infrastructure
Microservices behind an API gatewayYesCentralized token validation at the gateway
Long-lived background job authenticationConsiderRefresh token rotation adds complexity
Stateless serverless functionsYesJWT validation works well for cold starts
Real-time bidirectional communication (WebSocket)CautionToken validation on connect, re-auth on reconnect

Trade-offs

AspectAPI Keys / Basic AuthOAuth 2.0 / OIDC
Identity verificationNone or weakStrong (via IdP verification)
Delegated authorizationNot supportedFirst-class support
Token lifetimeUsually long-livedShort-lived with refresh
RevocationImmediate if key rotatedDelayed (until token expires)
Implementation complexityLowHigher
Standards complianceNoneRFC-compliant
User identity in tokensNot availableStandard in OIDC ID tokens
Multi-factor auth supportExternal to systemBuilt 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:

ScopeClaims
openidsub (user identifier)
profilename, family_name, given_name, preferred_username, picture
emailemail, email_verified
addressaddress
phonephone_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:

  1. If using JWTs with local validation, services should continue working until their cached JWKS expires
  2. If using reference tokens requiring validation calls, switch to allowlist mode temporarily
  3. Scale the authorization server if it is overloaded
  4. 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:

  1. Immediately revoke the client secret: rotate credentials in IdP admin console
  2. Invalidate all existing tokens for that client (if IdP supports bulk revocation)
  3. Audit which resources were accessed with the compromised credentials
  4. Review logs for data exfiltration or unauthorized actions
  5. 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:

  1. Update token validation to explicitly specify and check the expected algorithm
  2. Reject tokens with “none” algorithm
  3. Do not accept different key types than expected (e.g., symmetric keys for asymmetric algorithms)
  4. 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:

  1. If IdP supports per-user token revocation, revoke all refresh tokens for affected users
  2. Force re-authentication for affected users
  3. If using Auth0 or similar managed IdP, enable anomaly detection to auto-revoke on suspicious activity
  4. 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

MetricWhat It Tells YouAlert Threshold
token_issuance_totalToken issuance rate by grant typeUnexpected grant type volume
token_validation_failure_totalValidation failures by reason>1% failure rate
token_validation_duration_secondsToken validation latencyp99 > 100ms
jwks_cache_miss_totalJWKS fetches from IdP>10% miss rate indicates cache misconfiguration
active_sessions_totalCurrently active refresh tokensSudden drop indicates revocation event
oauth_error_totalOAuth errors by error codeAny 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 identifier
  • oauth.grant_type: authorization_code, client_credentials, refresh_token
  • oauth.scopes: Array of requested scopes
  • oauth.validation.result: success, failure
  • oauth.failure.reason: expired, invalid_signature, invalid_audience, insufficient_scope

Dashboards to Build

  1. OAuth/OIDC Health: Token issuance rate, validation success/failure ratio, error breakdown
  2. Token Lifecycle: Average token lifetime, refresh rate, revocation events
  3. Client Activity: Token usage by client, scope distribution, unusual client behavior
  4. 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

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.

#microservices #mtls #security

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.

#microservices #secrets-management #security

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.

#microservices #service-identity #spiffe