๐Ÿค– AI Tools
ยท 4 min read

AI Agent Authentication: OAuth, API Keys, and Scoped Permissions (2026)


When an AI agent calls an API on behalf of a user, it needs credentials. The question is: whose credentials, with what scope, and how do you prevent the agent from doing more than it should?

Most teams solve this by hardcoding their own API keys into the agent. This is a security disaster โ€” the agent has the same access as the developer, with no audit trail and no way to revoke access per-agent.

Hereโ€™s how to do it properly.

The credential hierarchy

User credentials (full access)
  โ””โ”€โ”€ Agent credentials (scoped access)
       โ””โ”€โ”€ Tool credentials (minimal access)
            โ””โ”€โ”€ Per-action tokens (single-use)

Each layer should have less access than the one above. An agent should never have more permissions than the user who created it.

Pattern 1: Scoped API keys

Create dedicated API keys for each agent with minimal permissions:

# Bad: agent uses your personal API key
GITHUB_TOKEN = os.environ["MY_GITHUB_TOKEN"]  # Full repo access

# Good: agent uses a scoped token
AGENT_GITHUB_TOKEN = os.environ["AGENT_GITHUB_TOKEN"]  # Read-only, specific repos

For GitHub, create a fine-grained personal access token:

  • Repository access: only the repos the agent needs
  • Permissions: contents: read, pull_requests: write (no admin, no delete)
  • Expiration: 30-90 days (force rotation)

For OpenAI/Anthropic, use project-scoped API keys with spending limits.

Pattern 2: OAuth on behalf of users

When your agent acts on behalf of different users, each user should authenticate separately:

from authlib.integrations.httpx_client import AsyncOAuth2Client

async def get_user_github_client(user_id: str):
    # Retrieve user's OAuth token from secure storage
    token = await get_stored_token(user_id, "github")
    
    if token.is_expired():
        token = await refresh_token(token)
        await store_token(user_id, "github", token)
    
    return AsyncOAuth2Client(token=token)

# Agent uses the user's scoped token
async def create_pr(user_id: str, repo: str, branch: str, title: str):
    client = await get_user_github_client(user_id)
    response = await client.post(
        f"https://api.github.com/repos/{repo}/pulls",
        json={"title": title, "head": branch, "base": "main"},
    )
    return response.json()

The Zapier Agent SDK handles this automatically โ€” it manages OAuth for 9,000+ apps so you donโ€™t have to build token refresh logic.

Pattern 3: Least-privilege tool definitions

Define exactly what each tool can do:

from agents import Agent, function_tool

@function_tool
def read_file(path: str) -> str:
    """Read a file from the project directory. Cannot access files outside /workspace."""
    safe_path = os.path.realpath(path)
    if not safe_path.startswith("/workspace"):
        raise PermissionError("Access denied: outside workspace")
    return open(safe_path).read()

@function_tool
def write_file(path: str, content: str) -> str:
    """Write a file. Cannot write to system directories or config files."""
    safe_path = os.path.realpath(path)
    blocked = ["/etc", "/usr", "/bin", ".env", ".git/config"]
    if any(safe_path.startswith(b) or safe_path.endswith(b) for b in blocked):
        raise PermissionError(f"Cannot write to {path}")
    with open(safe_path, "w") as f:
        f.write(content)
    return f"Written {len(content)} bytes to {path}"

# Agent only gets the tools it needs
review_agent = Agent(
    name="Reviewer",
    tools=[read_file],  # Read-only โ€” no write access
)

coding_agent = Agent(
    name="Coder",
    tools=[read_file, write_file],  # Read + write
)

In Gemini CLI subagents, you can restrict tools per-agent in the agent definition file. This is the cleanest implementation of least-privilege for agents.

Credential storage

Never store credentials in:

  • Environment variables on shared systems
  • Agent prompts or system instructions
  • Git repositories
  • Log files

Use a secrets manager:

# AWS Secrets Manager
import boto3

def get_secret(name: str) -> str:
    client = boto3.client("secretsmanager")
    response = client.get_secret_value(SecretId=name)
    return response["SecretString"]

# Or use a .env file with restricted permissions (development only)
# chmod 600 .env

For production, use your cloud providerโ€™s secrets manager or HashiCorp Vault. For development, a .env file with chmod 600 is acceptable.

Audit trails

Log every authenticated action the agent takes:

async def audited_tool_call(user_id, agent_name, tool_name, args, result):
    await db.execute("""
        INSERT INTO agent_audit_log 
        (user_id, agent_name, tool_name, args, result, timestamp)
        VALUES ($1, $2, $3, $4, $5, NOW())
    """, user_id, agent_name, tool_name, json.dumps(args), str(result)[:1000])

This audit log answers: โ€œWhat did the agent do, on whose behalf, and when?โ€ Essential for debugging and compliance.

Token rotation

Automate credential rotation:

async def rotate_agent_tokens():
    """Run weekly via cron or Claude Code Routine."""
    agents = await db.fetch("SELECT * FROM agent_credentials WHERE expires_at < NOW() + INTERVAL '7 days'")
    
    for agent in agents:
        new_token = await create_scoped_token(
            service=agent["service"],
            scopes=agent["scopes"],
            expires_in_days=90,
        )
        await db.execute(
            "UPDATE agent_credentials SET token = $1, expires_at = $2 WHERE id = $3",
            new_token, datetime.utcnow() + timedelta(days=90), agent["id"]
        )

Set up a Claude Code Routine to run this weekly.

Multi-tenant agent security

When your agent serves multiple users, isolate credentials completely:

ConcernSolution
User A sees User Bโ€™s dataSeparate OAuth tokens per user
Agent accesses wrong userโ€™s toolsValidate user_id on every tool call
Credential leakEncrypt at rest, rotate regularly
Over-privileged agentScoped tokens, least-privilege tools
No audit trailLog every tool call with user context

The practical minimum

If youโ€™re just getting started, do at least these three things:

  1. Use scoped API keys โ€” never give an agent your personal full-access token
  2. Validate tool inputs โ€” check paths, URLs, and arguments before execution
  3. Log everything โ€” youโ€™ll need the audit trail when something goes wrong

Everything else (OAuth flows, secrets managers, token rotation) can come later as you scale.

Related: AI Agent Security ยท MCP Security Checklist ยท AI Agent Error Handling ยท Zapier Agent SDK Guide ยท How to Sandbox Local AI Models ยท Gemini CLI Subagents ยท Claude Code Routines