Skip to main content

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

AttackResult
Fake page captures ZID + equation, replays to any nodeRefused. The policy is replicated to every node, and the register door consults it too.
Fake page also asks the user to type the wordNot 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 passwordOut of scope. Recovery still works, by design.

Turning it on

Settings → Protected LoginTurn 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:

  1. Any device with a live session → Settings → generate a new word.
  2. The recovery password → equation reissue mints a session.
  3. The node admin → Accounts → Clear Protected Login (audited, fleet-wide).

API

Method · pathAuthPurpose
GET /api/zeq-auth/protectedsession{enabled, set_at, slug, link_base, nodes_total, nodes_holding} — never the word
POST /api/zeq-auth/protectedsession{generate:true} or {word} → the link, once
DELETE /api/zeq-auth/protectedsessionoff, fleet-wide
GET /api/zeq-auth/protected/slug/:slugpublic{zid} from public data only
POST /api/zeq-auth/login-v3now accepts optional slug and word
POST /api/zeq-auth/register-v3same optional fields; consults the policy before creating anything
POST /api/admin/accounts/:zid/protected-login/clearadminthe 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.