Skip to content
Learn Agentic AI12 min read0 views

API Authentication for AI Agent Services: API Keys, OAuth2, and JWT Patterns

Implement secure authentication for AI agent APIs using API keys for simple access, OAuth2 for delegated authorization, and JWT tokens for stateless verification. Learn token lifecycle management, scope-based permissions, and security best practices.

Authentication Challenges for AI Agent APIs

AI agent APIs face unique authentication challenges. Agents run unattended, so they cannot participate in interactive login flows. They often need different permission levels — a research agent should not have the same access as an admin agent. Agents may also delegate work to sub-agents, creating chains of authorization that need proper scoping.

The three primary authentication patterns — API keys, OAuth2 client credentials, and JWT tokens — each solve different parts of this puzzle. Most production agent systems combine two or three of them depending on the context.

API Key Authentication

API keys are the simplest starting point. They work well for server-to-server agent communication where both sides are trusted internal services:

from fastapi import FastAPI, Security, HTTPException
from fastapi.security import APIKeyHeader
import hashlib
import secrets

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")

# In production, store hashed keys in a database
API_KEYS = {
    # key_hash -> {agent_id, scopes, rate_limit_tier}
}

def hash_key(key: str) -> str:
    return hashlib.sha256(key.encode()).hexdigest()

def generate_api_key(agent_id: str, scopes: list[str]) -> str:
    key = f"sk-agent-{secrets.token_urlsafe(32)}"
    API_KEYS[hash_key(key)] = {
        "agent_id": agent_id,
        "scopes": scopes,
        "active": True,
    }
    return key  # Only shown once at creation time

async def verify_api_key(key: str = Security(api_key_header)) -> dict:
    key_hash = hash_key(key)
    record = API_KEYS.get(key_hash)
    if not record or not record["active"]:
        raise HTTPException(status_code=401, detail="Invalid API key")
    return record

@app.get("/v1/conversations")
async def list_conversations(auth: dict = Security(verify_api_key)):
    if "conversations:read" not in auth["scopes"]:
        raise HTTPException(status_code=403, detail="Insufficient scope")
    return {"conversations": []}

Key design decisions: always hash keys before storage so a database leak does not expose credentials, prefix keys with a recognizable pattern like sk-agent- for easy identification in logs, and attach scopes to each key for fine-grained access control.

OAuth2 Client Credentials for Agent-to-Agent Auth

When agents from different organizations or trust boundaries need to communicate, OAuth2 client credentials provide a standardized flow. The agent exchanges a client ID and secret for a short-lived access token:

from datetime import datetime, timedelta
import jwt

TOKEN_SECRET = "your-signing-secret"  # Use a vault in production
TOKEN_EXPIRY = timedelta(minutes=30)

class OAuth2ClientStore:
    clients = {
        "agent-billing-v2": {
            "secret_hash": hash_key("billing-secret-abc123"),
            "scopes": ["billing:read", "billing:write"],
            "agent_name": "Billing Agent",
        },
    }

client_store = OAuth2ClientStore()

@app.post("/oauth/token")
async def issue_token(client_id: str, client_secret: str, scope: str = ""):
    client = client_store.clients.get(client_id)
    if not client:
        raise HTTPException(status_code=401, detail="Unknown client")

    if hash_key(client_secret) != client["secret_hash"]:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    requested_scopes = scope.split() if scope else client["scopes"]
    # Only grant scopes the client is authorized for
    granted = [s for s in requested_scopes if s in client["scopes"]]

    token = jwt.encode(
        {
            "sub": client_id,
            "scopes": granted,
            "exp": datetime.utcnow() + TOKEN_EXPIRY,
            "iat": datetime.utcnow(),
            "type": "access_token",
        },
        TOKEN_SECRET,
        algorithm="HS256",
    )
    return {
        "access_token": token,
        "token_type": "bearer",
        "expires_in": int(TOKEN_EXPIRY.total_seconds()),
        "scope": " ".join(granted),
    }

The agent requests a token once, caches it, and includes it in subsequent API calls. When the token expires, the agent requests a new one. This avoids sending the client secret with every request.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

JWT Verification Middleware

Once tokens are issued, every endpoint needs to verify them. Build a reusable dependency:

from fastapi import Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

bearer_scheme = HTTPBearer()

async def get_current_agent(
    credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> dict:
    token = credentials.credentials
    try:
        payload = jwt.decode(token, TOKEN_SECRET, algorithms=["HS256"])
    except jwt.ExpiredSignatureError:
        raise HTTPException(
            status_code=401,
            detail="Token expired",
            headers={"WWW-Authenticate": "Bearer"},
        )
    except jwt.InvalidTokenError:
        raise HTTPException(
            status_code=401,
            detail="Invalid token",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return payload

def require_scope(required: str):
    async def checker(agent: dict = Depends(get_current_agent)):
        if required not in agent.get("scopes", []):
            raise HTTPException(
                status_code=403,
                detail=f"Missing required scope: {required}",
            )
        return agent
    return checker

@app.get("/v1/billing/balance")
async def get_balance(agent: dict = Depends(require_scope("billing:read"))):
    return {"balance": 150.00, "currency": "USD"}

Token Lifecycle and Rotation

Agents need to handle token refresh without interrupting their work:

import httpx
import asyncio
from datetime import datetime

class AgentTokenManager:
    def __init__(self, token_url: str, client_id: str, client_secret: str):
        self.token_url = token_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token: str | None = None
        self.expires_at: datetime | None = None
        self._lock = asyncio.Lock()

    async def get_token(self) -> str:
        async with self._lock:
            if self.access_token and self.expires_at:
                # Refresh 60 seconds before expiry
                if datetime.utcnow() < self.expires_at - timedelta(seconds=60):
                    return self.access_token

            async with httpx.AsyncClient() as client:
                response = await client.post(self.token_url, data={
                    "client_id": self.client_id,
                    "client_secret": self.client_secret,
                })
                data = response.json()
                self.access_token = data["access_token"]
                self.expires_at = datetime.utcnow() + timedelta(
                    seconds=data["expires_in"]
                )
                return self.access_token

# Usage in an agent
token_mgr = AgentTokenManager(
    token_url="https://auth.agents.internal/oauth/token",
    client_id="agent-research-v1",
    client_secret="research-secret-xyz",
)

async def call_billing_api():
    token = await token_mgr.get_token()
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://billing-agent.internal/v1/billing/balance",
            headers={"Authorization": f"Bearer {token}"},
        )
        return response.json()

The AgentTokenManager uses a lock to prevent multiple concurrent token refreshes and proactively refreshes 60 seconds before expiry to avoid any window where the token is invalid.

FAQ

When should I use API keys versus OAuth2 for agent authentication?

Use API keys for internal agent-to-agent communication within a single trust boundary where simplicity matters. Use OAuth2 client credentials when agents cross trust boundaries, when you need short-lived tokens, or when you want centralized token management with a dedicated auth server.

How do I revoke access for a compromised agent?

For API keys, mark the key as inactive in your database — revocation is immediate on the next request. For JWTs, you cannot truly revoke them before expiry since they are stateless. Mitigate this by keeping token lifetimes short (15-30 minutes) and maintaining a deny-list of revoked token IDs checked during verification.

Should each agent have its own credentials or share a service account?

Each agent should have its own credentials. Shared service accounts make it impossible to audit which agent performed an action, enforce per-agent rate limits, or revoke access to a single agent without affecting others. The small overhead of managing individual credentials pays for itself in observability and security.


#Authentication #AIAgents #OAuth2 #JWT #APISecurity #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.