Hanzo

Hanzo IAM

Identity and Access Management for the Hanzo AI Platform

Hanzo IAM

API reference · Hanzo IAM API → — every endpoint, generated from the OpenAPI spec.

Hanzo IAM is the identity provider for the Hanzo platform, served at hanzo.id. It is a standards-compliant OpenID Connect (OIDC) and OAuth 2.0 authorization server that issues the JWTs every other Hanzo service trusts. It supports password, social OAuth2, Web3 wallet, WebAuthn/passkey, LDAP, SAML, device-code, and MFA authentication.

Issuerhttps://hanzo.id
API prefix/v1/iam
Discoveryhttps://hanzo.id/.well-known/openid-configuration
JWKShttps://hanzo.id/v1/iam/.well-known/jwks
Sourcegithub.com/hanzoai/iam

Every Hanzo IAM endpoint lives under the single canonical prefix /v1/iam. The standard discovery path /.well-known/openid-configuration is served as a back-compatible alias so off-the-shelf OIDC clients work out of the box.

Features

  • OAuth2 / OIDC: Authorization Code (+ PKCE), Client Credentials, Refresh Token, Device Code, and Token Exchange grants
  • Per-host white-label: one deployment serves many domains, each with its own issuer, branding, and JWKS
  • Web3 Authentication: MetaMask, WalletConnect, Coinbase, Rainbow
  • WebAuthn / Passkeys: FIDO2 passwordless authentication with hardware keys or biometrics
  • LDAP & SAML: Directory integration and SAML 2.0 SP/IdP
  • MFA: TOTP, WebAuthn, SMS, and email verification
  • Post-quantum tokens: JWT signing with MLDSA65 (ML-DSA-65, FIPS 204) in addition to RS256/ES256
  • Multi-tenancy: Organizations, applications, and user isolation with RBAC

OIDC Endpoints

All endpoints are under https://hanzo.id/v1/iam.

EndpointPath
DiscoveryGET /.well-known/openid-configuration (alias) · GET /v1/iam/.well-known/openid-configuration
JWKSGET /v1/iam/.well-known/jwks
AuthorizeGET /v1/iam/oauth/authorize
TokenPOST /v1/iam/oauth/token
UserinfoGET /v1/iam/oauth/userinfo
Logout / end-sessionGET,POST /v1/iam/oauth/logout
Introspect (RFC 7662)POST /v1/iam/oauth/introspect
Revoke (RFC 7009)POST /v1/iam/oauth/revoke
Device codePOST /v1/iam/oauth/device
Dynamic client registrationPOST /v1/iam/oauth/register

Applications: the <org>-<app> convention

Every consumer of Hanzo IAM registers an application, and the application's name follows one rule:

app slug = <org>-<app>

For example: hanzo-brain, hanzo-cloud, hanzo-chat, acme-verify. The organization that owns the data goes in the organization field; the app name is namespaced with the org so it stays globally unique. Your application's client_id and the aud (audience) claim of issued tokens both default to this <org>-<app> slug.

Register an application

curl -X POST https://hanzo.id/v1/iam/add-application \
  -H "Authorization: Bearer $IAM_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "owner": "admin",
    "name": "hanzo-brain",
    "displayName": "Hanzo Brain",
    "organization": "hanzo",
    "cert": "cert-built-in",
    "tokenFormat": "JWT",
    "grantTypes": ["authorization_code", "refresh_token"],
    "redirectUris": ["https://brain.hanzo.ai/callback"]
  }'

clientId and clientSecret are generated automatically if omitted. Store the returned clientSecret in Hanzo KMS — never in source or plaintext config.

For idempotent, declarative provisioning (CI/CD, operators), use POST /v1/iam/admin/applications/upsert with an Authorization: Bootstrap <token> header instead of a session/admin JWT.

Authorization Code + PKCE Flow

This is the recommended flow for web apps, SPAs, and mobile clients.

Discover

curl https://hanzo.id/.well-known/openid-configuration

The issuer and every endpoint come back scoped to the requesting host (hanzo.id).

Redirect to authorize

https://hanzo.id/v1/iam/oauth/authorize
  ?response_type=code
  &client_id=hanzo-brain
  &redirect_uri=https://brain.hanzo.ai/callback
  &scope=openid%20profile%20email
  &state=RANDOM_STATE
  &code_challenge=<S256(verifier)>
  &code_challenge_method=S256

The user authenticates on the branded login page and is redirected back with ?code=...&state=....

Exchange the code for tokens

curl -X POST https://hanzo.id/v1/iam/oauth/token \
  -d grant_type=authorization_code \
  -d code=$AUTH_CODE \
  -d redirect_uri=https://brain.hanzo.ai/callback \
  -d client_id=hanzo-brain \
  -d client_secret=$CLIENT_SECRET \
  -d code_verifier=$PKCE_VERIFIER

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Get user info

curl https://hanzo.id/v1/iam/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Validating tokens in a resource server

Any Hanzo service validates a hanzo.id token offline: fetch the JWKS once, then verify each request locally.

  1. Fetch the signing keys from https://hanzo.id/v1/iam/.well-known/jwks (cache by kid).
  2. Verify the JWT signature (RS256 by default).
  3. Require iss == https://hanzo.id.
  4. Require aud == <org>-<app> (your own client_id).
  5. Read sub (the user ID) and owner (the user's organization) to scope data.
import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://hanzo.id/v1/iam/.well-known/jwks'))

export async function verify(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://hanzo.id',
    audience: 'hanzo-brain', // your <org>-<app> client_id
  })
  return { userId: payload.sub, org: payload.owner as string }
}

The Hanzo Gateway performs this validation at the edge and injects trusted X-Org-Id, X-User-Id, and X-User-Email headers (stripping any client-supplied identity headers) for upstream services.

White-labeling by domain

Hanzo IAM is multi-tenant white-label by design. A single deployment serves many domains:

  • Per-host issuer — the issuer, authorization endpoint, and JWKS are derived per request from X-Forwarded-Host (set by Hanzo Ingress). Each domain returns its own discovery document.
  • Per-app branding — logo, title, favicon, theme color, and custom CSS are configured on each organization and application, and resolved on the login page by client_id (or by org in the path, /login/{org}).

This is how hanzo.id presents Hanzo branding while the same engine can serve entirely separate, independently-branded identity surfaces for other tenants.

Authentication Methods

MethodNotes
Passwordargon2id hashing
OAuth2 socialGitHub, Google, Microsoft, Apple, Discord, X
Web3 walletsMetaMask, WalletConnect, Coinbase, Rainbow
WebAuthn / PasskeysFIDO2 hardware keys and biometrics
LDAPPer-organization directory integration
SAML 2.0Service provider and identity provider
Device codeCLIs and input-constrained devices
MFATOTP, WebAuthn, SMS, email

SDK

import { signIn } from '@hanzo/iam/browser'

await signIn({
  iamUrl: 'https://hanzo.id',
  clientId: 'hanzo-brain', // your <org>-<app> slug
  redirectUri: 'https://brain.hanzo.ai/callback',
})

Next Steps

The canonical IAM_* environment variable contract for consumer services

How IAM stores and reads its own secrets from Hanzo KMS

Multi-party computation authentication for secure key recovery

Secret management for client secrets and signing keys

How is this guide?

Last updated on

On this page