Stop pretending the two-factor field can work

Signing in with a two-factor code failed with a bare 401 and "Invalid
credentials", which sent the user off to check a password that was
perfectly good (#75). It cannot work, and the app already knew.

Stalwart accepts a TOTP code only through an OAuth flow -- its own web
interface is an OAuth client, which is why signing in *there* succeeds --
and it offers only the authorization-code and device flows. There is no
password grant, so a client holding a username and password has nowhere
to exchange them plus a code for a token. The concatenated
`password$code` form this README claimed was accepted is not a route the
server has, and appears never to have been. What was verified live on
0.16.19 was enabling and disabling 2FA, never signing in with a code.

The contradiction was already in the codebase: turning 2FA *on* mints an
app password and reseals the session onto it, precisely because a plain
password stops working from that moment. The sign-in page was the one
place still assuming otherwise.

Three changes, no new capability:

  - A 401 on a sign-in that carried a code now says what is happening
    and where to go instead, and says the password is probably fine.
    A sign-in without a code is untouched, so an ordinary typo still
    reads as an ordinary typo.

  - The field stays, and is honest about itself. Removing it would leave
    someone with 2FA finding nothing at all, which is worse than finding
    a field that explains the situation and points at app passwords.

  - The README's claim is corrected rather than quietly dropped, and
    real 2FA support is written into the roadmap as what it is: an OAuth
    implementation, handing sign-in to Stalwart and holding a refresh
    token instead of a sealed password.
This commit is contained in:
2026-08-26 14:46:08 -07:00
parent b37c422e7d
commit d98c425a9a
4 changed files with 71 additions and 2 deletions
+13 -1
View File
@@ -79,7 +79,19 @@ export function LoginPage() {
<div className="field">
<label htmlFor="t">Two-factor code</label>
<input id="t" className="input" inputMode="numeric" autoComplete="one-time-code" placeholder="123456" value={totp} onChange={(e) => setTotp(e.target.value)} autoFocus />
<span className="hint">Enter the code from your authenticator app if your account uses 2FA.</span>
{/*
Kept, and honest about itself. Stalwart accepts a TOTP code only
through an OAuth flow, and offers no password grant, so no client
holding a username and password can pass one — the field cannot
work here today. It stays because someone with 2FA will look for
it, and finding nothing is worse than finding this; the hint sends
them somewhere that does work, and the server explains it again if
they try anyway.
*/}
<span className="hint">
Most mail servers, Stalwart included, do not accept two-factor codes from webmail use an app password instead, created in
your mail server's own settings. This field is here for servers that do.
</span>
</div>
) : (
<button type="button" className="btn btn-ghost btn-sm" style={{ marginBottom: 12, color: "var(--fg-muted)" }} onClick={() => setShowTotp(true)}>