Identity & access for the Hanzo cloud — OpenID Connect / OAuth2 with PKCE, standards only.
Hanzo IAM is the identity service behind every Hanzo sign-in: OpenID Connect
discovery, the authorize + token endpoints (authorization code + PKCE, refresh,
client_credentials, RFC 8693 token exchange), UserInfo, JWKS, SCIM 2.0
provisioning, MFA / WebAuthn, service accounts, and social federation
(Google, GitHub).
It is a clean-room, native rewrite on the Hanzo stack — zip over
hanzoai/orm, no legacy surface, no Beego, no xorm. The identity binary owns its
source outright and collapses to one way of doing each thing. The retired
legacy Beego fork lives at
hanzoai/iam-v1, private and archived, and is out of every graph.
Clients never hand-roll OAuth. They authenticate through the @hanzo/iam
SDK against the endpoints below — one way, no legacy paths (HIP-0111).
Requires Go 1.27+ and openssl. Signing key material is deliberately never stored in SQLite (schema.Cert.PrivateKey is json:"-"), so local dev mounts one RSA PEM in IAM_SIGNING_KEYS matching the seeded cert name:
git clone https://github.com/hanzoai/iam && cd iam
# 1. Generate a local dev signing key and minimal seed config (gitignored under .dev/)
mkdir -p .dev/keys
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out .dev/keys/cert-dev
cat > .dev/init_data.json <<'EOF'
{
"organizations": [{"owner":"admin","name":"hanzo"}],
"certs": [{"owner":"admin","name":"cert-dev","cryptoAlgorithm":"RS256"}],
"applications": [{"owner":"admin","name":"hanzo-dev","clientId":"hanzo-dev","organization":"hanzo","enablePassword":true}]
}
EOF
# 2. Boot local OIDC + JWKS on 127.0.0.1:8000 (SQLite in data/iam.db)
IAM_DEV_HOST_RELATIVE=1 IAM_SIGNING_KEYS=.dev/keys \
go run . serve \
--http http://127.0.0.1:8000 \
--zap 127.0.0.1:19653 \
--init-data .dev/init_data.jsonVerify from a second terminal:
curl -sS http://127.0.0.1:8000/.well-known/openid-configuration
curl -sS http://127.0.0.1:8000/v1/iam/.well-known/jwkshanzoai/cloud defaults CLOUD_IAM_ISSUER=http://127.0.0.1:8000 (with ZAP on :9653, which is why the snippet above binds IAM's ZAP port to 19653 when running both on one machine). See the full Self-Hosting & Air-Gapped Deployment Guide.
| Concern | Component | Notes |
|---|---|---|
| HTTP | hanzoai/zip |
Typed zip.Get[In,Out] handlers on the Fiber v3 engine; specificity routing; OpenAPI 3.1 at the edge |
| Storage | hanzoai/orm |
Typed Go records + KV cache over one orm.DB abstraction. Embedded SQLite by default (hanzoai/sqlite, pure-Go, WAL) — never Postgres |
| OIDC / OAuth2 | in-tree | RS256 today; ML-DSA-65 hybrid JWT + real JWKS from the Cert entity. No external OIDC library |
| Password verify | internal/cred |
Algorithm resolved from the stored row — argon2id + bcrypt, verify-only, fail-closed |
| Authorization | hanzoai/authz |
One canonical policy engine, called over ZAP RPC |
| Inter-service | hanzoai/zap |
Binary RPC service↔service. HTTPS is the external edge only |
The HTTP contract is RFC/OpenID-standard only. There are no legacy verb aliases
(get-users, add-user, issue-user-token, …) and no /api/ prefix — /v1/
throughout. Paths are relative to the brand serverUrl.
| Capability | Standard | Endpoint |
|---|---|---|
| Discovery / AS metadata | RFC 8414 · OIDC Discovery | /.well-known/openid-configuration |
| JWKS | RFC 7517 | /v1/iam/.well-known/jwks |
| Authorize | RFC 6749 (code + PKCE S256) |
/v1/iam/oauth/authorize |
| Token | RFC 6749 (code, refresh, client_credentials, password) |
/v1/iam/oauth/token |
| Token exchange / on-behalf-of | RFC 8693 | /v1/iam/oauth/token (grant_type=…token-exchange) |
| Introspection / revocation | RFC 7662 / RFC 7009 | /v1/iam/oauth/{introspect,revoke} |
| UserInfo (account claims) | OIDC UserInfo | /v1/iam/oauth/userinfo |
| Logout | OIDC RP-initiated logout | /v1/iam/oauth/logout |
| Identity provisioning | SCIM 2.0 (RFC 7644 / 7643) | /v1/iam/scim/v2/Users |
| Social sign-in / federation | OIDC/OAuth2 Relying Party | /v1/iam/oauth/authorize?provider=<name> → /v1/iam/oauth/callback |
| Wallet sign-in / linking | CAIP-122 · EIP-4361 | /v1/iam/web3/nonce → /v1/iam/web3/verify |
| Disconnect a sign-in method | — | /v1/iam/unlink |
A wallet linked here rides on every token as wallets —
[{"chain":"evm","address":"0x…"}], the addresses whose CAIP-122 proof IAM
verified — beside did, the account's did:lux:<subject> identifier derived
from sub. UserInfo answers with both, read fresh. Neither is proof of PRESENT
control: the assertion is as fresh as the token, so anything authorizing a
payment takes its own signature. LLM.md has the derivation, the unlink rules,
and the optional on-chain anchor.
PKCE S256 always; client_secret_basic; scopes openid profile email.
client_id is <org>-<app> (globally unique); redirectUris must be the
framework's exact callback.
Brands (set serverUrl): hanzo → iam.hanzo.ai · lux → lux.id ·
zoo → zoo.id · bootnode → id.bootno.de · pars → pars.id. Shared infra
white-labels by domain — never the Hanzo mark on a Lux or Zoo surface.
Every handler and the drift tool are written once against orm.DB, never a
driver. Pick the backend at boot with --store:
sqlite(default) — embedded, pure-Go, WAL. No Postgres.sql—hanzoai/sqlover ZAP.datastore—hanzoai/datastoreover ZAP (ZAP-native persistence + snapshots, zero code change).
go build ./...
# Seed config + serve OIDC / login (SQLite by default; requires $IAM_SIGNING_KEYS and either $IAM_ISSUER or IAM_DEV_HOST_RELATIVE=1)
IAM_ISSUER=https://iam.example.com IAM_SIGNING_KEYS=/etc/iam/keys go run . serve --init-data /etc/iam/init_data.json
# ZAP-native persistence instead of embedded SQLite
IAM_ISSUER=https://iam.example.com IAM_SIGNING_KEYS=/etc/iam/keys go run . serve --store datastore --init-data /etc/iam/init_data.json
# Read-only v1 → v2 drift report (needs a `-tags migration` build)
go run . compare --legacy postgres://…/iam
go run . versionserve flags: --store (sqlite|sql|datastore), --db (SQLite path),
--zap (ZAP listen), --http (HTTP edge), --init-data (new-only seed;
${VAR} expands from env). Deploy env: IAM_ISSUER=https://<brand-id>;
the on-behalf-of (token-exchange) allow-lists IAM_TOKEN_EXCHANGE_APPS and
IAM_ADMIN_TOKEN_EXCHANGE_APPS, matched by the globally-unique client_id;
IAM_IMPERSONATION_APPS, the sites a SuperAdmin may open signed in as a person
(POST /v1/iam/impersonate; LLM.md "Impersonation"), by client_id, unset = none;
and the credential-administration capability IAM_KEY_MINT_ALLOWED_APPS,
matched by application name (distinct from token exchange, so exchanging tokens
does not grant the capability).
The service is embeddable via server.Route and builds on Hanzo CI
(ghcr.io/hanzoai/iam).
Authenticate only through @hanzo/iam against the canonical OIDC endpoints —
no hand-rolled OAuth, no genericOAuth({discoveryUrl}), no per-app path strings,
no legacy paths. SDK subpaths cover every runtime: @hanzo/iam/server
(validateToken / getServerSession), @hanzo/iam/betterauth,
@hanzo/iam/nextauth, @hanzo/iam/react + @hanzo/iam/browser (SPA PKCE),
@hanzo/iam/passport. Keep originFrontend empty in prod so discovery is
host-relative.
OIDC/OAuth2 core is live and tested end to end (login → PKCE code → token → JWT):
discovery + JWKS, credential login (argon2id / bcrypt), the token endpoint,
introspection / revocation, UserInfo, RFC 8693 token exchange, SCIM 2.0
provisioning, and Google / GitHub federation. Current release line: v1.33.x.
See LLM.md for architecture details and the full RFC surface.
Dual-licensed under MIT or Apache-2.0 at your option, as the OSS core tier of HIP-0130.
Open source · every language · on-chain settlement. hanzo.ai · docs.hanzo.ai · Self-Hosting Guide
SDKs in every language — Python (flagship) · TypeScript · Go · Rust · C++ · Swift · Kotlin · umbrella