SDK Authentication: API Key, OAuth, and Token Management in Client Libraries
Learn how to implement multiple authentication strategies in AI agent SDKs, including API key management, OAuth 2.0 flows, automatic token refresh, and authentication middleware patterns.
Authentication Strategies for Agent SDKs
Most AI agent platforms start with API key authentication and graduate to OAuth as they add multi-tenant features. A well-designed SDK supports both without forcing users to rewrite their code when upgrading.
The key insight is to abstract authentication behind a provider interface. The HTTP client should not care whether it is attaching an API key header or a bearer token from an OAuth flow — it just asks the auth provider for the current credentials.
API Key Authentication
API keys are the simplest and most common pattern. The SDK accepts a key at construction time and attaches it to every request:
import os
from typing import Protocol
class AuthProvider(Protocol):
"""Protocol for authentication providers."""
def get_headers(self) -> dict[str, str]: ...
class APIKeyAuth:
"""Authenticates requests with a static API key."""
def __init__(self, api_key: str | None = None) -> None:
self.api_key = api_key or os.environ.get("MYAGENT_API_KEY")
if not self.api_key:
raise ValueError(
"API key required. Pass api_key= or set MYAGENT_API_KEY."
)
def get_headers(self) -> dict[str, str]:
return {"Authorization": f"Bearer {self.api_key}"}
The AuthProvider protocol defines the contract. Any auth strategy that implements get_headers() works with the client. This is the critical design decision — decouple the auth mechanism from the HTTP transport.
OAuth 2.0 Client Credentials
For server-to-server authentication, OAuth 2.0 client credentials flow is standard. The SDK exchanges a client ID and secret for a time-limited access token:
import time
import httpx
from dataclasses import dataclass
@dataclass
class TokenResponse:
access_token: str
expires_at: float
token_type: str
class OAuthClientCredentials:
"""OAuth 2.0 client credentials with automatic token refresh."""
def __init__(
self,
client_id: str,
client_secret: str,
token_url: str = "https://auth.myagent.ai/oauth/token",
scopes: list[str] | None = None,
) -> None:
self.client_id = client_id
self.client_secret = client_secret
self.token_url = token_url
self.scopes = scopes or []
self._token: TokenResponse | None = None
self._http = httpx.Client()
def _fetch_token(self) -> TokenResponse:
response = self._http.post(
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": " ".join(self.scopes),
},
)
response.raise_for_status()
data = response.json()
return TokenResponse(
access_token=data["access_token"],
expires_at=time.time() + data["expires_in"] - 30,
token_type=data["token_type"],
)
def _ensure_valid_token(self) -> TokenResponse:
if self._token is None or time.time() >= self._token.expires_at:
self._token = self._fetch_token()
return self._token
def get_headers(self) -> dict[str, str]:
token = self._ensure_valid_token()
return {"Authorization": f"Bearer {token.access_token}"}
The 30-second buffer before expiry (expires_in - 30) prevents race conditions where a token expires between header generation and the server receiving the request.
TypeScript Auth Middleware
In TypeScript, implement the same pattern with an interface and a request interceptor approach:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
interface AuthProvider {
getHeaders(): Promise<Record<string, string>>;
}
class APIKeyAuth implements AuthProvider {
constructor(private readonly apiKey: string) {}
async getHeaders(): Promise<Record<string, string>> {
return { Authorization: `Bearer ${this.apiKey}` };
}
}
class OAuthAuth implements AuthProvider {
private token: { accessToken: string; expiresAt: number } | null = null;
constructor(
private readonly clientId: string,
private readonly clientSecret: string,
private readonly tokenUrl: string,
) {}
async getHeaders(): Promise<Record<string, string>> {
if (!this.token || Date.now() >= this.token.expiresAt) {
await this.refreshToken();
}
return { Authorization: `Bearer ${this.token!.accessToken}` };
}
private async refreshToken(): Promise<void> {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
const data = await response.json();
this.token = {
accessToken: data.access_token,
expiresAt: Date.now() + (data.expires_in - 30) * 1000,
};
}
}
Wiring Auth Into the Client
The client constructor accepts either an API key string or an auth provider instance. This preserves the simple path while enabling advanced authentication:
class AgentClient:
def __init__(
self,
api_key: str | None = None,
auth: AuthProvider | None = None,
) -> None:
if auth is not None:
self._auth = auth
elif api_key is not None:
self._auth = APIKeyAuth(api_key)
else:
self._auth = APIKeyAuth() # Falls back to env var
def _request(self, method: str, path: str, **kwargs):
headers = self._auth.get_headers()
# Merge auth headers with request headers
kwargs.setdefault("headers", {}).update(headers)
return self._http.request(method, path, **kwargs)
Users who just need an API key pass a string. Users with OAuth requirements pass a provider. The SDK handles both identically in the HTTP layer.
Secure Credential Storage
Never log, serialize, or expose credentials in error messages. Implement a __repr__ that masks sensitive data:
class APIKeyAuth:
def __repr__(self) -> str:
masked = self.api_key[:4] + "..." + self.api_key[-4:]
return f"APIKeyAuth(api_key='{masked}')"
This ensures that if the auth object appears in a traceback, the full key is not leaked.
FAQ
Should an SDK store API keys in a config file?
No. SDKs should accept keys at runtime via constructor parameters or environment variables. Storing keys in files creates security risks — config files end up in version control, shared filesystems, or backups. Let the user's deployment tooling (secrets managers, environment variables) handle storage.
How do I handle token refresh in concurrent scenarios?
Use a lock to prevent multiple simultaneous token refreshes. In Python, use threading.Lock() for sync clients or asyncio.Lock() for async. Without a lock, ten concurrent requests on an expired token will trigger ten separate token refresh calls, wasting API quota and potentially causing rate limiting.
Should the SDK support multiple authentication methods simultaneously?
No. A single client instance should use one authentication method. If a user needs to call the API with different credentials (for example, on behalf of different tenants), they should create separate client instances. Mixing authentication methods within a single client creates ambiguity about which credentials are used for each request.
#Authentication #OAuth #APIKeys #SDKDesign #Security #AgenticAI #LearnAI #AIEngineering
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.