Setting up an IAM-for-agents workstation

Stand up Keycloak, Vault, and OpenFGA locally and verify your first OAuth flow end to end.

🧰 Module 0 6 min read Not started

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

  1. Run a local OAuth 2.1 / OIDC IdP (Keycloak).
  2. Run a local secrets backend (HashiCorp Vault dev mode).
  3. Run a local policy engine (OPA + OpenFGA).
  4. Configure CLIs (kcadm.sh, vault, opa, step, mkcert).
  5. Create dev accounts on at least one hosted IdP (Auth0 / WorkOS / Clerk).
  6. Sanity-check with a hello-world OAuth flow.

1. Base tooling

bash
# 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.

bash
mkcert -install
mkcert localhost 127.0.0.1 ::1

2. 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.

yaml
# 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:ro
bash
docker compose -f docker-compose.idp.yml up -d
open https://localhost:8443

Log in as admin / admin. Create:

  • A realm called agents-dev.
  • A client agent-app with:
    • Client type: OpenID Connect.
    • Capability: Standard flow + Service accounts + Device flow + CIBA.
    • Valid Redirect URIs: http://localhost:5173/*.
    • Web origins: +.
  • A user alice with credentials.

Discover the well-known endpoint:

bash
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)

bash
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_xxxx

We'll use Vault to broker per-tool credentials in lesson 5.


4. OPA + OpenFGA (policy engines)

OPA — policy as code

bash
docker run --rm -d --name opa \
  -p 8181:8181 \
  openpolicyagent/opa:latest run --server --log-level=info

OPA 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)

bash
docker run --rm -d --name openfga \
  -p 8080:8080 -p 8081:8081 -p 3000:3000 \
  openfga/openfga:latest run

OpenFGA 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:

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:

python
# 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()
bash
uv run python hello_mcp.py &
mcp dev ./hello_mcp.py    # inspector UI

7. Hello OAuth flow (sanity check)

python
# 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()
bash
uv run python hello_oauth.py

Sign 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 ps shows keycloak, vault, opa, openfga running.
  • curl -k <issuer>/.well-known/openid-configuration returns JSON.
  • You logged in to a hosted IdP dev tenant and copied creds.
  • hello_oauth.py printed tokens you could decode.
  • mcp dev ./hello_mcp.py opened the inspector.
  • vault kv get tools/github shows your dev secret.

If any fail, fix before lesson 1.


9. Common pitfalls

  1. Mixing http redirect URIs and https IdPs → CORS / cookie weirdness.
  2. Forgetting code_challenge_method=S256 → PKCE silently turns into plain.
  3. Leaving Vault in dev mode in CI → state vanishes per restart (use file backend for persistence).
  4. Using Keycloak's master realm for apps → mixing admin + tenant data. Always make a new realm.
  5. 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.