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 handles delegated API access, while OIDC adds a standard identity assertion for sign-in. The guide walks through user and service flows, token validation, scopes, gateway checks, and session behavior, then covers failures such as issuer outages, leaked credentials, and signing-key rotation. Use it to choose a flow for each client, validate tokens at the resource boundary, and plan revocation and recovery while keeping authorization in each service.
OAuth 2.0 and OIDC for Microservices
Introduction
OAuth 2.0 lets a client receive limited access to an API without handling the user’s password. OpenID Connect builds on OAuth 2.0 with a standard way to verify a user’s identity, so the two protocols solve related but distinct problems.
This guide compares common user and service flows, explains token validation and scope checks, and covers risks such as token theft, signing-key rotation, and identity-provider outages. It also shows where authorization still belongs in the resource service.
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 issues tokens after the applicable client and user checks
- Resource server: The API that holds the protected resources
When to Use OAuth 2.0 and OIDC
Use OAuth 2.0 when a client needs delegated access to an API, such as a user authorizing an app to read selected files. Use OIDC when an application also needs a standard sign-in flow and identity claims. A request that needs both usually uses OIDC over OAuth 2.0.
Choose the flow to match the client:
- Use Authorization Code with PKCE for browser, mobile, and other public clients. A backend web app can also use this flow and keep tokens on the server.
- Use Client Credentials when one service calls another as itself, with no user acting on the request.
- Use a normal application session for a first-party app when all you need is to keep a user signed in; OAuth is not required just to create a login session.
Do not use OIDC as API authorization by itself. APIs should accept access tokens issued for that API and enforce scopes and resource-level permissions. Do not use Client Credentials to represent a user, or share one service account across unrelated workloads.
Authorization Code Flow
The Authorization Code flow is used by web and native clients. Public clients use PKCE because they cannot keep a client credential secret; a backend-for-frontend can keep tokens on the server, while a browser-only client may handle tokens in the browser and needs a deliberate storage and XSS strategy.
sequenceDiagram
participant User
participant Client as Client App
participant Auth as Authorization Server
participant Resource as Resource Server
User->>Client: Start sign-in
Client->>Client: Create state and PKCE verifier
Client->>User: Redirect with state and code challenge
User->>Auth: Authenticate and approve access
Auth->>Client: Redirect with code and state
Client->>Client: Verify state
Client->>Auth: Exchange code with PKCE verifier
Auth->>Client: Return tokens allowed for this client
Client->>Resource: Request with access token
Resource->>Client: Protected resource
The client sends the authorization request with a registered redirect_uri, requested scopes, transaction-specific state, and a PKCE challenge. After the user signs in, the authorization server returns a short-lived code and the state value; the client verifies state before exchanging the code with its PKCE verifier. A confidential client also authenticates at the token endpoint using its registered method. A public client must not send a client_secret because it cannot protect one. Redact authorization codes from logs and other telemetry; PKCE limits the value of a code intercepted elsewhere.
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 its registered client authentication
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 registered method, such as a protected secret, mutual TLS, or private_key_jwt. There is no user interaction. The authorization server returns a token that Service A uses to call Service B, which validates the token and applies its own authorization rules.
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 should be short-lived, but their lifetime is set by the authorization server and risk policy rather than a universal 5-to-15-minute rule. When a client is issued a refresh token, it can use that token to request a new access token without prompting the user again.
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.
With refresh-token rotation, the authorization server issues a replacement and invalidates the token just used. If both a legitimate client and an attacker reuse tokens from the same family, the server can detect reuse and revoke the active token family. Provider policy and client behavior determine whether rotation is enabled.
OpenID Connect: Adding Identity to OAuth 2.0
OAuth 2.0 is an authorization framework; it does not define a standard user sign-in assertion. OpenID Connect (OIDC) adds that identity layer and defines the ID token and standard identity claims.
OIDC defines an ID token, a signed assertion intended for the client that contains authentication claims. An access token is intended for a resource server; clients should treat its format as opaque, whether the issuer uses a JWT or a reference token.
ID Token vs Access Token
Here is the key distinction.
An access token lets its bearer call a resource server, which validates it and applies the required authorization checks. Clients should treat access tokens as opaque and use them as issued; an authorization server may encode one as a readable JWT or use an opaque reference token, and clients must not depend on that format.
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 required in an ID token and identifies the user within its issuer. Use the pair (iss, sub) as the stable key in an application database, since subject identifiers are scoped to an issuer.
JWT Tokens and Claims
JSON Web Tokens (JWTs) are one way to represent claims in access and identity tokens. A signed JWT is a JSON Web Signature (JWS): its header and payload are Base64URL-encoded and its signature can be verified, but the payload is not encrypted. Encrypted JWTs use a different structure.
A compact JWS has three parts:
- Header: Algorithm and token metadata
- Payload: Claims
- Signature: Verifies integrity and issuer, when checked against a trusted key and expected claims
{
"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.
Authorization servers publish verification keys in a JSON Web Key Set (JWKS). For OIDC, obtain its URI from trusted issuer metadata rather than guessing a path. A verifier can use the token’s kid to select a key, then check the signature and required claims with a maintained JWT library.
import { createRemoteJWKSet, jwtVerify } from "jose";
const issuer = "https://auth.example.com";
const metadata = await fetch(`${issuer}/.well-known/openid-configuration`).then(
(response) => response.json(),
);
const JWKS = createRemoteJWKSet(new URL(metadata.jwks_uri));
async function validateToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
issuer,
audience: "api.example.com",
algorithms: ["RS256"],
});
return payload;
}
The allowed algorithm must match the issuer’s configuration; pinning it here is an example, not a universal choice.
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.
Use a JWT library that verifies the signature and validates the required claims as one operation. Do not make authorization decisions from decoded but unverified claims. Check exp and nbf as required by the token profile, and apply issuer-specific rules to claims such as iat.
Clock skew can make tokens appear not-yet-valid or expired at different services. Keep clocks synchronized and set only a small validation tolerance based on measured drift and your risk requirements; a large tolerance extends the time a token can be accepted.
Monitor exp validation failures separately from other validation failures. A spike in expiration-only rejections often indicates clock skew problems, not token theft. Set up alerting for this metric so you catch clock drift before it causes outages.
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.
Audience validation prevents a class of attacks where a token issued for one service is reused against a different service. A compromised mobile app that obtains a token for mobile-app should not be able to use that token to call api.example.com. Without audience validation, the API only checks that the token is signed by a trusted issuer, not that it was issued for this specific API.
For an ID token, OIDC requires the audience to identify the client that requested authentication. An access token is intended for its resource server; the authorization server determines how that audience is represented. Validate each token against the expected issuer and audience for its role rather than applying ID-token rules to access tokens.
Common mistakes: checking audience exists instead of matching the expected value, using string equality when multiple audiences are allowed (tokens can have multiple audiences as an array), and not handling the case where a token has no audience claim at all. If a token has no aud claim, reject it unless your IdP explicitly allows audience-less tokens for your use case.
Scope and Permission Validation
Access tokens may carry a scope or scp claim listing granted permissions. Before an action, the resource server should check the required scope and any resource-specific policy; a scope alone does not prove ownership or access to a particular record.
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. OIDC sign-in through a common IdP can provide single sign-on across applications that trust that provider’s session.
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 sign-in session used during redirects. Each relying party (RP) usually keeps its own application session after login; an access token does not automatically reflect a later IdP logout or revocation.
Shared session store or logout events: Applications can coordinate local session invalidation through a shared store or supported back-channel logout. Token expiry, introspection, or another explicit revocation check may still be needed for API calls.
// 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
Cookie behavior matters when an OAuth or OIDC redirect crosses sites:
- SameSite=Strict: The IdP cookie is withheld on cross-site navigation, so an existing IdP session may not be available during the redirect.
- SameSite=Lax: Cookies generally accompany top-level safe-method navigations, which fits common authorization-code redirects. Cross-site POST callbacks and embedded silent sign-in can behave differently.
- SameSite=None; Secure: Allows cross-site cookie use where the browser permits it, but does not prevent CSRF by itself. Add transaction-bound state and appropriate CSRF defenses.
Modern browser privacy controls can block third-party cookies even when a cookie is marked SameSite=None. Do not rely on iframe-based silent sign-in as the only way to renew a session; use a supported redirect flow or a refresh-token design that meets the provider’s browser-app guidance.
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.
A gateway may check broad scopes before forwarding requests, while each resource service checks the permissions and ownership required for its operation.
GET /api/orders/12345
Authorization: Bearer eyJhbGc...
// Service-level scope check
app.get("/api/orders/:id", async (req, res) => {
const token = req.token;
const scopes = (token.scope ?? "").split(/\s+/);
if (!scopes.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 can reject obviously insufficient requests, but the service remains responsible for enforcing its own authorization rules and data ownership.
API Gateway Integration
An API gateway can validate tokens before forwarding requests, which centralizes some checks. It is not the only valid enforcement point: resource services must still protect their own operations and data.
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" });
}
}
A gateway can pass verified identity context downstream only over an authenticated channel that prevents clients from forging those headers. Services should accept that context only from the trusted gateway, enforce their own resource-level permissions, and remain protected if a route can bypass the gateway. Network location alone does not establish trust.
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 identity. To call another service, it authenticates to the authorization server with its registered client-authentication method, receives an access token, and presents that token to the resource API.
// 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 (!tokenCache.token || Date.now() >= tokenCache.expiresAt - 60000) {
const { token, expiresIn } = await getServiceToken();
tokenCache.token = token;
tokenCache.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: JavaScript-readable storage exposes tokens to successful XSS. HttpOnly cookies prevent scripts from reading cookie contents, but browsers send cookies automatically, so cookie-based designs also need CSRF defenses. Service credentials belong in a managed secret system or workload identity where available.
Token revocation: A locally validated JWT has no built-in immediate revocation. Short lifetimes, deny lists, introspection, or other stateful checks can address different revocation needs, each with availability and latency costs. Reference tokens let a resource server query current status, subject to introspection and caching behavior.
Scope creep: Request only the scopes you need. A compromised token with admin:* is far more dangerous than one with read:profile.
Client credentials: Treat confidential-client credentials like passwords. Do not commit them to source control; use a secret manager or workload identity and rotate credentials when policy or an incident requires it. Public clients cannot keep a static secret confidential.
For more on securing your infrastructure, see Rate Limiting for protecting APIs from abuse and Service Mesh for mutual TLS between services.
Security and Compliance Notes
Use Authorization Code with PKCE for interactive clients, exact redirect URI matching, and TLS for authorization and resource requests. Validate issuer, audience, signature, expiry, and the token type expected by each endpoint. For OIDC sign-in, validate the ID token according to OIDC Core, including its issuer, audience, signature, expiry, and nonce when supplied. An ID token proves an authentication event to its client; it is not an API access token.
Keep access tokens short-lived and restrict scopes to the task. Protect refresh tokens with rotation or sender-constraining where supported, and revoke them when a session or credential is compromised. Store client secrets in a managed secret store; public clients must not embed a secret they cannot keep confidential.
Treat claims and authentication events as personal data. Request only claims the application needs, restrict access to identity logs, define retention periods, and never record raw tokens, authorization codes, or client secrets. Map consent, access, deletion, and retention requirements to the laws and policies that apply to your users and organization; OAuth or OIDC support alone does not establish compliance.
For protocol-level requirements, see the OAuth 2.0 Security Best Current Practice (RFC 9700) and OpenID Connect Core 1.0.
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: A stolen refresh token can be replayed to obtain new access tokens. Mitigations include issuer-supported rotation or cryptographic sender constraint, along with monitoring for suspicious reuse.
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, an attacker could make token requests as a confidential client. Use its registered authentication method, such as a protected secret, mutual TLS, or private_key_jwt; a public client_id alone does not prove client identity.
MFA Implementation Patterns via OIDC
OIDC lets you layer additional authentication factors without building MFA logic into your application code.
Identity providers can enforce MFA before issuing tokens when the relevant policy requires it. Services should rely on documented issuer claims and policy, not assume every token represents the same authentication strength.
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: A client can request stronger authentication for a sensitive action. A service should require a particular acr value only when the provider defines that value and its policy guarantees the required assurance.
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: Clients should treat access tokens as opaque and use them only with the intended resource server. Even when an access token is a readable JWT, its format and claims are not a client identity contract.
Skipping audience validation: A token meant for one API should not work on another. Always validate the audience claim.
Long-lived tokens: Choose token lifetime from the threat model and issuer policy. Shorter access-token lifetimes reduce exposure after theft, while refresh-token controls and resource authorization still matter.
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.
Leaving the redirect surface broad: Register exact redirect URIs and reject unregistered destinations. Wildcards and open redirects can send authorization codes to an attacker-controlled site.
Skipping request correlation: Validate the returned state value and, for OIDC, validate nonce in the ID token when one was sent. PKCE protects the authorization-code exchange; it does not replace these checks.
Sending credentials in URLs or logs: Bearer tokens and authorization codes can leak through browser history, proxy logs, analytics, and referrer data. Send access tokens in the Authorization header over TLS and keep them out of URLs and logs.
Assuming a gateway check replaces authorization: A gateway can reject invalid tokens, but each resource service still needs to check permissions and ownership for the operation it performs.
Production Failures and Mitigations
Scenario: Authorization Server Unavailable
Symptoms: New token requests and sign-ins fail. Introspection-based API checks may also fail; locally validated JWTs can continue to work only while cached trusted keys and token claims remain valid.
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"
# Read the JWKS URI published in trusted OIDC issuer metadata
curl -s https://auth.example.com/.well-known/openid-configuration | jq '.jwks_uri'
Mitigation:
- Continue local JWT validation only while a trusted signing key is available and all issuer, audience, expiry, and policy checks still pass.
- For reference tokens, decide the outage behavior in advance; do not bypass authorization with an allowlist fallback.
- Restore authorization-server availability and check network policies and DNS resolution.
Prevention:
- Run authorization server with high availability (multiple replicas)
- Cache trusted JWKS keys and honor provider cache guidance so known keys remain available during brief outages.
- Plan key rotation and authorization-server failures; local validation reduces per-request dependence but does not remove the issuer dependency for new tokens or key changes.
- 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 credentials in a managed secret system or use workload identity; do not commit secrets to source control.
- Where supported, use stronger client authentication such as mTLS or
private_key_jwt; these can authenticate a Client Credentials request rather than replace the grant. - 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
- Use sender-constrained refresh tokens, such as mechanisms supported by DPoP or mutual TLS, when the provider and clients support them.
- 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 |
Active application sessions | Sudden drop may indicate a logout or 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"
Observability Checklist
- Record token issuance, validation failures, refresh failures, consent changes, and authorization denials with client, grant type, scope, result, and reason.
- Track issuance and validation rates, validation latency, authorization-server errors, JWKS refreshes, and refresh-token reuse detections.
- Add trace spans around authorization-server calls and token validation; use a request or trace ID to correlate events across the gateway and resource service.
- Alert on sustained validation failures, authorization-server or JWKS endpoint outages, unexpected grant types or scope changes, and refresh-token reuse.
- Redact tokens, authorization codes, secrets, and unnecessary identity claims from logs, traces, and metric labels.
This checklist complements the metrics, log examples, trace attributes, dashboards, and alert rules above. Keep user identifiers out of high-cardinality metric labels; use access-controlled logs for investigations that need subject-level detail.
Trade-Off Table
| Approach | Security | Complexity | Performance | Typical Use Case |
|---|---|---|---|---|
| JWTs vs Reference Tokens | ||||
| JWTs (self-contained) | Local checks need trusted keys and policy state | Low per-request network cost | Higher availability during brief issuer outages | Distributed resource servers |
| Reference Tokens | Status can be checked through introspection | Introspection adds a dependency | Depends on introspection availability and cache policy | Centralized revocation checks |
| Authorization Code Flow | ||||
| with PKCE | High | Medium | Good | Mobile apps, SPAs, web apps |
| without PKCE | Low | Low | Good | Trusted first-party clients only |
| Client Credentials Flow | ||||
| Default (shared secret/cert) | Medium | Low | High | Service-to-service, daemons, backends |
| with Mutual TLS | High | High | High | High-security service communication |
| Token Storage | ||||
| HttpOnly Cookies | High | Medium | Good | Browser-based apps |
| LocalStorage | Low | Low | Good | Legacy apps, non-sensitive use cases |
| Memory (JavaScript variables) | Medium | Medium | Good | Single-page apps with short sessions |
| Algorithm Choices | ||||
| RS256/ES256 (asymmetric) | Public keys can verify; signing key stays private | Medium | Good | Multiple resource servers |
| HS256 (symmetric) | Every verifier holding the secret could also sign | Low | Good | Small, tightly controlled deployments |
| Response Types | ||||
code (Authorization Code) |
High | Medium | Good | Most user-facing applications |
Implicit response (token) |
Not recommended for new clients; avoid front-channel access tokens | — | — | Migrate legacy clients |
| OIDC Hybrid response | Use only when its front-channel ID token response is specifically needed | High | Good | Specialized OIDC clients |
Quick Recap Checklist
- Use OAuth for delegated API access and OIDC when the client also needs a sign-in identity assertion.
- Choose a flow for the client type; use PKCE for public authorization-code clients and protect confidential-client credentials.
- Validate token signature, issuer, audience, expiry, and intended token type before trusting claims.
- Treat ID tokens as client-facing identity assertions, not API access tokens.
- Choose local JWT validation or introspection based on revocation needs, key distribution, and availability.
- Pin the expected signing algorithm and key type; shared HS256 secrets let each verifier sign as well.
- Check scopes and resource permissions at the service handling the request.
- Plan JWKS rotation and issuer-outage behavior; never bypass authorization as a fallback.
- Protect browser sessions against both XSS and CSRF, and protect refresh tokens against reuse.
- Authenticate gateway-to-service traffic and pass user context only when downstream services need it.
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
- Time checks: reject expired or not-yet-valid tokens, using only a small clock-skew tolerance based on measured drift and policy
- 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:
- Self-contained JWTs can be validated locally, reducing per-request network dependence when trusted keys are cached
- Reference tokens are opaque to clients and resource servers; introspection adds a network dependency unless safely cached
- A locally validated JWT has no built-in immediate revocation; an application can add stateful revocation checks, with a latency and availability cost
- Reference tokens let a resource server check current token status through introspection, subject to availability and any caching policy
- Hybrid approach: short-lived JWTs for normal operations, reference tokens for high-privilege actions requiring immediate revocation capability
- Choose access-token lifetime from the issuer's risk policy; short lifetimes reduce exposure but do not replace authorization checks
Expected answer points:
- localStorage is accessible via JavaScript, making it vulnerable to XSS attacks that can exfiltrate tokens
- HttpOnly cookies prevent JavaScript from reading the cookie value, though XSS can still make requests as the signed-in user
- Cookies are sent automatically, so protect cookie-authenticated endpoints against CSRF with appropriate SameSite settings and request validation
- localStorage persists across browser sessions until explicitly cleared; cookie expiration is controlled
- HttpOnly and Secure cookies reduce JavaScript access to tokens, but the right browser architecture depends on the threat model and whether a backend-for-frontend is available
- Do not assume a cookie removes XSS or CSRF risk; apply the browser-app guidance from the identity provider and protect refresh-token use
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
- The authorization server can revoke the active token family after detecting reuse; exact response depends on its rotation policy
- Rotation helps detect reuse, but an attacker who uses a stolen token first may still obtain tokens before the legitimate client detects the problem
- 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 according to issuer metadata and library behavior, then refresh when keys rotate or an unknown
kidappears - Benefit: token validation avoids a key fetch on every request; token issuance and key rotation still depend on the authorization server
- Risk: stale keys or unavailable metadata can disrupt validation during key rotation
- Recommendation: follow issuer cache guidance, test rotation behavior, and monitor cache refreshes and misses
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:
- The client creates an unpredictable, transaction-specific nonce and sends it in the OIDC authentication request
- The authorization server returns the nonce value in the ID token; it is not generally hashed by the client
- The client verifies the nonce in the returned ID token matches what it sent
- Reject an ID token whose nonce does not match the value saved for the current sign-in transaction
- Checking the expected nonce binds the ID token to the sign-in transaction; reject mismatches and do not reuse transaction state
Expected answer points:
- PKCE binds the authorization request to the token exchange with a per-transaction verifier; it does not replace confidential-client authentication
- The client sends an S256 hash of its random
code_verifieras thecode_challenge, then sends the verifier at the token endpoint - Public clients MUST use PKCE; current OAuth security guidance also recommends it for confidential clients
- A confidential client still authenticates with its registered method, while a public client must not rely on a static secret
acr (Authentication Context Class Reference) claim differ from the amr (Authentication Methods References) claim in OIDC?Expected answer points:
acridentifies an authentication context; the meaning and assurance level of its values depend on the issuer's policyamrlists authentication methods used, such as password or one-time password, when the issuer supplies it- The issuer defines the meaning of
acrvalues; do not assume they map to a particular assurance standard without an agreed profile - Use
acrfor policy only when its semantics are documented, and treatamras issuer-reported method information
Expected answer points:
- Silent authorization is an authorization request that asks the provider not to show UI, commonly with
prompt=none; it relies on an existing provider session - A refresh-token grant is a separate token request and does not itself perform a new user authentication
- Embedded silent sign-in may fail when browser privacy controls block third-party cookies
- Use a supported top-level redirect or a browser-app refresh-token pattern instead of relying on hidden iframe renewal
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
- Where supported and configured, the provider can notify relying parties using front-channel or back-channel logout; cross-application logout is not automatic or universally available
- Token revocation: call the authorization server's token revocation endpoint to invalidate refresh tokens
- Front-channel logout: where supported, the provider notifies RPs through the user's browser; delivery depends on browser behavior and RP configuration
- Back-channel logout: the provider sends a signed logout token to registered RP endpoints; each RP validates it and clears its own session
- Session invalidation: clear local sessions across all services, not just the originating one
state parameter in OAuth 2.0 authorization requests?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
- Failing to bind and validate the response can let an attacker inject a response into the wrong browser session or cause login CSRF
- State should be: cryptographically random, stored server-side in session, validated on redirect before code exchange
- Bind state to the initiating browser session and validate it on return when used; public authorization-code clients must also use PKCE, and OIDC clients validate nonce when sent
Expected answer points:
- Coarse-grained:
read:ordersgrants read access to all orders - Fine-grained: combine scopes with attributes (e.g.,
read:orders:ownor 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:readthat impliesorders:read:basic - Entitlement systems: externalize authorization decisions to a policy engine (Open Policy Agent) that evaluates token claims against resource attributes
- Example:
read:ordersscope + order.userId == token.sub claim = authorized to read specific order - Avoid scope explosion: use resource prefixes, group related permissions, consider ABAC for complex scenarios
auth_time claim in OIDC ID tokens help with session management and security?Expected answer points:
auth_timerecords when the user last actively authenticated, as seconds since the Unix epoch- OIDC clients can request a maximum authentication age and compare the returned
auth_timewith that requirement - For sensitive operations, require recent authentication only when the provider's policy and the application's session rules support it
auth_timedescribes authentication time;iatdescribes when the ID token was issued
Expected answer points:
- Sender-constrained tokens require the client to prove possession of a key in addition to presenting the token
- OAuth supports mechanisms such as DPoP, which uses an application-level key proof, and mutual-TLS certificate-bound tokens
- A copied token is less useful to an attacker who does not also control the bound key or certificate
- The authorization server and resource server must both support and validate the chosen mechanism
- These mechanisms add key management and client integration work; IP address or device fingerprinting is not cryptographic proof of possession
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
- An access token can carry authorization for its intended API; an ID token is for the OIDC client and should not be forwarded as an API credential
- When downstream services need user context, propagate an audience-appropriate access token or a trusted, integrity-protected context
- A gateway can validate an external access token, then pass user context only over an authenticated, authorized service channel
- mTLS can authenticate the calling workload; it does not by itself prove end-user identity
- Propagate user identity only when downstream services need it, using an audience-appropriate token or a trusted, integrity-protected context
- Each service still enforces permissions for its own resources; do not trust headers supplied by an untrusted caller
Further Reading
- Distributed Caching — Explore cache strategies and their effect on latency and reliability.
- Microservices Roadmap — Review service communication and operational patterns.
- RESTful API Design — Apply consistent authentication and authorization rules at API boundaries.
- OAuth 2.0 Security Best Current Practice (RFC 9700) — Current security guidance for OAuth clients and authorization servers.
- OAuth 2.0 Demonstrating Proof of Possession (DPoP, RFC 9449) — Bind tokens to a client-held key.
- OAuth 2.0 Mutual-TLS (RFC 8705) — Client authentication and certificate-bound tokens.
Conclusion
OAuth 2.0 and OIDC work across microservices when each boundary checks the token and permission meant for it. Use OIDC for user sign-in, access tokens for API calls, and mTLS to authenticate service workloads where appropriate. Plan for key rotation, revocation, and identity-provider outages, and keep each service responsible for its own resource checks.
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.
API Authentication vs. Authorization: Identity and Access
Understand API authentication and authorization, how they differ in request handling, and how to avoid common identity and access-control mistakes.
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.