How to Authenticate AI Agents Without OAuth or Shared API Keys
On this page
An autonomous agent starts running tests at midnight. Forty minutes into a refactor, it needs to save a milestone to your project tracker.
The agent stalls. In your terminal, a browser URL appears, waiting for a human to click a consent screen.
Why traditional authentication breaks with AI agents#
Every standard authentication flow in web development assumed one of two things:
- A human with a browser: OAuth redirect flows rely on a person sitting in front of a screen to inspect permissions, click "Allow", and authorize a callback URL. An autonomous agent running in a background container or cron job cannot complete this loop.
- A machine with root credentials: Server-to-server systems use static, long-lived API keys (
sk_live_...). Handing a root API key to an AI agent means pasting secret credentials into prompt contexts, terminal logs, and model provider telemetry.
Neither model fits an agent acting on behalf of a human user across multiple third-party services.
The failure modes in production#
The prompt injection risk. When an agent holds a raw, long-lived API key in its context window, any untrusted content it processes — a GitHub issue, a fetched webpage, an email — can instruct the agent to print or exfiltrate that key. Once leaked, a static key grants persistent access until someone manually revokes it.
The OAuth interruption tax. Building an OAuth flow into an agent workflow forces human intervention into tasks that were supposed to run autonomously. When an agent manages five different tools, the human becomes a full-time approval clerk clicking OAuth consent dialogs.
The blast radius problem. An API key created for a personal tool often grants broad access across an entire organization. If a script needs to read a single issue, handing it an account-wide API key creates an unnecessarily large security exposure.
The scoped token architecture#
The solution separates the long-lived user credential from the short-lived request token:
[ User Key: aon_live_... ] ──> Stays securely in MCP transport
│
▼
[ Agent calls get_token("notes.com") ]
│
▼
[ Identity Engine signs RS256 JWT ]
- Audience: notes.com (exact hostname)
- Lifetime: 300 seconds (5 minutes)
- Subject: verified email address
│
▼
[ Target API verifies signature locally against public JWKS ]
Three properties make this safe for both sides:
- Domain binding: The token carries an
audclaim matching the target service's normalized domain. A token minted fornotes.comis rejected bygithub.comwithAUDIENCE_MISMATCH. If an agent is tricked into sending the token to an untrusted server, the token cannot be reused elsewhere. - Ephemeral lifetime: Tokens expire after five minutes (
exp = iat + 300). They are never persisted to disk or stored in database tables. - Local cryptographic verification: The receiving API verifies the RS256 signature locally against public keys published at
/.well-known/jwks.json. Verification takes zero database lookups and zero external network roundtrips on the request path.
How to implement it in your backend#
Using @agentonboard/sdk, your route handler reads the Bearer token, verifies it against public keys, and resolves your own user in three lines:
import { requireAgentAccount } from "@agentonboard/sdk/next";
export async function POST(request: Request) {
const session = await requireAgentAccount(request, {
audience: "notes.com",
resolveAccount: async ({ email }) => {
// Find the user by verified email in your database
return await db.user.findUnique({ where: { email } });
},
});
// Automatically returns 401 (expired/invalid) or 403 (no account)
if (!session.ok) return session.response;
return Response.json({ success: true, created_by: session.user.id });
}
The human user configures their AgentOnboard API key once in their agent runner (like Claude Code or Cursor). The agent requests a 5-minute scoped token only when it reaches your service.
The documentation covers framework bindings for Next.js, Express, Hono, and Python FastAPI.