mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
* feat(web3): restore canonical @hanzo/iam SPA PKCE auth (HIP-0111)
The brand-neutral refactor regressed login to a hand-rolled OAuth authorize
URL (no PKCE, wrong path `${iamUrl}/oauth/authorize`) plus a backend
code-exchange POST to /v1/auth/oauth/callback — a route api-go does not
expose. Both break IAM login: the hand-rolled URL hits a path IAM serves
its 200 SPA catch-all for (silent breakage), and the callback 404s.
Restore the one canonical way per HIP-0111:
- lib/iam.ts: @hanzo/iam/browser IAM client, white-labelled by the active
brand (serverUrl + clientId), PKCE S256, endpoints resolved via OIDC
discovery. No hand-rolled authorize URL, no genericOAuth, no /api/auth.
- login: getIam().signinRedirect() in production (SDK owns PKCE + state).
- callback: getIam().handleCallback() does the PKCE token exchange
client-side; the IAM access token becomes the session bearer.
- brand default: bootnode IAM is id.bootno.de / bootnode-platform (was
hanzo.id / bootnode-web — wrong issuer + unregistered client).
- package.json: add @hanzo/iam ^0.11.0 (used but missing from deps).
- public/config.example.json: in-repo reference for the per-brand catalog
(registered clientId per host) so the deploy provisions real clients.
Build + typecheck green.
* feat(api): validate IAM tokens via JWKS in production (HIP-0111)
The api-go backend only had an HS256 verifier (shared JWT_SECRET). After the
FE completes IAM PKCE it presents an IAM-issued RS256 access token, which the
HMAC verifier rejects (SigningMethodHMAC check) — IAM login fails at the API.
Converge on the canonical IAM validation path:
- iam/jwks.go: JWKSVerifier validates RS256/ES256 tokens against the issuer's
JWKS, resolved via OIDC discovery ({issuer}/.well-known/openid-configuration
-> jwks_uri). Keys cached, refreshed on unknown kid (rate-limited). stdlib
only — no new dependency. Issuer is checked (WithIssuer).
- main.go: production selects JWKSVerifier(IAM_URL); dev keeps HS256 for the
built-in email/password flow. One way per environment.
- config: ValidateSecrets() fails fast if production boots with placeholder/
empty JWT_SECRET or API_KEY_SALT — secrets MUST come from KMS, never the
in-code default. IsProduction() gates it.
- iam.go: drop redundant exp/iat int64 fields from Claims — duplicate json
tags shadowed jwt.RegisteredClaims and silently disabled expiry validation
(latent bug in the HS path too; now exp is enforced).
Tests: JWKS valid/expired/wrong-issuer/unknown-kid/HS-rejected + config
secret guard. go build + go vet + go test all green.
* harden(api): enforce JWT audience when IAM_CLIENT_ID is set
Adversarial review follow-up: without an aud check, a token minted for ANY
client of the same IAM issuer would be accepted. NewJWKS now takes the
expected audience (the deployment's IAM_CLIENT_ID); when set, tokens whose
aud does not match are rejected (token minted for a different client is no
longer accepted). Empty audience preserves issuer+signature-only behavior.
Test: matching aud passes, mismatched aud rejected.
* fix(web): sync lockfile for @hanzo/iam + remove invalid pnpm-workspace.yaml
test-web failed at `pnpm install --frozen-lockfile` for two reasons:
1. web/pnpm-workspace.yaml carried only `allowBuilds: {sharp: true}` — not a
valid pnpm 9 field, and a workspace file without a `packages:` key makes
pnpm 9 abort with ERR_PNPM_INVALID_WORKSPACE_CONFIGURATION (pre-existing
on main). web/ is a single package, not a workspace, so the file is removed
and sharp's build allowance moved to package.json `pnpm.onlyBuiltDependencies`.
2. This branch added `@hanzo/iam@^0.11.0` to package.json but never regenerated
pnpm-lock.yaml, so --frozen-lockfile rejected the out-of-date lock.
Regenerated the lockfile (pnpm 9.15.9) — it now pins @hanzo/[email protected].0. Verified
locally: `pnpm install --frozen-lockfile` is clean and `pnpm run build`
(next build) compiles all routes incl. the refactored /login + /auth/callback.
---------
Co-authored-by: zeekay <[email protected]>
30 lines
1.6 KiB
JSON
30 lines
1.6 KiB
JSON
{
|
|
"_comment": "REFERENCE multi-brand catalog (HIP-0111). The deploy (universe / CF-Pages) provisions config.json (and/or window.__BRAND_CATALOG__) from a catalog of this shape, host-keyed. iam.url is the canonical OIDC issuer (serverUrl); iam.clientId is the REGISTERED Casdoor app for that host (<org>-web3 / bootnode-platform / pars-app) whose redirectUris include https://<host>/auth/callback. These are PUBLIC values (no secrets) — the @hanzo/iam SPA PKCE flow is a public client. Do NOT invent client IDs: an unregistered clientId returns 'Invalid client_id' and IAM serves its 200 SPA catch-all, which is silent breakage. Brand identity (logos/colors/links) is elided here; only the iam + host wiring is shown as the source of truth.",
|
|
"defaultKey": "bootnode",
|
|
"hosts": [
|
|
{ "match": "web3.hanzo.ai", "key": "hanzo" },
|
|
{ "match": "cloud.lux.network", "key": "lux" },
|
|
{ "match": "web3.lux.network", "key": "lux" },
|
|
{ "match": "web3.zoo.ngo", "key": "zoo" },
|
|
{ "match": "cloud.pars.network", "key": "pars" },
|
|
{ "match": "bootno.de", "key": "bootnode" }
|
|
],
|
|
"brands": {
|
|
"hanzo": {
|
|
"iam": { "url": "https://iam.hanzo.ai", "clientId": "hanzo-web3", "domain": "iam.hanzo.ai" }
|
|
},
|
|
"lux": {
|
|
"iam": { "url": "https://lux.id", "clientId": "lux-web3", "domain": "lux.id" }
|
|
},
|
|
"zoo": {
|
|
"iam": { "url": "https://zoo.id", "clientId": "zoo-web3", "domain": "zoo.id" }
|
|
},
|
|
"pars": {
|
|
"iam": { "url": "https://pars.id", "clientId": "pars-app", "domain": "pars.id" }
|
|
},
|
|
"bootnode": {
|
|
"iam": { "url": "https://id.bootno.de", "clientId": "bootnode-platform", "domain": "id.bootno.de" }
|
|
}
|
|
}
|
|
}
|