API Authentication Best Practices in 2026

Guilliano Molaire Guilliano Molaire 10 min read

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:

  1. Create a new client in the admin console
  2. Set Client authentication to OFF (public client)
  3. Enable Standard flow
  4. Set PKCE Code Challenge Method to S256
  5. 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:

  1. Create a new client
  2. Set Client authentication to ON
  3. Enable Service account roles
  4. Disable Standard flow and Direct access grants
  5. 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:

  1. In the client settings, set Client Authenticator to “X509 Certificate”
  2. Configure the Subject DN pattern that Keycloak should expect
  3. 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

  1. The client generates an ephemeral key pair
  2. When requesting a token, the client sends a DPoP proof (a signed JWT) containing the public key
  3. The authorization server binds the access token to that public key
  4. When calling an API, the client sends both the access token and a new DPoP proof
  5. 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:

  1. In client settings, go to Advanced
  2. 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.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman