For an agent to use your API autonomously it needs two things: (1) what endpoints exist and what they want, (2) how to authenticate. This guide pairs RFC 8414 OAuth Authorization Server Metadata with an OpenAPI 3.1 well-known catalogue so an agent can consume your API without human handholding. AIDE's proto-openapi-catalog and proto-oauth-discovery checks verify the wiring.
What AIDE actually checks
proto-openapi-catalog — three steps:
- Does
https://<domain>/.well-known/openapireturn HTTP 200? - Does the body parse as a valid OpenAPI 3.1 document?
- Does at least one operation include
x-ai-tagsor equivalent AI metadata?
proto-oauth-discovery — two steps:
- Does
https://<domain>/.well-known/oauth-authorization-serverreturn HTTP 200? - Does the body include the RFC 8414 mandatory fields (
issuer,authorization_endpoint,token_endpoint,response_types_supported)?
When both PASS, an agent dispatcher can discover your API and complete OAuth automatically.
Step 1 — OpenAPI 3.1 document
If you use FastAPI or Flask-OpenAPI the spec is already auto-generated. Map it onto the well-known path:
# FastAPI example — apps/api/src/aide_api/main.py
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI(
title="AIDE Scan API",
version="1.0.0",
description="REST API for AI-readiness scans",
)
@app.get("/.well-known/openapi", include_in_schema=False)
async def well_known_openapi():
"""Discovery-friendly OpenAPI document."""
schema = app.openapi()
# Enrich for AI agents
schema["info"]["x-ai-friendly"] = True
schema["info"]["x-mcp-server"] = "https://mcp.aide.tr/v1/rpc"
# Tag every operation (example)
for path, methods in schema["paths"].items():
for method, operation in methods.items():
if isinstance(operation, dict):
operation.setdefault("x-ai-tags", _infer_ai_tags(path, operation))
return JSONResponse(
schema,
headers={
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
)
def _infer_ai_tags(path: str, op: dict) -> list[str]:
"""Suggest AI tags from path — dispatchers use this as a filter."""
tags: list[str] = []
if "/scans" in path:
tags.append("audit")
if "/leaderboard" in path:
tags.append("benchmark")
if op.get("method", "").upper() == "GET":
tags.append("read-only")
return tags
x-ai-tags is non-standard but increasingly common. AIDE accepts variants such as x-ai-friendly, x-llm-tags, x-agent-skills.
Step 2 — OAuth Authorization Server Metadata
RFC 8414 defines a standard well-known endpoint:
// /.well-known/oauth-authorization-server
{
"issuer": "https://aide.tr",
"authorization_endpoint": "https://aide.tr/oauth/authorize",
"token_endpoint": "https://aide.tr/oauth/token",
"registration_endpoint": "https://aide.tr/oauth/register",
"scopes_supported": ["scan:create", "scan:read", "leaderboard:read"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
"pkce_required": true,
"device_authorization_endpoint": "https://aide.tr/oauth/device",
"x-ai-dynamic-registration": true
}
Critical fields:
| Field | Why |
|---|---|
| registration_endpoint | Dynamic Client Registration (RFC 7591) — agents register themselves without human ops |
| code_challenge_methods_supported: ["S256"] | PKCE is mandatory — safe for public clients |
| device_authorization_endpoint | Device flow (RFC 8628) — for headless agents |
| pkce_required: true | PKCE on every flow — combined with none auth this is ideal for agents |
| x-ai-dynamic-registration | AIDE flips PASS — signals automatic client onboarding is supported |
Step 3 — Dynamic Client Registration
Agents can't pre-configure a static client_id (every agent is different). RFC 7591 dynamic registration handles this:
from fastapi import APIRouter
from pydantic import BaseModel
import secrets
from datetime import datetime
router = APIRouter()
class ClientRegRequest(BaseModel):
client_name: str
redirect_uris: list[str]
grant_types: list[str] = ["authorization_code", "refresh_token"]
token_endpoint_auth_method: str = "none" # PKCE-friendly public client
scope: str = "scan:read"
@router.post("/oauth/register")
async def register_client(req: ClientRegRequest):
client_id = f"agent_{secrets.token_urlsafe(16)}"
await db.clients.insert({
"client_id": client_id,
"client_name": req.client_name,
"redirect_uris": req.redirect_uris,
"grant_types": req.grant_types,
"auth_method": req.token_endpoint_auth_method,
"scope": req.scope,
"created_at": datetime.utcnow(),
"is_agent": True, # mark for filtering / bulk revocation
})
return {
"client_id": client_id,
"client_id_issued_at": int(datetime.utcnow().timestamp()),
"redirect_uris": req.redirect_uris,
"grant_types": req.grant_types,
"token_endpoint_auth_method": req.token_endpoint_auth_method,
"scope": req.scope,
}
PKCE + public client (token_endpoint_auth_method: "none") is the safest combo for agents — no secret to leak.
Step 4 — PKCE flow
The agent runs the authorization-code flow with PKCE for the requested scope:
import hashlib, base64, secrets
# 1. Code verifier
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
# 2. Authorization redirect — user approves in a browser
auth_url = (
f"https://aide.tr/oauth/authorize?"
f"response_type=code&client_id={client_id}&"
f"redirect_uri={redirect_uri}&"
f"code_challenge={code_challenge}&"
f"code_challenge_method=S256&"
f"scope=scan:create"
)
# 3. After approval, exchange code for token
token_resp = await client.post(
"https://aide.tr/oauth/token",
data={
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": redirect_uri,
"client_id": client_id,
"code_verifier": code_verifier, # PKCE
},
)
access_token = token_resp.json()["access_token"]
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| OpenAPI securitySchemes empty | Agent can't discover auth | Define oauth2 flow + scopes under securitySchemes |
| .well-known/openapi returns 200 but info.title empty | AIDE WARNING | Validate the document on every build (Spectral linter in CI) |
| Dynamic registration returns 401 | Treated as a privileged endpoint | The endpoint must be anonymous; protect with rate limiting |
| redirect_uris whitelist excludes http://localhost | Local-dev agents fail | Allow http://localhost:* pattern for dev/test |
| scopes_supported empty | Agent doesn't know what to ask for | Publish ≥2–3 meaningful scopes (read, write, admin) |
Testing — what AIDE does
# OpenAPI catalog
curl -s https://your-site.com/.well-known/openapi | jq '.openapi, .info.title, (.paths | keys | length)'
# Expect: "3.1.0", "Site API", >0
# OAuth discovery
curl -s https://your-site.com/.well-known/oauth-authorization-server | jq '.issuer, .token_endpoint, .scopes_supported'
# Expect: all populated
# Dynamic registration probe
curl -X POST https://your-site.com/oauth/register \
-H 'Content-Type: application/json' \
-d '{"client_name":"test","redirect_uris":["http://localhost:8080/cb"]}'
# Expect: 200 with a client_id in the body
If all three hold, agent dispatchers can complete register → PKCE → API call without human input.
Production hardening
- Token TTL: access token 1 h, refresh token 30 d. Shorter = more agent traffic; longer = bigger compromise window.
- Audit log: Persist every grant + exchange in
oauth_grants. Mark agent clients (is_agent: true) — useful for bulk revocation. - Rate limit:
/oauth/registeris an abuse vector — 10 registrations/minute is plenty. - Scope inflation: Agents tend to ask for
admin. Enforce least-privilege defaults; escalation is a separate flow.
Related resources
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 7591 — OAuth 2.0 Dynamic Client Registration
- RFC 7636 — PKCE
- OpenAPI 3.1 spec
- AIDE check details:
/learn/proto-openapi-catalog,/learn/proto-oauth-discovery