device approval: drop the confirmation checkbox

No device page anyone actually uses has one — Google, GitHub and AWS all show
the code and an Approve button. It sat in front of the only action on the screen
and disabled it until ticked.

It was there as an anti-phishing gate, on the theory that a victim arriving from
a crafted verification_uri_complete link must not approve with one click. A
tickbox is not evidence of that: someone being walked through a crafted link
ticks it exactly as readily as they click Approve. The property that does the
work is the one that stays — the code is displayed and remains EDITABLE, so
approving is an explicit act on a value the human can read and correct against
what their own device shows.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zooqueen
2026-08-01 18:47:36 -07:00
co-authored by hanzo-dev
parent c78eddd735
commit 5d411ff26a
+9 -20
View File
@@ -55,12 +55,14 @@ function scrubUrl() {
export function DeviceApproval({ client, brand }: { client: AuthClient; brand: BrandContract }) {
const [phase, setPhase] = useState<Phase>({ s: 'checking' })
const [userCode, setUserCode] = useState(() => readUserCode())
// Anti-phishing gate: the `?user_code=` prefill is DISPLAY-ONLY. A signed-in
// victim who lands here from a crafted `verification_uri_complete` link must
// NOT be able to approve an attacker's device with one click — they have to
// explicitly affirm the code matches the one their OWN device shows. The
// prefill cannot tick this box, so it can never auto-approve.
const [confirmed, setConfirmed] = useState(false)
// The code is prefilled from `?user_code=` and stays EDITABLE, which is the
// anti-phishing property that matters: approving is an explicit click on a
// code the human can read and correct against what their own device shows.
// There was also a "I started this sign-in" checkbox in front of that click.
// No device page anyone actually uses has one — Google, GitHub and AWS all
// show the code and an Approve button — and a tickbox is not evidence: a
// victim being walked through a crafted link ticks it as readily as they
// click Approve. It bought nothing and cost every honest user a step.
const [error, setError] = useState<string | null>(null)
const appLabel = client.tenant.appName
@@ -181,26 +183,13 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
</p>
) : null}
<label className="hanzo-id-device-confirm">
<input
className="hanzo-id-check"
type="checkbox"
checked={confirmed}
onChange={(e) => setConfirmed(e.target.checked)}
disabled={busy}
/>
<span>
I started this sign-in on my own device, and this code matches the one it shows.
</span>
</label>
{error ? <p role="alert" className="hanzo-id-error">{error}</p> : null}
<div className="hanzo-id-cta-row">
<button
type="button"
className="hanzo-id-btn"
disabled={busy || userCode.trim().length === 0 || !confirmed}
disabled={busy || userCode.trim().length === 0}
onClick={approve}
>
{busy ? 'Approving' : consent ? 'Approve & grant access' : 'Approve'}