IAM signals MFA with a STRING in the login response `data`
("RequiredMfa" = org forces MFA, user not enrolled; "NextMfa" = user has
MFA, needs a challenge) — never the `mfa_required` boolean the portal was
checking. So every first login fell through to /onboarding, silently
bypassing 2FA.
parseLoginResponse now branches on `data` BEFORE the /onboarding return:
- RequiredMfa -> { mfaRequired, mfaStage: 'enroll' }
- NextMfa -> { mfaRequired, mfaStage: 'challenge', mfaTypes }
New AuthClient methods drive the flow against the canonical /v1/iam surface:
getAccount, mfaInitiate, mfaVerify, mfaEnable, mfaChallenge.
Wire contract (verified live vs iam.hanzo.ai): the /v1/iam/mfa/setup/*
calls put EVERY param on the query string with an EMPTY body — the only
shape that satisfies both IAM's authz self-match (objOwner/objName are
read from the query only when the body is empty; a urlencoded body fails
the JSON-unmarshal and yields "Unauthorized operation") and the MFA
controller. owner/name ride the query on every call, incl. verify.
UI: MfaEnrollForm renders forced TOTP enrollment — QR from the otpauth://
URI via @paulmillr/qr (secret never leaves the browser), manual-key
fallback, recovery code, and the existing OTPForm for code entry. No skip
control. Login.tsx routes onMfaRequired by stage; the existing OTPForm is
reused for the NextMfa challenge.
Tests: 8 new MFA wire-contract tests (pure, mocked fetch). 14/14 green.
16 lines
382 B
JSON
16 lines
382 B
JSON
{
|
|
"name": "@hanzo/id",
|
|
"private": true,
|
|
"version": "0.1.29",
|
|
"description": "Hanzo ID — white-label login + identity verification portal (Vite + @hanzo/gui)",
|
|
"scripts": {
|
|
"build": "pnpm -r build",
|
|
"dev": "pnpm --filter @hanzo/id-web dev",
|
|
"tc": "pnpm -r tc",
|
|
"clean": "bash scripts/clean.sh"
|
|
},
|
|
"devDependencies": {
|
|
"typescript": "^5.9.3"
|
|
}
|
|
}
|