API Authentication Best Practices in 2026
Last updated: March 2026
APIs are the connective tissue of modern applications. Every microservice call, mobile app request, and third-party integration depends on API authentication to verify identity and authorize access. Getting API authentication right is not just about security — it directly affects developer experience, system performance, and regulatory compliance.
This guide covers the current state of API authentication in 2026, including when to use each method, how to implement them with Keycloak, and the emerging standards that are reshaping how we think about API security.
The API Authentication Landscape
The API authentication space has consolidated around a few proven approaches. Here is the decision tree for choosing the right method:
Is the caller a user (via browser or mobile app)?
- Yes → OAuth 2.0 Authorization Code + PKCE
Is the caller a machine (service-to-service)?
- Yes, within the same trust boundary → OAuth 2.0 Client Credentials
- Yes, across organizational boundaries → mTLS or OAuth 2.0 + mTLS
Is the caller a simple integration (webhook, scheduled job)?
- Yes, low sensitivity → API Keys (with caveats)
- Yes, high sensitivity → OAuth 2.0 Client Credentials
Do you need sender-constrained tokens?
- Yes → DPoP or mTLS Certificate-Bound Access Tokens
Let us examine each approach in detail.
OAuth 2.0 Authorization Code with PKCE
This is the standard for user-facing API authentication. The upcoming OAuth 2.1 specification makes PKCE mandatory for all clients, and the implicit grant is officially deprecated.
Why PKCE Matters
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Without PKCE, an attacker who intercepts the authorization code (through a malicious app, compromised redirect, or network interception) can exchange it for tokens. PKCE binds the code to the original requester.
Keycloak Configuration
Configure a public OIDC client in Keycloak for your frontend application:
- Create a new client in the admin console
- Set Client authentication to OFF (public client)
- Enable Standard flow
- Set PKCE Code Challenge Method to S256
- Configure valid redirect URIs
Or use the Keycloak Config Generator to generate the client configuration.
Implementation Example
Here is a complete PKCE flow implementation for a JavaScript SPA:
// Generate PKCE code verifier and challenge
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}
function base64UrlEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/+/g, "-")
.replace(///g, "_")
.replace(/=+$/, "");
}
// Start authorization
async function login() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store verifier for later use
sessionStorage.setItem("pkce_verifier", codeVerifier);
const params = new URLSearchParams({
client_id: "frontend-app",
response_type: "code",
scope: "openid email profile",
redirect_uri: "https://app.example.com/callback",
code_challenge: codeChallenge,
code_challenge_method: "S256",
state: crypto.randomUUID(),
});
window.location.href =
`https://keycloak.example.com/realms/my-realm/protocol/openid-connect/auth?${params}`;
}
// Handle callback
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const codeVerifier = sessionStorage.getItem("pkce_verifier");
const response = await fetch(
"https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token",
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: "frontend-app",
code: code,
redirect_uri: "https://app.example.com/callback",
code_verifier: codeVerifier,
}),
}
);
const tokens = await response.json();
// tokens.access_token - use for API calls
// tokens.refresh_token - use to get new access tokens
// tokens.id_token - user identity claims
sessionStorage.removeItem("pkce_verifier");
return tokens;
}
For a more comprehensive look at OAuth 2.0 flows, see our OAuth 2.0 visual guide.
Calling APIs with Access Tokens
async function callApi(accessToken) {
const response = await fetch("https://api.example.com/data", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (response.status === 401) {
// Token expired, refresh and retry
const newTokens = await refreshTokens();
return callApi(newTokens.access_token);
}
return response.json();
}
OAuth 2.0 Client Credentials
For service-to-service communication where no user context is needed:
Keycloak Configuration
Create a confidential client with service account enabled:
- Create a new client
- Set Client authentication to ON
- Enable Service account roles
- Disable Standard flow and Direct access grants
- Assign appropriate roles to the service account
Implementation
import requests
from functools import lru_cache
from datetime import datetime, timedelta
class KeycloakServiceAuth:
def __init__(self, token_url, client_id, client_secret):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self._token = None
self._expires_at = None
def get_token(self):
if self._token and self._expires_at > datetime.now():
return self._token
response = requests.post(
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "openid",
},
)
response.raise_for_status()
data = response.json()
self._token = data["access_token"]
# Refresh 30 seconds before expiry
self._expires_at = datetime.now() + timedelta(
seconds=data["expires_in"] - 30
)
return self._token
def call_api(self, url, method="GET", **kwargs):
token = self.get_token()
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {token}"
response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code == 401:
# Force token refresh and retry
self._token = None
token = self.get_token()
headers["Authorization"] = f"Bearer {token}"
response = requests.request(method, url, headers=headers, **kwargs)
return response
# Usage
auth = KeycloakServiceAuth(
token_url="https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token",
client_id="backend-service",
client_secret="your-client-secret",
)
response = auth.call_api("https://api.example.com/internal/data")
JWT Validation: Local vs. Introspection
When your API receives a bearer token, it needs to validate it. There are two approaches:
Local JWT Validation
Validate the token’s signature, expiration, and claims locally using Keycloak’s public key:
import jwt
import requests
class JWTValidator:
def __init__(self, jwks_url, issuer, audience):
self.jwks_url = jwks_url
self.issuer = issuer
self.audience = audience
self._jwks_client = jwt.PyJWKClient(jwks_url, cache_keys=True)
def validate(self, token):
try:
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer=self.issuer,
audience=self.audience,
options={
"verify_exp": True,
"verify_iss": True,
"verify_aud": True,
"require": ["exp", "iss", "sub", "aud"],
},
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError("Token has expired")
except jwt.InvalidTokenError as e:
raise AuthError(f"Invalid token: {e}")
# Usage
validator = JWTValidator(
jwks_url="https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs",
issuer="https://keycloak.example.com/realms/my-realm",
audience="backend-api",
)
claims = validator.validate(request.headers["Authorization"].split(" ")[1])
Advantages: No network call for each request, lower latency, works offline.
Disadvantages: Cannot detect revoked tokens until they expire.
Token Introspection
Ask Keycloak directly if the token is valid:
def introspect_token(token, client_id, client_secret, introspection_url):
response = requests.post(
introspection_url,
data={
"token": token,
"token_type_hint": "access_token",
},
auth=(client_id, client_secret),
)
response.raise_for_status()
result = response.json()
if not result.get("active"):
raise AuthError("Token is not active")
return result
Advantages: Always reflects current token state (revoked tokens are detected immediately).
Disadvantages: Adds a network call per request, increases latency, creates a dependency on Keycloak availability.
Which to Choose?
Use local validation as the default and add introspection for specific scenarios:
| Scenario | Approach |
|---|---|
| Standard API calls | Local JWT validation |
| High-security operations (financial transactions) | Introspection |
| Long-lived tokens (> 5 minutes) | Introspection or short token lifetimes |
| Offline/air-gapped environments | Local validation only |
| Token revocation is critical | Introspection with caching (TTL < token lifetime) |
For more on JWT token structure and lifecycle, see our guide on JWT token lifecycle management. You can also use the JWT Token Analyzer to inspect token contents during development.
Mutual TLS (mTLS)
mTLS provides strong authentication for service-to-service communication by requiring both the client and server to present certificates.
When to Use mTLS
- Service mesh: Internal microservice communication where both sides are known
- B2B integrations: Third-party API access with organizational trust
- Regulatory requirements: PSD2, Open Banking, and similar standards mandate mTLS
- Certificate-bound access tokens: Binding OAuth tokens to client certificates (RFC 8705)
Keycloak mTLS Client Authentication
Configure a Keycloak client to authenticate using X.509 certificates instead of client secrets:
- In the client settings, set Client Authenticator to “X509 Certificate”
- Configure the Subject DN pattern that Keycloak should expect
- Configure your TLS termination point to pass client certificates to Keycloak
# Nginx configuration to pass client certificate to Keycloak
server {
listen 443 ssl;
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client optional_no_ca;
location / {
proxy_pass http://keycloak:8080;
proxy_set_header X-Client-Cert $ssl_client_raw_cert;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
DPoP (Demonstration of Proof-of-Possession)
DPoP is a mechanism for sender-constraining OAuth access tokens without requiring mTLS. The client generates a key pair and proves possession of the private key when using the token.
How DPoP Works
- The client generates an ephemeral key pair
- When requesting a token, the client sends a DPoP proof (a signed JWT) containing the public key
- The authorization server binds the access token to that public key
- When calling an API, the client sends both the access token and a new DPoP proof
- The API verifies that the DPoP proof was signed by the key bound to the access token
Keycloak DPoP Configuration
Keycloak 24+ supports DPoP. Enable it on your client:
- In client settings, go to Advanced
- Enable DPoP bound access tokens
For a detailed implementation example, see our guide on DPoP with the Keycloak Admin API.
When DPoP Makes Sense
- Public clients: SPAs and mobile apps where client secrets are not feasible
- Token theft mitigation: Even if an access token is stolen, it cannot be used without the private key
- Environments where mTLS is impractical: Browser-based applications that cannot use client certificates
API Keys: Limited but Useful
API keys are the simplest form of API authentication. They are just a static string passed in a header or query parameter. They are not suitable for user authentication, but they have valid use cases:
When API Keys Are Appropriate
- Rate limiting and usage tracking: Identifying which application is making requests
- Low-sensitivity read-only APIs: Public data APIs where you need accountability but not strong security
- Webhook verification: Validating that webhook payloads come from a known sender
When API Keys Are Not Enough
- User-specific data: API keys identify an application, not a user
- Sensitive operations: API keys cannot provide fine-grained authorization
- Mobile/frontend apps: API keys cannot be kept secret in client-side code
If you are using API keys today, evaluate whether OAuth 2.0 Client Credentials would provide better security without significant complexity overhead. See our post on API keys vs. OAuth for a detailed comparison.
Rate Limiting and Abuse Prevention
Regardless of your authentication method, implement rate limiting:
# Example rate limiting middleware (conceptual)
from datetime import datetime, timedelta
from collections import defaultdict
class RateLimiter:
def __init__(self, max_requests=100, window_seconds=60):
self.max_requests = max_requests
self.window = timedelta(seconds=window_seconds)
self.requests = defaultdict(list)
def check(self, client_id):
now = datetime.now()
cutoff = now - self.window
# Clean old entries
self.requests[client_id] = [
ts for ts in self.requests[client_id] if ts > cutoff
]
if len(self.requests[client_id]) >= self.max_requests:
return False
self.requests[client_id].append(now)
return True
# Apply different limits based on authentication type
rate_limits = {
"api_key": RateLimiter(max_requests=100, window_seconds=60),
"oauth_client_credentials": RateLimiter(max_requests=1000, window_seconds=60),
"oauth_user": RateLimiter(max_requests=300, window_seconds=60),
}
Token Best Practices
Access Token Lifetime
Keep access tokens short-lived:
- 5-15 minutes for standard API access
- 1-5 minutes for high-security scenarios
- Use refresh tokens (with rotation) for session continuity
Configure these in Keycloak under Realm Settings > Tokens or per-client in Client > Advanced.
Scope Design
Design granular scopes for your APIs:
# Good: specific, actionable scopes
orders:read
orders:create
users:manage
reports:export
# Bad: overly broad scopes
admin
read
write
Map scopes to Keycloak client scopes and roles for enforcement. For RBAC patterns in Keycloak, see our feature guide.
Token Content
Minimize the data in access tokens:
- Include only the claims the API needs for authorization decisions
- Use opaque reference tokens for APIs that do not need to read token contents
- Avoid putting sensitive PII in tokens (tokens are base64-encoded, not encrypted)
You can inspect token contents using the JWT Token Analyzer and SAML Decoder for SAML-based integrations.
Logging and Monitoring
Track API authentication events for security and debugging:
- Log successful and failed authentication attempts with correlation IDs
- Monitor token issuance rates for anomaly detection
- Alert on unusual patterns: spikes in failed validations, requests from new IP ranges, credential stuffing indicators
- Correlate with Keycloak audit logs to trace authentication events end-to-end
For centralized monitoring of your identity infrastructure, Skycloak provides insights dashboards that track authentication metrics across your deployment.
Security Checklist
Before deploying your API authentication:
- [ ] HTTPS everywhere (no exceptions)
- [ ] PKCE enabled for all authorization code flows
- [ ] Access tokens are short-lived (< 15 minutes)
- [ ] Refresh token rotation is enabled
- [ ] Client secrets are not embedded in frontend code
- [ ] JWT signature verification uses the JWKS endpoint (not hardcoded keys)
- [ ] Token audience (
aud) claim is validated - [ ] Token issuer (
iss) claim is validated - [ ] Rate limiting is in place per client/user
- [ ] Authentication failures are logged (without logging sensitive data)
- [ ] CORS is configured properly for browser-based clients
- [ ] DPoP or mTLS is used for high-value operations
For a comprehensive overview of security practices, see our security page.
Conclusion
API authentication in 2026 is centered on OAuth 2.0 with PKCE as the default for user-facing flows, client credentials for service-to-service, and DPoP or mTLS for sender-constrained tokens. API keys still have a place for simple identification and rate limiting, but they should not be your primary authentication mechanism for sensitive data.
Keycloak provides all of these mechanisms out of the box, making it a solid foundation for API security — see the Keycloak securing applications guide for protocol-level details. Whether you manage it yourself or use a managed Keycloak service, the patterns in this guide apply regardless of your deployment approach.
Ready to implement these API authentication patterns? Check out Skycloak’s pricing for managed Keycloak hosting that handles the infrastructure, or explore the documentation for integration guides.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.