---
# === IDENTITY ===
id: software/patterns/oauth2-client-credentials/2026
canonical_question: "How do I implement OAuth2 Client Credentials flow?"
aliases:
  - "OAuth2 client credentials grant"
  - "machine-to-machine OAuth2"
  - "M2M authentication OAuth"
  - "service-to-service OAuth token"
  - "client_credentials grant_type"
  - "OAuth2 server-to-server auth"
  - "client ID client secret token exchange"
  - "OAuth2 two-legged flow"
entity_type: software_reference
domain: software > patterns > oauth2_client_credentials
region: global
jurisdiction: global
temporal_scope: 2012-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.92
freshness: annual
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "RFC 6749 published 2012-10; no breaking changes since — OAuth 2.1 (draft) preserves client_credentials grant unchanged"
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Client Credentials grant MUST only be used by confidential clients (server-side) that can securely store credentials — never from browsers, mobile apps, or SPAs"
  - "Never expose client_secret in client-side code, version control, or logs — use a secrets manager (Vault, AWS Secrets Manager, Azure Key Vault)"
  - "Access tokens SHOULD be short-lived (15-60 minutes) — the spec does not issue refresh tokens for this grant type"
  - "Always request minimum required scopes — principle of least privilege prevents lateral movement if credentials are compromised"
  - "Token endpoint MUST be called over TLS (HTTPS) — client credentials sent in plaintext over HTTP can be intercepted"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to act on behalf of a user (access user's resources, user consent required)"
    use_instead: "Use Authorization Code flow with PKCE (software/patterns/oauth2-authorization-code-pkce/2026)"
  - condition: "Client is a browser SPA or mobile app that cannot securely store a client_secret"
    use_instead: "Use Authorization Code flow with PKCE — no client_secret required"
  - condition: "Device has no browser and limited input (smart TV, IoT)"
    use_instead: "Use Device Authorization Grant / Device Code flow (software/patterns/oauth2-device-flow/2026)"
  - condition: "Need simple API key authentication without OAuth infrastructure"
    use_instead: "Use API key pattern (software/patterns/api-key-management/2026)"

# === AGENT HINTS ===
inputs_needed:
  - key: "auth_provider"
    question: "Which authorization server / identity provider are you using?"
    type: choice
    options: ["Auth0", "Okta", "Microsoft Entra ID (Azure AD)", "AWS Cognito", "Google Cloud", "Keycloak", "Custom/Other"]
  - key: "language"
    question: "Which programming language is your service written in?"
    type: choice
    options: ["Python", "Node.js/TypeScript", "Go", "Java/Kotlin", "C#/.NET", "Other"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/oauth2-client-credentials/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Implementation — understanding token format used by most OAuth2 providers"
  related_to:
    - id: "software/patterns/oauth2-authorization-code-pkce/2026"
      label: "OAuth2 Authorization Code with PKCE"
    - id: "software/patterns/oauth2-device-flow/2026"
      label: "OAuth2 Device Authorization Grant"
    - id: "software/patterns/api-key-management/2026"
      label: "API Key Management Patterns"
    - id: "software/system-design/auth-system/2026"
      label: "Authentication System Design"
  alternative_to:
    - id: "software/patterns/api-key-management/2026"
      label: "API Key Authentication — simpler but less secure alternative for M2M"
    - id: "software/patterns/mtls-authentication/2026"
      label: "Mutual TLS (mTLS) — certificate-based M2M auth without OAuth"
  often_confused_with:
    - id: "software/patterns/oauth2-authorization-code-pkce/2026"
      label: "Authorization Code with PKCE — user-delegated access, not machine-to-machine"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "RFC 6749: The OAuth 2.0 Authorization Framework — Section 4.4 Client Credentials Grant"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
    type: rfc_spec
    published: 2012-10-01
    reliability: authoritative
  - id: src2
    title: "Client Credentials Flow — Auth0 Documentation"
    author: Auth0
    url: https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow
    type: official_docs
    published: 2025-01-15
    reliability: high
  - id: src3
    title: "OAuth 2.0 Client Credentials Grant Type"
    author: OAuth.net
    url: https://oauth.net/2/grant-types/client-credentials/
    type: community_resource
    published: 2024-06-01
    reliability: high
  - id: src4
    title: "OAuth 2.0 client credentials flow on the Microsoft identity platform"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow
    type: official_docs
    published: 2025-02-01
    reliability: high
  - id: src5
    title: "Token Best Practices — Auth0 Documentation"
    author: Auth0
    url: https://auth0.com/docs/secure/tokens/token-best-practices
    type: official_docs
    published: 2025-01-15
    reliability: high
  - id: src6
    title: "OAuth 2.0 Security Best Current Practice (draft-ietf-oauth-security-topics-29)"
    author: IETF OAuth Working Group
    url: https://www.ietf.org/archive/id/draft-ietf-oauth-security-topics-29.html
    type: rfc_spec
    published: 2024-12-01
    reliability: authoritative
  - id: src7
    title: "Implement Client Credentials Grant — Okta Developer Documentation"
    author: Okta
    url: https://developer.okta.com/docs/guides/implement-grant-type/clientcreds/main/
    type: official_docs
    published: 2025-01-01
    reliability: high
---

# OAuth2 Client Credentials Flow: Machine-to-Machine Authentication

## TL;DR

- **Bottom line**: The Client Credentials grant (RFC 6749 Section 4.4) lets a server-side application exchange its `client_id` + `client_secret` for an access token — no user interaction required. It is the standard pattern for machine-to-machine (M2M) authentication.
- **Key tool/command**: `POST /oauth/token` with `grant_type=client_credentials&client_id=ID&client_secret=SECRET&scope=SCOPE`
- **Watch out for**: Not caching tokens — every uncached token request adds latency and can trigger rate limits; cache tokens in memory until `expires_in - buffer`.
- **Works with**: Any OAuth 2.0 compliant authorization server (Auth0, Okta, Microsoft Entra ID, AWS Cognito, Keycloak, Google Cloud IAM, custom servers).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- **Confidential clients only** — this grant MUST NOT be used by public clients (SPAs, mobile apps, desktop apps). The client must be able to securely store its secret. [src1]
- **Never expose client_secret** — do not commit to version control, embed in client-side code, or log. Use environment variables or a secrets manager (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault). [src6]
- **No refresh tokens** — the spec explicitly states refresh tokens SHOULD NOT be issued for this grant. Re-authenticate with credentials to get a new token. [src1]
- **TLS required** — the token endpoint MUST be accessed over HTTPS. Credentials sent over HTTP can be intercepted. [src1]
- **Minimum scope** — always request only the scopes your service actually needs. Over-scoped tokens increase blast radius on compromise. [src6]

## Quick Reference

| Step | Action | Details |
|---|---|---|
| 1. Register client | Create M2M application in your IdP | Obtain `client_id` and `client_secret` |
| 2. Configure scopes | Define API permissions | Server-side, no user consent needed |
| 3. Request token | `POST /oauth/token` | `grant_type=client_credentials` + credentials + scope |
| 4. Auth method | HTTP Basic or POST body | `Authorization: Basic base64(id:secret)` or form params |
| 5. Receive token | Parse JSON response | `{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }` |
| 6. Cache token | Store in memory | Cache until `expires_in - 60s` buffer |
| 7. Use token | Add to API calls | `Authorization: Bearer {access_token}` |
| 8. Handle expiry | Re-request on 401 | Fetch new token, retry original request |
| 9. Rotate credentials | Periodic key rotation | Use two active secrets for zero-downtime rotation |
| 10. Monitor | Log token requests | Track issuance rate, failures, scope usage |

**Token Request Parameters:**

| Parameter | Required | Value |
|---|---|---|
| `grant_type` | Yes | `client_credentials` |
| `client_id` | Yes | Your application's client ID |
| `client_secret` | Yes | Your application's client secret |
| `scope` | Recommended | Space-separated list of requested scopes |
| `audience` | Provider-specific | API identifier (Auth0, Okta) |
| `resource` | Provider-specific | Resource URI (Microsoft Entra ID) |

**Common Provider Token Endpoints:**

| Provider | Token Endpoint |
|---|---|
| Auth0 | `https://{tenant}.auth0.com/oauth/token` |
| Okta | `https://{domain}/oauth2/default/v1/token` |
| Microsoft Entra ID | `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token` |
| AWS Cognito | `https://{domain}.auth.{region}.amazoncognito.com/oauth2/token` |
| Google Cloud | `https://oauth2.googleapis.com/token` |
| Keycloak | `https://{host}/realms/{realm}/protocol/openid-connect/token` |

## Decision Tree

```
START: Does your service need to call a protected API?
├── Is there a user context (acting on behalf of a user)?
│   ├── YES → Use Authorization Code flow with PKCE (not Client Credentials)
│   └── NO ↓
├── Is the client a server-side application that can securely store secrets?
│   ├── NO → Cannot use Client Credentials. Use API keys or mTLS instead.
│   └── YES ↓
├── Does the provider support certificate-based auth?
│   ├── YES and security policy requires it → Use client_assertion (JWT signed with cert)
│   └── NO or shared secret is acceptable ↓
├── Does the provider require an audience/resource parameter?
│   ├── Auth0/Okta → Include `audience` parameter
│   ├── Microsoft Entra ID → Include `scope` with `.default` suffix
│   └── Standard → Use `scope` parameter only
└── IMPLEMENT → POST to token endpoint, cache token, add Bearer header to API calls
```

## Step-by-Step Guide

### 1. Register a Machine-to-Machine application

Create an M2M (non-interactive) application in your identity provider. Record the `client_id` and `client_secret`. Store the secret in your secrets manager immediately — never copy it to code or config files. [src2]

```bash
# Example: Auth0 — create via Management API
curl -X POST "https://{tenant}.auth0.com/api/v2/clients" \
  -H "Authorization: Bearer {mgmt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Backend Service",
    "app_type": "non_interactive",
    "grant_types": ["client_credentials"]
  }'
```

**Verify**: Check your IdP dashboard — the new application should appear with `client_credentials` as the only allowed grant type.

### 2. Configure API permissions (scopes)

Authorize your application to access specific APIs with specific scopes. This is provider-specific but typically done in the IdP admin console. [src2]

```bash
# Example: Auth0 — grant API access to the client
curl -X POST "https://{tenant}.auth0.com/api/v2/client-grants" \
  -H "Authorization: Bearer {mgmt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "{your_client_id}",
    "audience": "https://api.example.com",
    "scope": ["read:data", "write:data"]
  }'
```

**Verify**: In IdP console, confirm the application has the expected API grants and scopes listed.

### 3. Request an access token

Send a POST request to the token endpoint with `grant_type=client_credentials`. Credentials can be sent as HTTP Basic auth or as POST body parameters. [src1]

```bash
# Method A: Credentials in POST body (most common)
curl -X POST "https://{tenant}.auth0.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "audience=https://api.example.com"

# Method B: HTTP Basic Authentication (RFC 6749 recommended)
curl -X POST "https://{tenant}.auth0.com/oauth/token" \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "audience=https://api.example.com"
```

**Verify**: Response is `200 OK` with JSON containing `access_token`, `token_type`, and `expires_in`:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 86400
}
```

### 4. Cache the token and use it

Cache the token in memory with an expiry buffer. Use it in the `Authorization` header for all API calls. On `401 Unauthorized`, refresh the token and retry. [src5]

```python
import time, threading

class TokenCache:
    """Thread-safe in-memory token cache with expiry buffer."""
    def __init__(self, fetch_fn, buffer_seconds=60):
        self._fetch = fetch_fn
        self._buffer = buffer_seconds
        self._token = None
        self._expires_at = 0
        self._lock = threading.Lock()

    def get_token(self):
        if time.time() < self._expires_at:
            return self._token
        with self._lock:
            if time.time() < self._expires_at:  # Double-check after lock
                return self._token
            data = self._fetch()
            self._token = data["access_token"]
            self._expires_at = time.time() + data["expires_in"] - self._buffer
            return self._token
```

**Verify**: Call `get_token()` twice in succession — the second call should return instantly without an HTTP request (cache hit).

### 5. Implement credential rotation

Set up two active client secrets with overlapping validity. Rotate the older secret periodically without downtime. [src6]

```bash
# 1. Generate a new secret (provider-specific)
# Auth0: POST /api/v2/clients/{client_id}/credentials
# Okta: POST /api/v1/apps/{app_id}/credentials/secrets

# 2. Deploy the new secret to your service
# 3. Verify the new secret works (test token request)
# 4. Revoke the old secret after deployment is confirmed
```

**Verify**: Confirm token requests succeed with the new secret. Monitor for failures during rotation window.

## Code Examples

### Python: Client Credentials with httpx

```python
# Input:  client_id, client_secret, token_url, audience
# Output: Bearer access token for API calls

import httpx  # httpx >= 0.27.0

def get_client_credentials_token(
    token_url: str,
    client_id: str,
    client_secret: str,
    scope: str = "",
    audience: str = "",
) -> dict:
    """Request token via OAuth2 Client Credentials grant."""
    payload = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
    }
    if scope:
        payload["scope"] = scope
    if audience:
        payload["audience"] = audience

    resp = httpx.post(token_url, data=payload)
    resp.raise_for_status()
    return resp.json()  # {"access_token": "...", "expires_in": 3600, ...}
