Setting up an IAM-for-agents workstation
Stand up Keycloak, Vault, and OpenFGA locally and verify your first OAuth flow end to end.
Why this matters
You'll need a working IdP (identity provider), a secrets manager, a policy engine, and a local MCP server to make every later lesson concrete instead of theoretical. One careful afternoon of setup saves dozens of "but how do I actually try this?" moments.
Learning objectives
- Run a local OAuth 2.1 / OIDC IdP (Keycloak).
- Run a local secrets backend (HashiCorp Vault dev mode).
- Run a local policy engine (OPA + OpenFGA).
- Configure CLIs (
kcadm.sh,vault,opa,step,mkcert). - Create dev accounts on at least one hosted IdP (Auth0 / WorkOS / Clerk).
- Sanity-check with a hello-world OAuth flow.
1. Base tooling
# Python 3.12 + uv (see other curricula)
uv venv .venv && source .venv/bin/activate
uv pip install requests authlib httpx python-jose[cryptography] \
pydantic fastapi uvicorn[standard] structlog pyjwt cryptography \
mcp[cli] openai anthropic
# Helpful CLIs
brew install jq mkcert step kubectl helm cosign
# Windows: winget install ...mkcert is invaluable — it gives you locally-trusted TLS certs so you can run Keycloak / MCP servers on https:// without the warnings that hide real bugs.
mkcert -install
mkcert localhost 127.0.0.1 ::12. Keycloak (your local IdP)
Keycloak is a feature-complete OAuth 2.1 / OIDC / SAML IdP. Lets you test every flow without paying or rate-limiting.
# docker-compose.idp.yml
services:
keycloak:
image: quay.io/keycloak/keycloak:26.0
command: start-dev --hostname=localhost --https-port=8443
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HEALTH_ENABLED: "true"
KC_HTTPS_CERTIFICATE_FILE: /certs/localhost.pem
KC_HTTPS_CERTIFICATE_KEY_FILE: /certs/localhost-key.pem
ports: ["8080:8080", "8443:8443"]
volumes:
- ./localhost.pem:/certs/localhost.pem:ro
- ./localhost-key.pem:/certs/localhost-key.pem:rodocker compose -f docker-compose.idp.yml up -d
open https://localhost:8443Log in as admin / admin. Create:
- A realm called
agents-dev. - A client
agent-appwith:- Client type: OpenID Connect.
- Capability: Standard flow + Service accounts + Device flow + CIBA.
- Valid Redirect URIs:
http://localhost:5173/*. - Web origins:
+.
- A user
alicewith credentials.
Discover the well-known endpoint:
curl -k https://localhost:8443/realms/agents-dev/.well-known/openid-configuration | jq .You'll reuse this throughout the curriculum.
3. HashiCorp Vault (secrets backend)
docker run --rm -d --name vault \
-p 8200:8200 \
--cap-add=IPC_LOCK \
-e VAULT_DEV_ROOT_TOKEN_ID=root \
hashicorp/vault:1.18
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=root
vault status
vault secrets enable -path=tools kv-v2
vault kv put tools/github token=ghp_dev_only_xxxxWe'll use Vault to broker per-tool credentials in lesson 5.
4. OPA + OpenFGA (policy engines)
OPA — policy as code
docker run --rm -d --name opa \
-p 8181:8181 \
openpolicyagent/opa:latest run --server --log-level=infoOPA is for policy decisions (allow/deny) against arbitrary JSON input. We'll write Rego policies for tool access in lesson 6.
OpenFGA — relationship-based access control (ReBAC)
docker run --rm -d --name openfga \
-p 8080:8080 -p 8081:8081 -p 3000:3000 \
openfga/openfga:latest runOpenFGA models permissions as relationships ("alice is owner of doc:42"). Excellent for multi-tenant agent products where you need to know "can this agent, acting for this user, read this document?".
5. Hosted IdPs (pick at least one)
Real-world agent apps usually run against a hosted IdP. Sign up for a free dev tenant on one of:
- Auth0 (Okta CIC): mature, generous free tier, good docs.
- WorkOS: B2B-flavoured, simple SSO + Directory Sync, agent-friendly.
- Clerk: developer DX, fast to integrate, good React SDKs.
- Stytch: passwordless + auth for AI agents (they pushed early on agent identity primitives).
- Descope: low-code flows; supports agent flows.
For agent-specific patterns, Auth0 + WorkOS + Stytch are the most explicit about agent identity as of 2026.
For each, create an application and copy the client_id, client_secret, and issuer URL into a .env:
KEYCLOAK_ISSUER=https://localhost:8443/realms/agents-dev
AUTH0_ISSUER=https://your-tenant.us.auth0.com/
AUTH0_CLIENT_ID=...
AUTH0_CLIENT_SECRET=...6. Local MCP server scaffold
We'll write a real MCP server with OAuth in lesson 4. For now, verify the SDK:
# hello_mcp.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run()uv run python hello_mcp.py &
mcp dev ./hello_mcp.py # inspector UI7. Hello OAuth flow (sanity check)
# hello_oauth.py
import os, secrets, hashlib, base64, urllib.parse, webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
ISSUER = "https://localhost:8443/realms/agents-dev"
CLIENT_ID = "agent-app"
REDIRECT = "http://localhost:5173/cb"
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
url = f"{ISSUER}/protocol/openid-connect/auth?" + urllib.parse.urlencode({
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT,
"response_type": "code",
"scope": "openid profile email offline_access",
"code_challenge": challenge,
"code_challenge_method": "S256",
"state": secrets.token_urlsafe(16),
})
webbrowser.open(url)
class CB(BaseHTTPRequestHandler):
def do_GET(self):
q = urllib.parse.urlparse(self.path).query
params = dict(urllib.parse.parse_qsl(q))
token = requests.post(
f"{ISSUER}/protocol/openid-connect/token",
data={
"grant_type": "authorization_code",
"code": params["code"],
"redirect_uri": REDIRECT,
"client_id": CLIENT_ID,
"code_verifier": verifier,
},
verify=False, # mkcert ca is fine in dev
).json()
self.send_response(200); self.end_headers()
self.wfile.write(b"ok, check terminal")
print(token)
HTTPServer(("localhost", 5173), CB).handle_request()uv run python hello_oauth.pySign in as alice. You should see an access token, refresh token, and ID token in the terminal. Paste the access token into jwt.io and inspect the claims.
This proves your IdP, PKCE flow, and tooling all work.
8. Validation checklist
-
docker psshowskeycloak,vault,opa,openfgarunning. -
curl -k <issuer>/.well-known/openid-configurationreturns JSON. - You logged in to a hosted IdP dev tenant and copied creds.
-
hello_oauth.pyprinted tokens you could decode. -
mcp dev ./hello_mcp.pyopened the inspector. -
vault kv get tools/githubshows your dev secret.
If any fail, fix before lesson 1.
9. Common pitfalls
- Mixing
httpredirect URIs andhttpsIdPs → CORS / cookie weirdness. - Forgetting
code_challenge_method=S256→ PKCE silently turns into plain. - Leaving Vault in dev mode in CI → state vanishes per restart (use file backend for persistence).
- Using Keycloak's master realm for apps → mixing admin + tenant data. Always make a new realm.
- Letting Docker rebind ports → tokens have wrong
iss. Pin ports.
10. References
- Keycloak documentation (
www.keycloak.org/documentation). - HashiCorp Vault docs.
- OPA + Rego docs.
- OpenFGA docs (
openfga.dev). - IETF OAuth Working Group (
oauth.net). - MCP Python SDK (
modelcontextprotocol.io). - Auth0 / WorkOS / Stytch developer docs.
Sign in to save your progress and earn badges.