signup: ask for a password only where an account can be created

hanzo.id/signup rendered a full credential form for EVERY client_id, and
IAM refuses signup at SUBMIT — so on 48 of 51 hanzo applications a visitor
typed an email and a password and only then got "the application does not
allow to sign up new account". The provider buttons dead-ended the same
way: federation PROVISIONS a local user for a new identity, so a first-time
GitHub sign-up hit the same gate after a full round trip through GitHub.

enableSignUp was already on AppLogin and already fetched -- SignupForm reads
the same row inside onSubmit, too late to change what is on screen. Reading
it at the page turns the refusal from a surprise into a state.

It FAILS OPEN on purpose: an unreadable app config still renders the form and
the server still refuses. This is honesty about an answer we already have, not
a gate; internal/oidc/signup.go stays the only gate.

Measured against LIVE IAM, built bundle, all three directions:

  client_id               enableSignUp   <form>  input[type=password]
  hanzo-bot               true             1       1
  hanzo-chat              false            0       0
  does-not-exist-probe    unresolvable     1       1   (fail-open)

The middle row is the fix; the outer rows are what make it evidence rather
than a blanket removal of the form.

No new CSS: .hanzo-id-info already exists for exactly this.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-08-06 18:14:04 -07:00
co-authored by Hanzo Dev
parent 9111c896cb
commit bb2575a7cb
+49
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react'
import type { BrandContract } from '@hanzo/id-shared'
import { SignupForm, SocialButtons, type AuthClient } from '@hanzo/id-auth'
import { BrandHeader } from '../components/BrandHeader'
@@ -14,6 +15,54 @@ export function Signup({ client, brand }: { client: AuthClient; brand: BrandCont
const codeChallenge = sp.get('code_challenge') ?? undefined
const codeChallengeMethod = (sp.get('code_challenge_method') as 'S256' | 'plain' | null) ?? undefined
const nonce = sp.get('nonce') ?? undefined
// Only ask for credentials an account can actually be made with. IAM refuses
// signup on an app with `enableSignUp:false` — 48 of 51 hanzo applications
// today — and it refuses at SUBMIT, so this page used to take an email and a
// password and only then answer "the application does not allow to sign up
// new account". The provider buttons dead-end the same way: federation
// PROVISIONS a local user for a new identity, so a first-time GitHub sign-up
// hits the same gate after a whole round trip through GitHub.
//
// `enableSignUp` is already on `AppLogin` and already fetched — SignupForm
// reads the same row inside onSubmit. Reading it here instead is what turns
// the refusal from a surprise into a state.
//
// It FAILS OPEN, deliberately: an unreadable app config renders the form, and
// the server still refuses. This is honesty about a known answer, not a gate —
// the gate is `internal/oidc/signup.go` and must stay the only one.
const [open, setOpen] = useState(true)
useEffect(() => {
let cancelled = false
client
.getAppLogin(clientIdOverride, redirectUri)
.then((app) => {
if (!cancelled && app) setOpen(app.enableSignUp)
})
.catch(() => {})
return () => {
cancelled = true
}
}, [client, clientIdOverride, redirectUri])
if (!open) {
return (
<div className="hanzo-id-page hanzo-id-signup">
<BrandHeader brand={brand} />
<main>
<h1>Create your {brand.name} account</h1>
<p className="hanzo-id-info">
This application does not accept new accounts. If you already have
one, sign in below.
</p>
<p className="hanzo-id-footer-links">
<a href={`/login${window.location.search}`}>Sign in</a>
</p>
</main>
</div>
)
}
return (
<div className="hanzo-id-page hanzo-id-signup">
<BrandHeader brand={brand} />