DEV Community

Mawyxx
Mawyxx

Posted on

Zero Human Auth for AI Agents: How to Replace OAuth with Cryptographic Passport

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()
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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:

  1. Redirect user to IdP
  2. User authenticates
  3. User approves scopes
  4. 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:

  1. Site login — agent proves identity; your backend receives a verifiable session artifact
  2. 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) ---|                          |
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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-sdkJWKS + 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-sdk

Docs: 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_TOKEN and LIME_SITE_TOKEN as secrets (Vault, K8s secrets, CI vars) — never in client-side code or LLM context
  • Treat request_id as single-use and bind it in verify_passport(expected_request_id=...)
  • Run one LimeSite per 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
Enter fullscreen mode Exit fullscreen mode

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)