An account with no SSH key could not use the web UI at all. CreateWebSession
has one caller, the /login?token= handler, and only web login over SSH could
mint a token for it; web signup requires pasting a public key.
An unauthenticated POST /login now mails the same one-time token to a verified
address, resolved by username or address. Bounded at five an hour per account in
the store, indexed, and by the existing token bucket per source. The response
does not vary with whether the account exists, whether its address is verified,
whether the account is disabled or pending, whether it is over its budget, or
whether the identifier was empty — including on timing, which is why the send is
asynchronous.
The session cookie moves from SameSite=Strict to Lax, because a link clicked
in a mail client is a cross-site navigation and Strict can drop the cookie
through the redirect that follows. Every cookie-authenticated mutating route
carries checkOrigin; the POST routes that do not take bearer tokens or no auth
at all.
Review caught a suspension bypass that the first draft introduced. The disabled
check lived in Dispatch, and every previous way to mint a login token went
through it. login() now re-reads the user after consuming a token and refuses a
disabled account with the same response a bad token gets, and SetUserDisabled
drops unclaimed login links along with sessions and API tokens. The guard is the
stronger of the two: SetUserDisabled is not transactional, so a request in
flight between its UPDATE and its DELETE could otherwise still open a session.
Emailed links live fifteen minutes; the SSH-minted path keeps its five. This does
not widen what a browser session can do — the web dispatches with ViaAPI: true,
so no SSHOnly command is reachable from one however it was obtained.
Parity updated: gitbay.wiki 5593f9c. Session minting is no longer listed as SSH-only by design — what a browser submits is a username or an address, not a credential, and the credential comes back by mail.
Design: docs/specs/2026-09-04-email-login-design.md.
Closes #155