---
title: "SDK Authentication: API Key, OAuth, and Token Management in Client Libraries"
description: "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."
canonical: https://callsphere.ai/blog/sdk-authentication-api-key-oauth-token-management
category: "Learn Agentic AI"
tags: ["Authentication", "OAuth", "API Keys", "SDK Design", "Security", "Agentic AI"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-06T01:02:44.624Z
---

# 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:

```mermaid
flowchart LR
    INPUT(["User intent"])
    PARSE["Parse plus
classify"]
    PLAN["Plan and tool
selection"]
    AGENT["Agent loop
LLM plus tools"]
    GUARD{"Guardrails
and policy"}
    EXEC["Execute and
verify result"]
    OBS[("Trace and metrics")]
    OUT(["Outcome plus
next action"])
    INPUT --> PARSE --> PLAN --> AGENT --> GUARD
    GUARD -->|Pass| EXEC --> OUT
    GUARD -->|Fail| AGENT
    AGENT --> OBS
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```python
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:

```python
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:

```typescript
interface AuthProvider {
  getHeaders(): Promise>;
}

class APIKeyAuth implements AuthProvider {
  constructor(private readonly apiKey: string) {}

  async getHeaders(): Promise> {
    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> {
    if (!this.token || Date.now() >= this.token.expiresAt) {
      await this.refreshToken();
    }
    return { Authorization: `Bearer ${this.token!.accessToken}` };
  }

  private async refreshToken(): Promise {
    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:

```python
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:

```python
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

---

Source: https://callsphere.ai/blog/sdk-authentication-api-key-oauth-token-management
