Hanzo IAM is the server-side identity and access management service for the Hanzo ecosystem.
<!-- Updated: 2026-07-23 -->
Category: Hanzo Ecosystem Related Skills: hanzo/hanzo-id.md, hanzo/hanzo-platform.md, hanzo/hanzo-kms.md, hanzo/hanzo-cloud.md Spec: HIP-0111 (auth) + HIP-0112 (cloud topology). Ground truth: ~/work/hanzo/CLAUDE.md.
Hanzo IAM is the server-side identity and access management service for the Hanzo ecosystem. Written in Go (Beego framework) with a React admin UI, providing OAuth 2.0/OIDC/SAML/CAS/LDAP/SCIM/WebAuthn/TOTP/RADIUS authentication and authorization. Serves SSO across hanzo.id, lux.id, zoolabs.id, and pars.id. Includes built-in MCP (Model Context Protocol) server for AI agent identity management.
hanzoai/iam — the clean rewrite and the ONLY server. It is embedded in hanzoai/cloud and serves the canonical OIDC surface under **/v1/iam/*** (see endpoints below). This is what production runs.hanzoai/iam-v1 — the retired predecessor. Do not build, deploy, or reference it for new work. The current server deliberately continues the version line to supersede it (MVS).@hanzo/iam SDK against /v1/iam/oauth/*. See hanzo/hanzo-id.md.hanzo-kv or redis in K8s)idp/ directoryghcr.io/hanzoai/iam:latestgithub.com/hanzoai/iam — original work, Apache-2.0 (retired predecessor: github.com/hanzoai/iam-v1)For client-side OAuth flows (login UI, token extraction, redirect handling), see hanzo/hanzo-id.md instead.
| Item | Value | |------|-------| | Admin UI | https://iam.hanzo.ai (internal) | | Login UI | https://hanzo.id (separate repo: hanzoai/id) | | API port | 8000 | | LDAP port | 389 (LDAPS: 636) | | RADIUS port | 1812 | | Image | ghcr.io/hanzoai/iam:latest | | Go module | github.com/hanzoai/iam | | Go version | 1.26 | | Frontend | React 18 + Ant Design 5 (pnpm) | | Config | conf/app.conf (Beego INI format) | | Init data | init_data.json | | Repo | github.com/hanzoai/iam | | License | Apache-2.0 |
Brands are selected host-based (one deployment, many origins). The brand host is the OIDC issuer; iam.hanzo.ai is the internal cluster/admin host and is never the issuer.
| Domain | Purpose | |--------|---------| | hanzo.id | Hanzo AI accounts (issuer) | | lux.id | Lux Network accounts (issuer) | | zoolabs.id | Zoo Labs accounts (issuer) — NOT zoo.id | | pars.id | Pars accounts (issuer) | | id.bootno.de | Bootnode accounts (issuer) | | id.ad.nexus | Ad Nexus accounts (issuer) | | iam.hanzo.ai | Internal backend/admin host (NOT an issuer) |
docker run -d --name hanzo-iam -p 8000:8000 ghcr.io/hanzoai/iam:latest
# Open http://localhost:8000 for admin UI
# compose.yml
services:
iam:
image: ghcr.io/hanzoai/iam:latest
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://hanzo:hanzo@postgres:5432/iam?sslmode=disable
- REDIS_URL=redis://redis:6379
- IAM_ORIGIN=https://hanzo.id
- ENABLE_MULTI_TENANT=true
- ALLOWED_ORIGINS=hanzo.id,zoolabs.id,lux.id,pars.id,iam.hanzo.ai
volumes:
- ./init_data.json:/app/init_data.json:ro
depends_on:
postgres:
condition: service_healthy
postgres:
image: ghcr.io/hanzoai/sql:latest
environment:
POSTGRES_USER: hanzo
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: iam
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hanzo -d iam"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: ghcr.io/hanzoai/kv:latest
command: kv-server --appendonly yes
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
git clone https://github.com/hanzoai/iam.git
cd iam
# Backend
go build -o server .
# Frontend
cd web && pnpm install && pnpm run build && cd ..
# Configure
cp conf/app.dev.conf conf/app.conf
# Edit conf/app.conf with your database credentials
# Run
./server
# Get all users in an organization
curl -s "https://iam.hanzo.ai/v1/iam/get-users?owner=hanzo" \
-H "Authorization: Bearer ${ADMIN_TOKEN}"
# Create a new application
curl -X POST "https://iam.hanzo.ai/v1/iam/add-application" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"owner": "admin",
"name": "app-myservice",
"organization": "hanzo",
"clientId": "my-client-id",
"clientSecret": "my-client-secret",
"redirectUris": ["https://myservice.hanzo.ai/callback"],
"expireInHours": 168,
"grantTypes": ["authorization_code", "refresh_token"]
}'
# Update user balance (billing integration)
curl -X POST "https://iam.hanzo.ai/v1/iam/update-user" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"owner": "hanzo",
"name": "username",
"balance": 10000
}'
┌──────────────────────────────────────────────────────────────┐
│ Hanzo IAM Server (Go) │
├──────────────┬──────────────┬──────────────┬─────────────────┤
│ OAuth2/OIDC │ SAML/CAS │ LDAP/SCIM │ WebAuthn/MFA │
├──────────────┴──────────────┴──────────────┴─────────────────┤
│ Beego Router → Controllers → Object (business logic) │
├──────────────┬──────────────┬──────────────┬─────────────────┤
│ 40+ IdPs │ Casbin RBAC │ MCP Server │ WAF (Coraza) │
├──────────────┴──────────────┴──────────────┴─────────────────┤
│ PostgreSQL/MySQL/SQLite │ Redis/Valkey │ File Storage │
└───────────────────────────┴────────────────┴─────────────────┘
↑ ↑ ↑
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ hanzo.id │ │ cloud. │ │ platform. │
│ (login UI) │ │ hanzo.ai │ │ hanzo.ai │
└─────────────┘ └─────────────┘ └─────────────┘
iam/
├── main.go # Entry point (Beego bootstrap, filter chain)
├── conf/ # Configuration files (INI format)
│ ├── app.conf # Active config (gitignored in prod)
│ ├── app.dev.conf # Docker dev (PostgreSQL)
│ ├── app.prod.conf # Production (hanzo.id)
│ ├── app.staging.conf # Staging (stg.hanzo.id)
│ └── waf.conf # Coraza WAF rules
├── controllers/ # HTTP handlers (Beego controllers)
│ ├── auth.go # OAuth2 authorization flows
│ ├── token.go # Token issuance and validation
│ ├── account.go # User account management
│ ├── user.go # User CRUD
│ ├── application.go # Application management
│ ├── organization.go # Organization management
│ ├── permission.go # Permission management
│ ├── role.go # Role management
│ ├── mfa.go # Multi-factor authentication
│ ├── webauthn.go # WebAuthn/Passkeys
│ ├── saml.go # SAML SSO
│ ├── cas.go # CAS protocol
│ ├── ldap.go # LDAP management
│ ├── scim.go # SCIM provisioning
│ ├── webhook.go # Webhook management
│ ├── wellknown_oidc_discovery.go # .well-known endpoints
│ └── wellknown_oauth_prm.go # OAuth metadata
├── object/ # Core business logic (models + ORM)
│ ├── adapter.go # Database adapter
│ ├── ormer.go # xorm ORM setup
│ └── transaction.go # Billing transactions
├── routers/ # Beego filters and route definitions
│ ├── router.go # All API route registrations
│ ├── authz_filter.go # Authorization filter
│ ├── cors_filter.go # CORS handling
│ ├── secure_cookie_filter.go # Secure cookie enforcement
│ ├── static_filter.go # SPA static file serving
│ └── mcp_util.go # MCP protocol utilities
├── idp/ # Identity provider implementations
│ ├── github.go, google.go, facebook.go, ... # 30+ providers
│ ├── metamask.go, web3onboard.go # Web3 providers
│ ├── goth.go # Goth library integration (40+ providers)
│ └── provider.go # Provider interface
├── mcp/ # MCP (Model Context Protocol) server
│ ├── base.go # MCP server setup and tool registry
│ ├── auth.go # MCP auth tools
│ ├── application.go # MCP app management tools
│ └── permission.go # MCP permission tools
├── authz/ # Casbin authorization engine
├── ldap/ # LDAP server implementation
├── radius/ # RADIUS server implementation
├── scim/ # SCIM protocol implementation
├── captcha/ # CAPTCHA generation
├── certificate/ # X.509 certificate management
├── email/ # Email templates and sending
├── notification/ # Push notifications
├── proxy/ # HTTP client proxy
├── storage/ # File storage backends
├── web/ # React admin UI (Ant Design)
│ ├── src/ # React source
│ └── package.json # pnpm, React 18, Ant Design 5
├── init_data.json # Bootstrap data (orgs, apps, users)
├── compose.yml # Production Docker Compose
├── Dockerfile # Multi-stage (frontend + backend + alpine)
├── Makefile # Build, test, deploy commands
└── k8s.yaml # Kubernetes deployment manifest
The main.go registers Beego filters in this order:
SecureCookieFilter (BeforeStatic) -- enforce secure cookies behind TLSStaticFilter (BeforeRouter) -- serve React SPA static filesAutoSigninFilter (BeforeRouter) -- auto-sign-in from cookiesCorsFilter (BeforeRouter) -- CORS headersTimeoutFilter (BeforeRouter) -- request timeout enforcementApiFilter (BeforeRouter) -- API auth and rate limitingPrometheusFilter (BeforeRouter) -- metrics collectionRecordMessage (BeforeRouter) -- audit loggingFieldValidationFilter (BeforeRouter) -- input validationThe server auto-discovers Redis in Kubernetes by trying DNS lookups for hanzo-kv and redis service names. Falls back to file-based sessions if no Redis is available.
init_data.json bootstraps the database on first run:
| Entity | Defaults | |--------|----------| | Organization | hanzo (dark theme, primary color #fd4444) | | Applications | app-hanzo, app-cloud, app-commerce | | Admin user | [email protected] with configurable balance | | Certificates | RSA certs for JWT signing |
Set initDataNewOnly = true in conf to avoid overwriting existing data on restart.
IAM tracks user credit balances. The flow is:
The built-in MCP server (in mcp/ directory) exposes IAM operations as MCP tools for AI agents:
Native implementations in idp/ directory:
| Category | Providers | |----------|-----------| | Code hosting | GitHub, GitLab, Gitee, Bitbucket | | Social | Google, Facebook, Twitter, LinkedIn, Discord, Reddit | | Enterprise | ADFS, Okta, Azure AD B2C, SAML (any) | | Messaging | Telegram, Lark, DingTalk, WeChat, WeCom, Line, Slack | | Web3 | MetaMask, Web3Onboard | | Regional | Alipay, Baidu, Bilibili, Douyin, QQ, Weibo, Kwai | | Via Goth | 40+ additional providers |
conf/app.conf)appname = hanzo-iam
httpport = 8000
runmode = dev
driverName = postgres
dataSourceName = user=hanzo password=xxx host=postgres port=5432 sslmode=disable dbname=hanzo_iam
dbName = hanzo_iam
redisEndpoint = redis:6379
authState = "hanzo"
origin = https://hanzo.id
staticBaseUrl = "https://cdn.hanzo.ai"
ldapServerPort = 389
ldapsServerPort = 636
radiusServerPort = 1812
radiusDefaultOrganization = "hanzo"
initDataNewOnly = true
initDataFile = "./init_data.json"
logConfig = {"adapter":"file", "filename": "logs/hanzo-iam.log", "maxdays":99999}
quota = {"organization": -1, "user": -1, "application": -1, "provider": -1}
# Database
POSTGRES_USER=hanzo
POSTGRES_PASSWORD=<generate-secure-password>
POSTGRES_DB=iam
# IAM server
IAM_ORIGIN=https://iam.hanzo.ai
ENCRYPTION_KEY=<32-byte-hex-key>
ENABLE_MULTI_TENANT=true
ALLOWED_ORIGINS=hanzo.id,zoolabs.id,lux.id,pars.id,iam.hanzo.ai
# Per-org client secrets (override init_data.json placeholders)
HANZO_CLIENT_SECRET=<generate-secret>
ZOO_CLIENT_SECRET=<generate-secret>
LUX_CLIENT_SECRET=<generate-secret>
PARS_CLIENT_SECRET=<generate-secret>
# Email (optional)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=<email-password>
PostgreSQL (recommended):
driverName = postgres
dataSourceName = user=hanzo password=xxx host=postgres port=5432 sslmode=disable dbname=hanzo_iam
MySQL (dataSourceName must NOT include database name -- it is appended from dbName):
driverName = mysql
dataSourceName = hanzo:pass@tcp(localhost:3306)/
dbName = hanzo_iam
make dev # Start local dev with Docker Compose (PostgreSQL)
make dev-down # Stop local dev
make run # Run Go server locally (go run)
make backend # Build Go binary to bin/manager
make frontend # Build React admin UI (pnpm)
make ut # Run Go unit tests with coverage
make docker-build # Build Docker image
make docker-push # Push to ghcr.io/hanzoai/iam
make deploy # Helm deploy to K8s
make staging # Start staging compose
make prod # Start production compose
make build-prod # Build and push production image
These are the ONLY client-facing paths. There is no legacy /oauth/ and no /api/login/. The @hanzo/iam SDK (src/paths.ts OIDC_PATHS) is the single source of truth; clients import it, never hardcode a path.
| Endpoint | Path | Method | |----------|------|--------| | Authorize | /v1/iam/oauth/authorize | GET | | Token | /v1/iam/oauth/token | POST | | Introspect | /v1/iam/oauth/introspect | POST | | Revoke | /v1/iam/oauth/revoke | POST | | UserInfo | /v1/iam/oauth/userinfo | GET | | Device | /v1/iam/oauth/device | POST | | Logout | /v1/iam/oauth/logout | GET | | OIDC Discovery | /.well-known/openid-configuration | GET | | JWKS | /v1/iam/.well-known/jwks | GET |
PKCE S256 always; client_secret_basic; scopes openid profile email; client_id = <org>-<app>. GOTCHA: any unregistered path returns a 200 text/html SPA catch-all (silent breakage, not a 404) — clients MUST hit these exact paths and keep originFrontend EMPTY so discovery stays host-relative.
/v1/iam/)| Resource | Endpoints | |----------|-----------| | Users | get-users, get-user, add-user, update-user, delete-user | | Organizations | get-organizations, get-organization, add-organization, update-organization | | Applications | get-applications, get-application, add-application, update-application | | Providers | get-providers, get-provider, add-provider, update-provider | | Roles | get-roles, get-role, add-role, update-role, delete-role | | Permissions | get-permissions, get-permission, add-permission, update-permission | | Tokens | get-tokens, get-token, delete-token | | Sessions | get-sessions, delete-session | | Records | get-records (audit log) | | Webhooks | get-webhooks, add-webhook, update-webhook | | Account | get-account, signup, login, logout | | Verification | send-verification-code, verify-code | | MFA | mfa-setup-initiate, mfa-setup-verify, mfa-setup-enable |
| Protocol | Path | |----------|------| | SAML | /api/saml/redirect, /api/saml/metadata | | CAS | /cas/login, /cas/logout, /cas/serviceValidate, /cas/proxyValidate | | LDAP | Port 389 (LDAPS 636) | | RADIUS | Port 1812 | | SCIM | /scim/v2/Users, /scim/v2/Groups | | Registry token | /v2/token (Docker registry auth) |
Prometheus metrics exposed at /api/metrics.
@hanzo/iamClients authenticate with the @hanzo/iam SDK (npm, 0.13.7) against the canonical /v1/iam/oauth/* endpoints. The retired @hanzo/iam-js-sdk and any hand-rolled OAuth are forbidden. Full client contract: hanzo/hanzo-id.md.
import { configureIam, startLogin, handleCallback, getUser } from "@hanzo/iam/browser"
configureIam({
serverUrl: "https://hanzo.id", // brand host / issuer
clientId: "hanzo-app", // <org>-<app>
redirectUri: "https://app.hanzo.ai/auth/callback",
})
await startLogin() // PKCE S256 handled by the SDK
await handleCallback() // on the callback route (run once)
const user = await getUser() // { sub, email, owner, ... }
import { validateToken } from "@hanzo/iam/server"
const claims = await validateToken(bearer, { serverUrl: "https://hanzo.id" })
const orgID = claims.owner // scope all queries to the org
Go/other services validate the JWT against JWKS (/v1/iam/.well-known/jwks) — in practice the gateway (api.hanzo.ai) does this and injects X-Org-Id/X-User-Id/X-User-Email for downstream services (and strips any client-supplied identity headers first).
IAM runs on the hanzo-k8s cluster at 24.199.76.156:
hanzoiampostgres.hanzo.svc (db: iam)kms.hanzo.ai)ghcr.io/hanzoai/iamcurl -f http://localhost:8000/healthz
| Issue | Cause | Solution | |-------|-------|----------| | "Access denied to database hanzo_iamhanzo_iam" | MySQL dataSourceName includes dbname | End with / not /dbname | | "could not open pg_filenode.map" | PostgreSQL volume corruption | docker compose down -v && docker volume prune | | Tokens expire instantly | expireInHours=0 on app | Set expireInHours=168 in the IAM app config | | Client gets a 200 HTML page instead of tokens | Wrong path hit the SPA catch-all | Use canonical /v1/iam/oauth/*; keep originFrontend empty; use the @hanzo/iam SDK (never a hardcoded path or genericOAuth({discoveryUrl})) | | "Missing PKCE code verifier" at callback | Callback effect ran twice / single-use code | Run handleCallback once (see hanzo-id.md); upgrade @hanzo/iam to 0.13.8+ | | initData overwrites on restart | initDataNewOnly=false | Set initDataNewOnly=true in conf | | App owner queries fail | Apps under owner=admin not org | WHERE owner='admin' not org name | | Redis not found | No Redis endpoint configured | Auto-discovers hanzo-kv or redis DNS; set redisEndpoint explicitly | | LDAP not starting | Port 389 in use | Check ldapServerPort in conf | | WAF blocking requests | Coraza rules too strict | Adjust conf/waf.conf |
A canonical multi-tenant deployment (e.g., a regulated-tokenization tenant + a biometric-identity tenant in the same IAM instance) uses:
Organisations: <tenant1> (canonical) + <tenant2> (peer).
SPA apps (authorization_code):
<tenant2>-admin — operator dashboard at <tenant2>.{env}.<domain><tenant2>-verify — standalone IDV flow at verify.{env}.<domain>Service app (client_credentials):
<tenant2>-daemon with aud=<tenant2>-daemon,<tenant1>-kms (needs both: the daemon scope for inbound calls + <tenant1>-kms to read secrets)
Internal docs sites are typically gated by NextAuth v5 + Google OAuth restricted to a corporate email domain, separate from IAM (engineering-only surface).
Companion docs live in the tenant's own repo under internal/content/docs/ and papers/.
hanzo/hanzo-id.md - Client-side login UI and OAuth flows (Next.js)hanzo/hanzo-platform.md - PaaS (uses IAM for auth)hanzo/hanzo-kms.md - Secret management (also uses IAM)hanzo/hanzo-cloud.md - Cloud dashboard (uses IAM for auth + billing)hanzo/hanzo-console.md - Admin console (uses IAM for auth)hanzo/hanzo-commerce.md - Commerce (writes user balances to IAM)hanzo/hanzo-universe.md - K8s infrastructure manifestsLast Updated: 2026-05-12
password grant type now works when enabled on the application. Previously the switch statement fell through to a nil token crash.Category: Hanzo Ecosystem Related: iam, iam, oauth2, oidc, saml, ldap, scim, webauthn, identity, authentication, authorization, sso Prerequisites: Go, Docker, PostgreSQL, OAuth2/OIDC concepts