Protected Login
A copied sign-in page can capture a ZID and equation and replay them to a real node. Protected Login closes that door. Once a user turns it on, no node mints a session for that ZID without the user's login word, and the word lives only in the user's personal link:
https://<home node>/s/<slug>/login/#<word>
The word rides in the URL fragment, so it never reaches a server log or a Referer header. The page moves it into sessionStorage, scrubs it from the address bar, and sends it only in the sign-in POST body to the same origin.
What it stops, plainly
| Attack | Result |
|---|---|
| Fake page captures ZID + equation, replays to any node | Refused. The policy is replicated to every node, and the register door consults it too. |
| Fake page also asks the user to type the word | Not stopped by software. The real flow never asks for the word; Settings says so. |
| Attacker knows the slug (it is public and equals the ZID) | Nothing gained. The word is only checked after a correct equation. |
| Attacker holds equation and recovery password | Out of scope. Recovery still works, by design. |
Turning it on
Settings → Protected Login → Turn on / new word. The user may choose a word (12+ characters, letters, digits and dashes) or receive four random words from a 2,048-word list (44 bits). The link is shown once; a new word can be generated any time from a signed-in session.
Setting a word makes the identity strict. There is no half mode: with the word optional, a phished equation would still sign in.
Forgot the word
Three doors, none of them new:
- Any device with a live session → Settings → generate a new word.
- The recovery password → equation reissue mints a session.
- The node admin → Accounts → Clear Protected Login (audited, fleet-wide).
API
| Method · path | Auth | Purpose |
|---|---|---|
GET /api/zeq-auth/protected | session | {enabled, set_at, slug, link_base, nodes_total, nodes_holding} — never the word |
POST /api/zeq-auth/protected | session | {generate:true} or {word} → the link, once |
DELETE /api/zeq-auth/protected | session | off, fleet-wide |
GET /api/zeq-auth/protected/slug/:slug | public | {zid} from public data only |
POST /api/zeq-auth/login-v3 | — | now accepts optional slug and word |
POST /api/zeq-auth/register-v3 | — | same optional fields; consults the policy before creating anything |
POST /api/admin/accounts/:zid/protected-login/clear | admin | the owner's door |
A sign-in with a correct equation but no word returns 401 with code: "PROTECTED_LOGIN_REQUIRED" and does not count toward the account lock. A wrong word returns the ordinary flat 401 and counts toward the same 10-in-15-minutes lock a wrong equation does.
How the policy travels
Each node keeps zeq_login_policy(zid, word_hash, revision, cleared). Writes fan out node-to-node as a portable scrypt hash, signed with the sender's Ed25519 node key over the fields, and a receiver applies a row only if its revision is newer, so a stale write cannot resurrect a cleared word. A node that has not migrated treats every identity as open, exactly as before the feature existed.