Skip to content

✨ Add Roundcube login with 2FA and short-lived webmail credentials - #1394

Merged
t2d merged 26 commits into
mainfrom
feat/webmail-2fa
Oct 26, 2026
Merged

t2d merged 26 commits into
mainfrom
feat/webmail-2fa

Conversation

@t2d

@t2d t2d commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Plugin side: roundcube-userli-plugin PR 9.

Goal

Enforce 2FA for mail. Roundcube logs in with the main password plus a TOTP code and then uses a short-lived app password for IMAP and SMTP. Mail programs of 2FA users need app passwords.

Changes

  • POST /api/roundcube/login checks the main password and, for 2FA users, a TOTP or backup code. It returns a webmail credential (an app password that expires after 12 hours) and its expiresAt. If the code is missing, it returns 401 twofactor required. Spam users and users who must change their password get 403, only after a correct password. Per-user attempt limits follow in Per-user limits for password and 2FA code checks #1400. App passwords are rejected.
  • POST /api/roundcube/logout revokes the webmail credential. It never touches regular app passwords. A password change and turning on MailCrypt also revoke all webmail credentials. An admin password reset removes all app passwords.
  • app_passwords.expires_at: expired credentials no longer authenticate and are pruned daily. Webmail credentials are hidden from the account page, the admin list, the metrics and the user stats.
  • New setting Require app passwords with 2FA (twofactor_app_passwords_required): Dovecot and Postfix reject the main password of 2FA users. It is on by default. A migration turns it off on existing installations, so their mail programs keep working. When it is on, the 2FA page tells users that their mail programs need app passwords and links to them.

Tested end to end with plugin release 1.1.0 against the dev stack (Roundcube, Dovecot, MailCrypt), with the setting both on and off.

🤖 Generated with Claude Code

@t2d
t2d added this pull request to stack #1395 October 21, 2026 14:36
@t2d t2d added enhancement New feature or request security labels Oct 21, 2026
Base automatically changed from feat/app-passwords to main October 21, 2026 14:41
@t2d
t2d force-pushed the feat/webmail-2fa branch 3 times, most recently from d9e204d to 5bcad2d Compare October 21, 2026 15:13
@t2d

t2d commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author
image

t2d and others added 12 commits October 25, 2026 12:42
POST /api/roundcube/login checks the main password and, for users with
2FA, a TOTP or backup code. It returns an app password that expires
after 12 hours, which Roundcube uses for IMAP and SMTP instead of the
main password. POST /api/roundcube/logout revokes it.

Webmail credentials are hidden from the account page, the admin list,
the metrics and the user stats. Expired ones are pruned daily.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New setting twofactor_app_passwords_required: Dovecot and Postfix
reject the main password of users with 2FA, so their mail programs need
app passwords and webmail the Roundcube login.

It defaults to on for new installations. A migration turns it off when
users already exist, so existing mail setups keep working until the
admin enables it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App passwords were never released, so the table is created with
last_login_time and expires_at right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Roundcube plugin ends the webmail session when the credential expires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Failed attempts on /api/roundcube/login get no Dovecot auth penalty, so
password and 2FA code guesses are limited to 10 per user in 15 minutes.
Spam users and users who must change their password get 403 like in the
Dovecot API, and a backup code is used up only once the credential exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gration

The migration relied on the settings cache being cleared after the
upgrade; a stale cache made existing installs fall back to the default
and enforce it. Now the default is off and the wizard turns it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase onto main replaced the allowAppPassword flag with
authenticateMailClient(), so the check moves there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@t2d
t2d force-pushed the feat/webmail-2fa branch from 4471a92 to 00ae510 Compare October 25, 2026 11:47
t2d and others added 8 commits October 25, 2026 12:57
- Check the spam role and a required password change before
  authenticating in the Roundcube login, like Dovecot and Postfix, so
  rejected logins don't count as last login
- Schedule the removal of webmail credentials on password change in the
  same flush as the new password, like removeByUser(), instead of a bulk
  delete in a listener after the flush

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…allations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Webmail credentials create no MailCrypt keys for users without them
- Metrics count only app passwords, not webmail credentials
- app:admin:password ends webmail sessions, admin reset without MailCrypt removes app passwords
- Roundcube login answers a MailCrypt decryption failure like the Dovecot API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webmail credentials of users without a MailCrypt key carry no key copy,
so Dovecot fails on them once a key exists. Also run the settings
migration insert through addSql and note why the login event comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The spam role and a required password change were reported for any
password, which revealed that the account exists and its state.
verifyPassword() dispatches no login event, so rejected logins still
don't count as last login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The limit let anyone lock a user out of webmail and is not needed to
avoid a regression: per-IP protection belongs to the webserver, as for
the web login. Per-user limits for all password and 2FA code checks
follow separately with consistent bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
None of them is in a release yet, so app_passwords is created in its
final shape together with the setting for existing installations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@t2d

t2d commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Hint on the two-factor page while twofactor_app_passwords_required is on (the default for new installations):

Two-factor page with the app password hint

t2d and others added 3 commits October 26, 2026 09:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…abled

The first app password or a recovery token creates a key without turning
MailCrypt on, so Dovecot never asks for it and the credential keeps
working. Only remove webmail credentials when MailCrypt gets enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mail login

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

@t2d
t2d marked this pull request as ready for review October 26, 2026 09:17
@t2d
t2d merged commit a48adaf into main Oct 26, 2026
12 checks passed
@t2d
t2d deleted the feat/webmail-2fa branch October 26, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant