luxfi/vote's zoo tenant is an SPA client, PKCE in the browser like the others. Co-authored-by: Hanzo Dev <[email protected]>
203 lines
13 KiB
YAML
203 lines
13 KiB
YAML
# Zoo IAM provision document — POLICY / DATA (this is Zoo's app graph).
|
|
#
|
|
# The MECHANISM is brand-neutral and lives in the OSS server (hanzoai/iam,
|
|
# package internal/provision, exposed as `iam2 provision`). It derives
|
|
# client_id (<org>-<app>), redirect URIs and grant types from each app's
|
|
# `type`, then idempotently upserts every client through
|
|
# POST /v1/iam/admin/applications/upsert. One line per app; the OSS ships ZERO
|
|
# brands, Zoo owns its own app data HERE.
|
|
#
|
|
# APPLY IT (deterministic, re-runnable — this is the ONLY way Zoo's IAM state
|
|
# is changed; never hand-edit applications in a console):
|
|
#
|
|
# make iam-provision # converge zoolabs.id
|
|
# make iam-provision-plan # derive + print, write nothing
|
|
#
|
|
# Re-running is a no-op by construction: the upsert keys on <org>-<app> and a
|
|
# request that omits clientSecret PRESERVES the secret IAM already issued, so
|
|
# convergence never rotates a live credential. Secrets stay in KMS
|
|
# (kms.zoo.network, path zoo/iam/clients/<org>-<app>) — never in this file.
|
|
#
|
|
# app `type` → derived shape (authoritative table: internal/provision/provision.go):
|
|
# spa public browser client; https://<host>/auth/callback per host
|
|
# confidential server-side web app; same redirects + an IAM-issued client_secret
|
|
# cli loopback http://127.0.0.1/callback + http://localhost/callback
|
|
# desktop custom-scheme deep link <deepLinkScheme>://oauth/<app>
|
|
# service machine-to-machine, client_credentials only, no redirects
|
|
#
|
|
# /auth/callback is the ONE browser callback path, verified against what the
|
|
# deployed Zoo apps actually send (zoo.cloud, wallet.zoo.network and zoo.vote
|
|
# all build redirect_uri = `${window.location.origin}/auth/callback`). It is NOT
|
|
# /api/... — the /v1-only rule holds, and the browser callback is unversioned.
|
|
#
|
|
# client_id is ALWAYS <org>-<app>. HIP-0111.
|
|
#
|
|
# `cert` is NOT cosmetic and NOT optional. issueTokens resolves app.Cert to
|
|
# sign, so a client created without one cannot mint an id_token, and the login
|
|
# fails AFTER a successful code exchange — the "cannot get user information
|
|
# without id_token" class. The create path copies req.Cert verbatim
|
|
# (internal/bootstrap/bootstrap.go: `a.GrantTypes, a.RedirectUris, a.Cert =
|
|
# req.GrantTypes, req.RedirectUris, req.Cert`), so an omitted cert means an
|
|
# EMPTY cert on every newly created client — which is exactly what the first
|
|
# converge of this document produced. Measured on the live IdP:
|
|
# zoo-cloud cert=cert-zoo (created 2026-07-25, the one Zoo login that works)
|
|
# zoo-wallet / zoo-dao / zoo-explore / zoo-id cert=<EMPTY> (created 2026-07-26)
|
|
# The convention is cert-<org>, confirmed against the two sibling consoles:
|
|
# lux-cloud=cert-lux, hanzo-cloud=cert-hanzo. cert-zoo is published in the live
|
|
# JWKS (https://zoolabs.id/v1/iam/.well-known/jwks), so tokens signed under it
|
|
# verify today. Every Zoo app therefore names cert-zoo; for zoo-cloud that is a
|
|
# literal no-op (it already holds cert-zoo), which is what makes stating it
|
|
# uniformly safe rather than a gamble on the working login.
|
|
#
|
|
# ─── ISSUER: Zoo's canonical issuer is https://zoolabs.id ────────────────────
|
|
# The host id.zoo.network serves issuer "https://zoolabs.id". That is a real
|
|
# disagreement, and it is resolved in FAVOUR of zoolabs.id, not against it:
|
|
#
|
|
# 1. IAM already stamps it. IAM_ISSUER_MAP on deploy hanzo/iam maps all three
|
|
# of zoolabs.id, www.zoolabs.id AND id.zoo.network to https://zoolabs.id,
|
|
# so every token Zoo has ever issued carries iss=https://zoolabs.id.
|
|
# Re-pointing the issuer invalidates every live session and every relying
|
|
# party's cached config; re-pointing an ALIAS costs nothing.
|
|
# 2. Discovery already agrees at both hosts. issuer, authorization_endpoint,
|
|
# token_endpoint and jwks_uri all resolve to zoolabs.id whichever host you
|
|
# fetch /.well-known/openid-configuration from. There is one issuer
|
|
# identity; id.zoo.network is a second NAME for it, not a second issuer.
|
|
# 3. Fleet convention. Every sibling brand's issuer is its <brand>.id apex —
|
|
# hanzo.id, lux.id, pars.id, osage.id — with id.<brand>.network as the
|
|
# alias. id.lux.network serves issuer https://lux.id: the identical shape.
|
|
# Zoo's <brand>.id apex is zoolabs.id (Zoo Labs Foundation is the legal
|
|
# entity). `zoo.id` does NOT resolve, so it cannot be the issuer — and any
|
|
# Zoo surface still pointing at https://zoo.id is simply broken.
|
|
# 4. White-label rule holds: zoolabs.id is Zoo-owned. No Lux/Hanzo leak.
|
|
#
|
|
# The DEFECT that remains is the DISCOVERY DOCUMENT, not the issuer choice:
|
|
# https://id.zoo.network/.well-known/openid-configuration returns
|
|
# issuer=https://zoolabs.id, and OIDC Discovery 1.0 §4.3 requires the issuer
|
|
# value to be identical to the URL prefix the document was fetched from. Any
|
|
# strict client (oidc-client-ts, MSAL, go-oidc, Spring Security, Keycloak
|
|
# adapters) rejects that document. zoo.cloud only survives because its
|
|
# hand-rolled SDK never validates iss. The fix belongs to the FRONTENDS and the
|
|
# ingress, not to this file: point every Zoo client's iamDomain/authority at
|
|
# https://zoolabs.id so discovery host == issuer, and reduce id.zoo.network to a
|
|
# 301. Registration data below is unaffected either way — a client's redirect
|
|
# URIs are hosts of the APP, never of the issuer.
|
|
#
|
|
# ─── WHAT IS DECLARED, AND ON WHAT EVIDENCE ──────────────────────────────────
|
|
# Only surfaces measured to drive an OIDC authorization_code flow are declared.
|
|
# Every Zoo host was probed; the ones that carry ZERO OAuth wiring are listed at
|
|
# the bottom so the omission is a recorded decision, not an oversight.
|
|
|
|
orgs:
|
|
- name: zoo
|
|
displayName: Zoo
|
|
homepage: https://zoo.ngo
|
|
deepLinkScheme: zoo
|
|
apps:
|
|
# zoo.cloud is the LIVE console and the only Zoo client that predates this
|
|
# document — the live records show createdAt 2026-07-25 for zoo-cloud and
|
|
# 2026-07-26 (this document's first converge) for all four below.
|
|
# Observed in the shipped bundle: iamClientId=`zoo-cloud`,
|
|
# redirectUri=`${window.location.origin}/auth/callback`, and the
|
|
# code->token exchange posts code_verifier — i.e. PKCE IN THE BROWSER.
|
|
# Hence `spa`, NOT confidential: a browser cannot hold a secret, and IAM
|
|
# demands client auth whenever one is stored, which is what makes a
|
|
# mis-registered console die on `invalid_client` at the callback.
|
|
# (deploy hanzo/cloud still carries a legacy ZOO_CLOUD_CLIENT_SECRET env;
|
|
# it is vestigial — the live flow is public+PKCE and the audience
|
|
# `zoo-cloud` is already in GATEWAY_ALLOWED_AUDIENCES.)
|
|
#
|
|
# www.zoo.cloud is SERVED by ingress zoo-cloud/cloud but its callback was
|
|
# NOT registered — logging in from the www host failed. Declaring it here
|
|
# fixes that.
|
|
# cloud.zoo.network is NOT declared, and that is a deliberate change from
|
|
# the first converge, which did register it. It has an ingress
|
|
# (hanzo/bootnode-web-ingress) but NO DNS RECORD AT ALL — `dig` returns
|
|
# nothing for A at 1.1.1.1 and 8.8.8.8 — so no browser can ever land
|
|
# there, and a redirect URI for a host that cannot exist is precisely the
|
|
# drift this document exists to prevent. It is also the one host the old
|
|
# text both declared here AND listed as non-resolving at the bottom; the
|
|
# contradiction had to break one way, and it breaks toward the measurement.
|
|
# Contrast Lux, which keeps cloud.lux.network for the same role: that host
|
|
# DOES resolve (172.67.134.56 / 104.21.25.122), so its line is earned.
|
|
# This converge also DROPS http://localhost:3000/auth/callback, which was
|
|
# registered on zoo-cloud: a loopback redirect has no place on a browser
|
|
# SPA (that is what `type: cli` is for), and the converged lux-cloud
|
|
# reference does not carry one either.
|
|
- { app: cloud, type: spa, cert: cert-zoo, hosts: [zoo.cloud, www.zoo.cloud] }
|
|
|
|
# Zoo Wallet. wallet.zoo.network/brand.json declares
|
|
# iamClientId=`zoo-wallet` and the session chunk runs the same PKCE
|
|
# authorize/token pair against /v1/iam/oauth/*. The client did not exist
|
|
# in IAM, so Zoo Wallet login was dead on arrival — this line is the fix.
|
|
# NOTE two further wallet defects that this file CANNOT fix: brand.json
|
|
# sets iamIssuer=https://zoo.id (does not resolve — see ISSUER above) and
|
|
# appDomain=wallet.zoo.ngo (does not resolve). Both are wallet-side.
|
|
- { app: wallet, type: spa, cert: cert-zoo, hosts: [wallet.zoo.network] }
|
|
|
|
# Zoo DAO. zoo.vote ships a HARDCODED Lux config —
|
|
# issuer="https://lux.id", client_id:"lux-dao" — on a Zoo domain: a
|
|
# white-label violation, and dead anyway (`lux-dao` is registered
|
|
# nowhere). `zoo-dao` is the correct client for zoo.vote/www.zoo.vote
|
|
# (both served by ingress hanzo/zoo-vote). The frontend must be repointed
|
|
# at https://zoolabs.id + zoo-dao; registering the client first is what
|
|
# makes that repoint a one-line change.
|
|
# vote.zoo.ngo serves an OLDER DAO build with no OAuth wiring at all and
|
|
# is deliberately NOT declared.
|
|
- { app: dao, type: spa, cert: cert-zoo, hosts: [zoo.vote, www.zoo.vote] }
|
|
|
|
# Zoo Explorer. The shipped _app chunk builds
|
|
# `${serverUrl}/oauth/authorize?...&redirect_uri=<origin>/auth/callback`
|
|
# behind authProvider:"oidc", but its runtime envs.js carries no
|
|
# NEXT_PUBLIC_OIDC_* keys, so account sign-in is switched off at runtime.
|
|
# Registering the client is the half that belongs in IAM; setting
|
|
# NEXT_PUBLIC_OIDC_SERVER_URL=https://zoolabs.id and
|
|
# NEXT_PUBLIC_OIDC_CLIENT_ID=zoo-explore is the half that belongs to the
|
|
# deployment. Set them to EXACTLY that: the Lux explorer is live proof of
|
|
# the failure mode, shipping NEXT_PUBLIC_OIDC_CLIENT_ID=
|
|
# "lux-explore-client-id" while IAM holds `lux-explore`, so its sign-in
|
|
# 400s on unknown client_id today.
|
|
- { app: explore, type: spa, cert: cert-zoo, hosts: [explore.zoo.network] }
|
|
|
|
# Zoo ID portal. zoolabs.id / www.zoolabs.id / id.zoo.network all serve
|
|
# the Sign-in SPA (ingresses hanzo/id and hanzo/hanzo-login). Its
|
|
# compiled brand table holds hanzo.id, lux.id, pars.id, osage.id and
|
|
# www.osage.id — and NO Zoo entry, so on a Zoo host it falls back to
|
|
# clientId:"" and orgId:"" and cannot start a login of its own. This is
|
|
# the client that entry must name; adding it to the login app's table is a
|
|
# separate frontend change.
|
|
- { app: id, type: spa, cert: cert-zoo, hosts: [zoolabs.id, www.zoolabs.id, id.zoo.network] }
|
|
- { app: vote, type: spa, hosts: [zoo.vote] } # clientId zoo-vote — luxfi/vote (zoo tenant). spa/PKCE, never confidential
|
|
|
|
# ─── MEASURED AND DELIBERATELY NOT DECLARED ──────────────────────────────────
|
|
# Reachable Zoo surfaces with ZERO OAuth wiring — every first-party chunk was
|
|
# downloaded and searched for /v1/iam/oauth/authorize, iamClientId and iamIssuer:
|
|
# zoo.exchange, exchange.zoo.network, trade.zoo.exchange (367 chunks, 0 hits;
|
|
# wallet-based auth only)
|
|
# bridge.zoo.network, bridge.zoo.ngo (BRIDGE_CLIENT_ID unset in __ENV.js;
|
|
# bridge.lux.network ships it empty too — bridge is not an IAM surface)
|
|
# status.zoo.network, kms.zoo.network, zoo.ngo, explore.zoo.ngo (0 hits)
|
|
# Zoo hostnames served by SHARED Hanzo backends, not Zoo clients:
|
|
# console.zoo.cloud -> svc cloud (IAM_CLIENT_ID=hanzo-cloud); there is no
|
|
# `zoo-console` audience in GATEWAY_ALLOWED_AUDIENCES, unlike lux-console.
|
|
# Resolves (129.212.164.5), 200.
|
|
# admin.zoo.cloud -> svc console behind middleware admin-guard@file
|
|
# (audiences admin-console / hanzo-admin-guard). It DOES resolve
|
|
# (104.21.47.184) and answers 401 {"authentication required"} — that 401
|
|
# is the guard working, not a dead host. Two ingresses claim it
|
|
# (hanzo/console-admin-guard-zoo-cloud and hanzo/console-admin-zoo-cloud);
|
|
# that duplicate belongs to the console, not to this document.
|
|
# Resolves, but is NOT an IAM surface:
|
|
# docs.zoo.ngo A 185.199.109.153 (GitHub Pages); 200 only with TLS
|
|
# verification off — the served certificate does not name docs.zoo.ngo.
|
|
# build.zoo.network A 134.199.138.27; http 301, https 503. Reachable and
|
|
# broken, which is a deployment defect, not a missing OAuth client.
|
|
# Hosts with NO DNS record at all (checked A at 1.1.1.1 and 8.8.8.8), so
|
|
# nothing can be declared for them:
|
|
# app.zoo.ngo, zoo.id, wallet.zoo.ngo, wallet-api.zoo.ngo,
|
|
# auth.zoo.ngo (has ingress hanzo/iam-redirect-zoo, no DNS),
|
|
# cloud.zoo.network (has ingress hanzo/bootnode-web-ingress, no DNS),
|
|
# api.cloud.zoo.network (has ingress hanzo/bootnode-api-ingress, no DNS),
|
|
# dao.zoo.ngo, id.zoo.ngo, market.zoo.network, billing.zoo.network, zoo.build
|
|
# Third-party parked domains, NOT ours: zoo.market (2.57.91.91),
|
|
# zoo.finance (3.33.130.190 / 15.197.148.33)
|