```

### Node.js: Client Credentials with fetch

```javascript
// Input:  tokenUrl, clientId, clientSecret, scope
// Output: { access_token, token_type, expires_in }

async function getClientCredentialsToken(tokenUrl, clientId, clientSecret, scope = '') {
  const params = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: clientId,
    client_secret: clientSecret,
  });
  if (scope) params.set('scope', scope);

  const res = await fetch(tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
  return res.json();
}
```

### Go: Client Credentials with golang.org/x/oauth2

```go
// Input:  clientID, clientSecret, tokenURL, scopes
// Output: *oauth2.Token with AccessToken field

package main

import (
    "context"
    "fmt"
    "golang.org/x/oauth2"          // v0.25.0+
    "golang.org/x/oauth2/clientcredentials"
)

func getToken(clientID, clientSecret, tokenURL string, scopes []string) (*oauth2.Token, error) {
    cfg := &clientcredentials.Config{
        ClientID:     clientID,
        ClientSecret: clientSecret,
        TokenURL:     tokenURL,
        Scopes:       scopes,
    }
    // TokenSource automatically caches and refreshes the token
    token, err := cfg.Token(context.Background())
    if err != nil {
        return nil, fmt.Errorf("token request failed: %w", err)
    }
    return token, nil
}
```

### cURL: Direct token request

```bash
# Input:  TOKEN_URL, CLIENT_ID, CLIENT_SECRET, SCOPE
# Output: JSON with access_token, token_type, expires_in

