Your AI agent needs to log into a customer's dashboard at 3 a.m. You cannot ask a human to click Allow. The agent has no browser. It cannot complete an OAuth redirect.
So you pick one of these:
- Store an immortal API key in the worker (and hope it never leaks)
- Run headless Chrome with Playwright (and fix it every time the login UI changes)
- Use Device Authorization Grant (and wait for someone to scan a QR code)
These are duct tape. They do not give you AI agent identity — a signed, auditable proof of which agent logged in, bound to a specific site session, with a separate lane for MCP OAuth on external tool servers.
Zero Human Auth means the human is not in the login path. It does not mean "no authentication."
How developers solve this today (and why it breaks)
Pattern A: static API key
# agent_worker.py — what many teams ship first
import os
import httpx
API_KEY = os.environ["CUSTOMER_DASHBOARD_API_KEY"] # rot? lol
async def fetch_orders():
async with httpx.AsyncClient() as client:
r = await client.get(
"https://customer.example/api/orders",
headers={"Authorization": f"Bearer {API_KEY}"},
)
r.raise_for_status()
return r.json()
Works until: key rotation, per-customer scoping, audit ("which agent did this?"), or a compromised worker exfiltrating god-mode access.
Pattern B: Playwright + OAuth
# brittle_login.py — do not ship this to prod
from playwright.async_api import async_playwright
async def oauth_as_robot(username: str, password: str) -> str:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto("https://customer.example/oauth/authorize?...")
await page.fill("#email", username)
await page.fill("#password", password)
await page.click("button[type=submit]")
# breaks on: MFA, CAPTCHA, CSS rename, A/B test, ToS
await page.wait_for_url("**/callback*")
# ... scrape cookies or localStorage ...
High maintenance. Often against the customer's terms. Impossible to reason about in CI.
Pattern C: after — one LIME call
import asyncio
import os
from lime_agents import LimeAgent
request_id = "lr_from_your_queue" # from site.create_login_request()
async def approve_login() -> None:
async with LimeAgent(agent_token=os.environ["LIME_AGENT_TOKEN"]) as agent:
result = await agent.login(request_id)
print(result.status) # APPROVED after successful approve (site receives JWT separately via SSE)
asyncio.run(approve_login())
The site backend receives the cryptographic passport over SSE and verifies it with JWKS. The agent never holds the user's session JWT.
Why classic OAuth breaks for headless agents
OAuth 2.0 was designed around human consent in a browser:
- Redirect user to IdP
- User authenticates
- User approves scopes
- Authorization code returns to the client
Headless agents fail at steps 1 and 3. Workarounds hurt:
| Workaround | Why it fails at scale |
|---|---|
| Device Authorization Grant | Still needs a human on another device |
| Client Credentials only | No per-login binding to a user session on your site |
| Headless browser automation | Breaks on MFA, CAPTCHA, UI changes; hard to audit |
| Static API keys | No short-lived, signed proof tied to a login request |
For MCP (Model Context Protocol) tool servers, the problem repeats: agents must call external MCP resource servers with standard Authorization: Bearer semantics — but you still should not mint long-lived secrets inside every agent process.
You need two lanes:
- Site login — agent proves identity; your backend receives a verifiable session artifact
- MCP OAuth — short-lived JWT for external tool servers, separate from site session
LIME implements both with cryptographic passports (RS256 JWTs) and official Python SDKs.
The fix: cryptographic passport + one agent call
Instead of redirecting a browser, LIME runs a headless login protocol:
Site backend LIME Core Agent worker
| | |
|-- create login request ->| |
|<- request_id ------------| |
| | |
|--- request_id ------------------------------------->|
| |<- PoW solve + approve ---|
| | (X-Agent-Token) |
|<- SSE: passport JWT -----| |
| (aud=lime-site-login) | |
|-- verify (JWKS cache) ---| |
Key properties:
-
Cryptographic passport — RS256 JWT signed by LIME Core (
aud=lime-site-login, TTL ~60s); sites verify via JWKS verification (GET /api/v1/core/.well-known/jwks.json) -
Zero Human Auth — agent calls
login(request_id)once; no OAuth redirect, no QR - Proof-of-Work — SHA-256 PoW challenge instead of CAPTCHA (bots pay compute, humans do not)
- Passport goes to the site, not the agent — even if the worker is compromised, it never holds the user's site session JWT
This is not "replace all OAuth everywhere." It is replace browser OAuth for autonomous agents on integrations you control.
Python: secure login for AI agents (site flow)
Install the SDKs:
pip install lime-agents-sdk lime-sites-sdk
Set secrets server-side only (never in prompts, never in frontend JS):
export LIME_SITE_TOKEN="st_..." # site backend
export LIME_AGENT_TOKEN="at_..." # agent worker
Site backend — create request, receive passport over SSE
from contextlib import asynccontextmanager
from fastapi import FastAPI
from lime_sites import InvalidPassportError, LimeSite
site: LimeSite
@asynccontextmanager
async def lifespan(app: FastAPI):
global site
site = LimeSite() # reads LIME_SITE_TOKEN
@site.on_login
async def on_login(request_id: str, passport: str | None) -> None:
if passport is None:
return # expired
try:
verified = await site.verify_passport(
passport,
expected_request_id=request_id,
)
except InvalidPassportError:
return
# Bind verified.claims["agent_id"] to your user session
print("agent logged in:", verified.claims["agent_id"])
yield
await site.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/login/start")
async def start_login() -> dict[str, str]:
req = await site.create_login_request()
return {"request_id": req.request_id}
verify_passport() checks signature (JWKS), aud == "lime-site-login", expiry (platform TTL ≤120s), optional request_id binding, and exposes claims such as agent_id, user_id, agent_reputation.
Agent worker — one call, PoW included
import asyncio
import os
from lime_agents import ApiError, LimeAgent, PowTimeoutError
async def main() -> None:
request_id = "lr_from_your_queue" # from site / job orchestrator
async with LimeAgent(agent_token=os.environ["LIME_AGENT_TOKEN"]) as agent:
try:
result = await agent.login(request_id)
print(result.status) # APPROVED after successful approve (site receives JWT separately via SSE)
print(result.approved_agent_id)
except PowTimeoutError:
print("PoW timeout — retry or increase pow_timeout")
except ApiError as exc:
print(f"{exc.code}: {exc.message}")
asyncio.run(main())
login() fetches the PoW challenge, solves it in a thread pool, and approves with X-Agent-Token. The cryptographic passport is delivered to your site via SSE — not returned to the agent.
MCP OAuth: tools without mixing credentials
Agents also call external MCP resource servers. That uses a different JWT (aud=mcp, ~5 minute TTL). Do not reuse the site passport for MCP.
import asyncio
import os
from lime_agents import CallToolResult, LimeAgent, Tool
MCP_ENDPOINT = "https://mcp.example.com/mcp" # streamable HTTP path
async def main() -> None:
async with LimeAgent(agent_token=os.environ["LIME_AGENT_TOKEN"]) as agent:
tools: list[Tool] = await agent.list_tools(MCP_ENDPOINT)
if not tools:
raise RuntimeError(f"No tools at {MCP_ENDPOINT}")
result: CallToolResult = await agent.call_tool(
MCP_ENDPOINT,
tools[0].name,
{"query": "status"},
)
if result.isError:
print("tool error:", result.content)
else:
print(result.content)
asyncio.run(main())
Credential lanes (swapping them breaks security):
| Lane | Header | Used for |
|---|---|---|
| LIME platform | X-Agent-Token |
login(), get_mcp_access_token()
|
| External MCP RS | Authorization: Bearer <mcp_jwt> |
list_tools, call_tool
|
On the MCP server side, verify Bearer tokens with lime-mcp-server-sdk — JWKS + RS256, cached keys, async-friendly.
Try it now. If this matches your problem: install takes about two minutes.
pip install lime-agents-sdk lime-sites-sdkDocs: https://lime.pics/docs
How LIME compares to alternatives
| Solution | Complexity | Agent-native? | MCP OAuth? | Price |
|---|---|---|---|---|
| Roll-your-own API key | Low | No | No | Free |
| Playwright + OAuth | High | No | No | Free (until it isn't) |
| Agent Passport (protocol + IETF draft) | Very High (delegation chains, 7 constraints) | Yes | Yes | Freemium (OSS + enterprise) |
| Auth0 for AI Agents | High | Yes | Yes | Freemium (free tier + paid plans) |
| LIME | Low | Yes | Yes | Free for identity |
LIME is free for AI agent identity and headless site login today. The business model is commission on future agent payments — similar to Stripe taking a cut on transactions, not charging you to issue passports.
Agent Passport is a full protocol (IETF draft) with delegation chains and seven normative constraints — powerful, but heavy to implement. Auth0 offers a freemium path with paid plans at scale. LIME optimizes for a smaller surface: one Python call on the agent, SSE + JWKS verification on the site, and MCP OAuth JWTs for external resource servers out of the box.
Security notes (read this before production)
What this model gets right
- Short-lived, signed artifacts instead of immortal API keys in agent memory
- Sites verify JWTs against cached Core JWKS — no LIME API round-trip on every request after keys are warm
- PoW rate-limits brute approval spam without UX friction for legitimate agents
- Separation of site passport vs MCP token limits blast radius
What you must still do
- Store
LIME_AGENT_TOKENandLIME_SITE_TOKENas secrets (Vault, K8s secrets, CI vars) — never in client-side code or LLM context - Treat
request_idas single-use and bind it inverify_passport(expected_request_id=...) - Run one
LimeSiteper process with a perpetual SSE connection; do not create per HTTP request - Rotate agent tokens from the LIME owner portal if a worker is compromised
- PoW is abuse resistance, not a malware detector — pair with your own agent allowlists and rate limits
Zero Human Auth removes the human from the authorization flow — it does not remove your responsibility for security. LIME gives you the tools (JWT, JWKS, PoW). Managing secrets, rotating tokens, and setting rate limits is still on you.
When to use LIME vs rolling your own
Build it yourself if you only need a single static API key between two services you own.
Reach for an AI agent identity layer when:
- Multiple agents must log into customer sites with auditable identity
- You need JWKS verification and standard JWT claims (
agent_id,request_id,agent_reputation,user_kyc_level) - You participate in the MCP ecosystem and want MCP OAuth without hand-rolling PyJWT + metadata fetch per request
Stop fighting OAuth. Ship the passport.
pip install lime-agents-sdk lime-sites-sdk
PyPI: lime-agents-sdk · lime-sites-sdk · lime-mcp-server-sdk
GitHub: lime-agents-sdk · lime-site-sdk · lime-mcp-server-sdk
Docs: https://lime.pics/docs
Give your agents a cryptographic passport. Verify it with JWKS. Keep MCP on its own OAuth lane.
Questions? Open an issue on GitHub or ping @MawyxxMC on X.
Tags for Dev.to: python, ai, security, jwt, mcp, oauth
Top comments (0)