hanzo-iam

Hanzo IAM is the server-side identity and access management service for the Hanzo ecosystem.

<!-- Updated: 2026-07-23 -->

Hanzo IAM - Identity and Access Management Server

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.

Overview

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.

Clean server vs retired fork

Why Hanzo IAM?

Tech Stack

When to use

For client-side OAuth flows (login UI, token extraction, redirect handling), see hanzo/hanzo-id.md instead.

Hard requirements

  1. PostgreSQL (or MySQL/SQLite) database
  2. Redis/Valkey for session storage (optional -- falls back to file sessions)
  3. init_data.json for bootstrap organizations, applications, and admin user
  4. TLS termination via Caddy, ingress, or reverse proxy for production

Quick reference

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

Domains served

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

One-file quickstart

Docker (quickest)

docker run -d --name hanzo-iam -p 8000:8000 ghcr.io/hanzoai/iam:latest
# Open http://localhost:8000 for admin UI

Docker Compose (full stack)

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

From source

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

Admin API examples

# 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
 }'

Core Concepts

Architecture

┌──────────────────────────────────────────────────────────────┐
│ 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 │
 └─────────────┘ └─────────────┘ └─────────────┘

Directory Structure

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

Filter Chain (request lifecycle)

The main.go registers Beego filters in this order:

  1. SecureCookieFilter (BeforeStatic) -- enforce secure cookies behind TLS
  2. StaticFilter (BeforeRouter) -- serve React SPA static files
  3. AutoSigninFilter (BeforeRouter) -- auto-sign-in from cookies
  4. CorsFilter (BeforeRouter) -- CORS headers
  5. TimeoutFilter (BeforeRouter) -- request timeout enforcement
  6. ApiFilter (BeforeRouter) -- API auth and rate limiting
  7. PrometheusFilter (BeforeRouter) -- metrics collection
  8. RecordMessage (BeforeRouter) -- audit logging
  9. FieldValidationFilter (BeforeRouter) -- input validation

Redis Auto-Discovery

The 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

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.

Billing Integration

IAM tracks user credit balances. The flow is:

  1. Commerce processes payments and calls IAM API to add credits
  2. Cloud consumes AI tokens and debits balance via IAM transactions
  3. IAM is the source of truth for user credit balances

MCP Server

The built-in MCP server (in mcp/ directory) exposes IAM operations as MCP tools for AI agents:

Supported Identity Providers

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 |

Configuration

Beego INI config (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}

Environment Variables

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

MySQL vs PostgreSQL

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

Makefile Commands

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

API Endpoints

OAuth2/OIDC — canonical surface (HIP-0111, host-relative to the issuer)

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.

Admin REST API (/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 Endpoints

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

Metrics

Prometheus metrics exposed at /api/metrics.

SDK Integration — ONE package: @hanzo/iam

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

TypeScript — browser (PKCE)

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, ... }

TypeScript — server-side token validation

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

Production Deployment

K8s (hanzo-k8s cluster)

IAM runs on the hanzo-k8s cluster at 24.199.76.156:

CI/CD

Health Check

curl -f http://localhost:8000/healthz

Troubleshooting

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

Multi-tenant deployment example

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

Service app (client_credentials):

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

Related Skills


Last Updated: 2026-05-12

Recent Changes (2026-03-28)

Category: Hanzo Ecosystem Related: iam, iam, oauth2, oidc, saml, ldap, scim, webauthn, identity, authentication, authorization, sso Prerequisites: Go, Docker, PostgreSQL, OAuth2/OIDC concepts