curl -s -X POST "$TOKEN_URL" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "scope=$SCOPE" | jq .
```

## Anti-Patterns

### Wrong: Hardcoding credentials in source code

```python
# BAD — credentials in source code get committed to git
client_id = "abc123"
client_secret = "super_secret_key_2026"
token = get_token(client_id, client_secret)
```

### Correct: Load credentials from environment or secrets manager

```python
# GOOD — credentials loaded from environment variables
import os
client_id = os.environ["OAUTH_CLIENT_ID"]
client_secret = os.environ["OAUTH_CLIENT_SECRET"]
token = get_token(client_id, client_secret)
```

### Wrong: Requesting a new token on every API call

```javascript
// BAD — token request on every call adds ~200ms latency + risks rate limiting
async function callApi(endpoint) {
  const { access_token } = await getToken();  // Network call every time!
  return fetch(endpoint, { headers: { Authorization: `Bearer ${access_token}` } });
}
```

### Correct: Cache tokens and reuse until near-expiry

```javascript
// GOOD — cache token, refresh only when expired (with 60s buffer)
let cached = { token: null, expiresAt: 0 };

async function callApi(endpoint) {
  if (Date.now() / 1000 >= cached.expiresAt) {
    const data = await getToken();
    cached = { token: data.access_token, expiresAt: Date.now() / 1000 + data.expires_in - 60 };
  }
  return fetch(endpoint, { headers: { Authorization: `Bearer ${cached.token}` } });
}
```

### Wrong: Requesting all available scopes

```bash
# BAD — over-scoped token: if compromised, attacker has full access
curl -X POST "$TOKEN_URL" -d "grant_type=client_credentials" \
  -d "client_id=$ID" -d "client_secret=$SECRET" \
  -d "scope=read:all write:all admin:all delete:all"
