device approval: stop naming the wrong app, and say what the page can vouch for

It read 'You are about to authorize hanzo-console' for a sign-in started by
hanzo-cli: appLabel was tenant.appName, this PORTAL's own branding, never the
client being approved. A consent screen that names the wrong party is worse than
one that names none — it teaches people the name means nothing.

It cannot honestly learn the right name either, and must not: the user_code is
40 bits and the only secret in this flow, so an endpoint that revealed which app
a code belonged to would be an oracle for hunting live codes — exactly what
device.go refuses to be, answering unknown, expired and already-approved with
one opaque refusal.

So the prompt asks about the thing this page CAN vouch for: the code in front of
the human, and whether they started the sign-in themselves. That is also where
the removed checkbox's intent belongs — in the sentence someone reads before
clicking, not a tickbox in front of it.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-08-01 20:30:38 -07:00
co-authored by Hanzo Dev
parent 1bb749b64b
commit 76c474a205
+21 -5
View File
@@ -64,7 +64,23 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
// 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
// NOT the client being approved. tenant.appName is this PORTAL's branding — a
// static per-tenant string — while the device authorization names its own
// application, which lives on the pending row and is what the backend actually
// approves (internal/oidc/device.go approveDevice: "the portal app the browser
// happens to be on is irrelevant to WHAT is being approved").
//
// So the page cannot honestly name the requesting client, and it must not
// LEARN it either: the user_code is 40 bits and the one secret in this flow, so
// an endpoint that said which app a code belongs to would be an oracle for
// hunting live codes — exactly what device.go refuses to be, with one opaque
// refusal for unknown / expired / already-approved.
//
// It read "You are about to authorize hanzo-console" for a sign-in started by
// hanzo-cli. A consent screen that names the wrong party is worse than one that
// names none: it teaches people that the name means nothing. What this page CAN
// vouch for is the code the human transcribed, so that is what it asks about.
const portalLabel = client.tenant.appName
// Resolve the issuer session: signed in → confirm, else → sign-in form. Reads
// same-origin from `/v1/iam/get-account` (cookie session; the brand `*.id`
@@ -154,8 +170,8 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
{email ? <p className="lede">Signed in as {email}</p> : null}
<p className="hanzo-id-device-prompt">
You are about to authorize <strong>{appLabel}</strong> to sign in on a device. Approve
ONLY if this code matches the one shown on that device.
A device is asking to sign in as you. Approve ONLY if the code below matches the
one shown on that device, and only if you started this sign-in yourself.
</p>
<label className="hanzo-id-field">
@@ -178,8 +194,8 @@ export function DeviceApproval({ client, brand }: { client: AuthClient; brand: B
{consent ? (
<p className="hanzo-id-info">
{brand.name} needs your consent to continue. By approving you grant {appLabel} access to
your profile.
{portalLabel} needs your consent to continue. By approving you grant the device
showing this code access to your profile.
</p>
) : null}