```

### Correct: Request minimum required scopes

```bash
# GOOD — only request what this specific service needs
curl -X POST "$TOKEN_URL" -d "grant_type=client_credentials" \
  -d "client_id=$ID" -d "client_secret=$SECRET" \
  -d "scope=read:orders write:shipments"
```

### Wrong: Using Client Credentials from a browser

```javascript
// BAD — client_secret exposed in browser JS, visible in DevTools
const token = await fetch('/oauth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: 'my_client_id',
    client_secret: 'EXPOSED_SECRET',  // Anyone can see this!
  }),
});
```

### Correct: Use Authorization Code with PKCE for browser apps

```javascript
// GOOD — PKCE flow for browser: no client_secret needed
// Redirect user to authorization endpoint with code_challenge
window.location.href = `${authUrl}/authorize?` +
  `response_type=code&client_id=${clientId}&` +
  `redirect_uri=${redirectUri}&` +
  `code_challenge=${codeChallenge}&code_challenge_method=S256`;
```

## Common Pitfalls

- **Token not cached, service hits rate limits**: Most providers rate-limit the token endpoint (e.g., Auth0: 1000 req/min). Fix: Cache tokens in memory with `expires_in - 60s` buffer. [src5]
- **Using HTTP instead of HTTPS for token endpoint**: Credentials sent in cleartext. Many providers reject HTTP entirely; others silently serve tokens over insecure connection. Fix: Always use `https://` URLs; configure TLS verification in your HTTP client. [src1]
- **Wrong Content-Type header**: Token endpoint expects `application/x-www-form-urlencoded`, not `application/json`. Using JSON body silently fails on most providers. Fix: Set `Content-Type: application/x-www-form-urlencoded` and URL-encode the body. [src1]
- **Forgetting audience/resource parameter**: Auth0 requires `audience`, Microsoft Entra ID requires `scope` with `.default` suffix. Without it, you get a token that cannot access any API. Fix: Check your provider's documentation for required parameters. [src2] [src4]
- **Not URL-encoding the client_secret**: Secrets containing `+`, `&`, `=`, or `%` break form encoding. Fix: Always URL-encode credentials when sending as POST body, or use HTTP Basic auth (base64-encoded). [src4]
- **Treating token_type as case-sensitive**: RFC 6749 specifies `token_type` is case-insensitive. Some providers return `bearer` (lowercase). Fix: Compare `token_type.toLowerCase() === "bearer"`. [src1]
- **No retry logic on token failure**: Network blips or provider outages cause hard failures. Fix: Implement exponential backoff (3 retries, 1s/2s/4s) for token requests. [src6]
- **Single secret with no rotation**: When a secret must be rotated, there is downtime. Fix: Maintain two active secrets; deploy new secret first, then revoke old one. [src6]

## Diagnostic Commands

```bash
# Test token request (replace variables)
curl -v -X POST "$TOKEN_URL" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&scope=$SCOPE"

# Decode a JWT access token (inspect claims without verification)
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Check token expiry from JWT claims
echo "$ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{exp: .exp, exp_human: (.exp | todate), iss: .iss, aud: .aud, scope: .scope}'

# Verify token against an API endpoint
curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS_TOKEN" "$API_URL/health"

# Check provider token endpoint connectivity
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" -X POST "$TOKEN_URL" -d ""

# Monitor token request latency
time curl -s -X POST "$TOKEN_URL" \
  -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" > /dev/null
```

## Version History & Compatibility

| Spec/Version | Status | Key Changes | Notes |
|---|---|---|---|
| RFC 6749 (2012) | Current Standard | Defined Client Credentials grant in Section 4.4 | Foundation spec; all providers implement this |
| OAuth 2.1 (draft-ietf-oauth-v2-1) | Draft (expected 2025-2026) | Preserves client_credentials grant unchanged | Removes implicit + ROPC grants; client_credentials unaffected |
| RFC 7523 (2015) | Current | JWT Profile for client authentication (client_assertion) | Alternative to shared secrets; uses signed JWT |
| RFC 8705 (2020) | Current | Mutual TLS for OAuth (sender-constrained tokens) | Binds tokens to client certificate; strongest auth method |
| draft-ietf-oauth-security-topics-29 (2024) | Draft BCP | Recommends asymmetric client auth; sender-constrained tokens | Guidance, not a breaking change |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Service-to-service / M2M communication | User needs to grant consent | Authorization Code + PKCE |
| Backend cron jobs calling APIs | Client is a browser SPA | Authorization Code + PKCE |
| Microservice-to-microservice auth | Client is a mobile/desktop app | Authorization Code + PKCE |
| CI/CD pipeline API access | IoT device with no secure storage | Device Authorization Grant |
| Daemon processes and workers | Simple internal API with no OAuth infra | API keys or mTLS |
| Automated data sync between services | Need user-specific data/permissions | Authorization Code + PKCE |

## Important Caveats

- **No user context**: Tokens issued via Client Credentials have no `sub` (subject) claim representing a user. API authorization logic must account for app-level vs. user-level tokens differently.
- **Provider-specific parameters**: Auth0 requires `audience`, Microsoft Entra ID uses `scope` with `.default` suffix, Okta uses `scope` directly. Always check your provider's documentation.
- **Certificate auth preferred for high security**: RFC 7523 (JWT assertion) and RFC 8705 (mTLS) are recommended over shared secrets by the IETF Security BCP. Shared secrets are acceptable but have higher credential leakage risk. [src6]
- **Rate limiting**: Token endpoints are typically rate-limited. Auth0 allows ~1000 requests/minute per tenant. Always cache tokens.
- **Opaque vs. JWT tokens**: Some providers issue opaque tokens (Auth0 for first-party APIs), others issue JWTs. Do not parse or validate tokens for APIs you do not own. [src4]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [JWT Implementation](/software/patterns/jwt-implementation/2026)
- [OAuth2 Authorization Code with PKCE](/software/patterns/oauth2-authorization-code-pkce/2026)
- [OAuth2 Device Authorization Grant](/software/patterns/oauth2-device-flow/2026)
- [API Key Management](/software/patterns/api-key-management/2026)
- [Authentication System Design](/software/system-design/auth-system/2026)
