mkerstner / passkeys
Optional passwordless passkey (WebAuthn/FIDO2) authentication for Roundcube. Each user may enrol one or more passkeys and sign in without a password; the standard username/password login and any 2FA plugins remain fully functional.
Package info
github.com/mkerstner/roundcube-plugin-passkeys
Type:roundcube-plugin
pkg:composer/mkerstner/passkeys
Requires
- php: >=8.1
- lbuchs/webauthn: ^2.2
- roundcube/plugin-installer: ~0.3.5
README
Optional passwordless passkey (WebAuthn / FIDO2) authentication for Roundcube.
Each user may enrol one or more passkeys and then sign in without typing a password, using a fingerprint, face, device screen lock, or a hardware security key. Passkeys are strictly opt-in per user and are designed to coexist with the existing authentication features:
- The standard username/password login form keeps working unchanged.
- Any 2FA plugin keeps working — this plugin does not touch the password
authenticatepath. - A user who never enrols a passkey sees no difference at all.
How passwordless works here
Roundcube authenticates against IMAP, so it always needs the real IMAP
password to open the mailbox. A passkey cannot supply that password directly.
This plugin therefore stores the IMAP password encrypted with a key that
only the passkey can derive (via the WebAuthn PRF / hmac-secret
extension). The key never leaves the browser/authenticator, and the server
keeps only ciphertext it cannot decrypt on its own. At login the passkey
unlocks the stored password in the browser, which then completes a normal
Roundcube login.
Because this relies on the PRF extension, passwordless login requires a
reasonably modern browser (Chrome/Edge 116+, Safari 18+, recent Firefox) and
a PRF-capable authenticator. See passkeys_on_no_prf in the configuration
for how unsupported devices are handled.
Skins
The plugin is skin-independent and needs no per-skin files:
- Styles are loaded from the plugin root (
passkeys.css), not from a skin folder, so they apply on any skin — including skins that do not inherit fromelastic. - The "Sign in with a passkey" button is added to the login form via the
core
loginform_contenthook, so it renders — with an "or sign in using passkeys" divider — directly below the standard Login button. Because the login form (and these extra buttons) are rendered by Roundcube core, not by the skin, this placement is identical on every skin with no skin template changes. The divider and button are hidden until the client script confirms the browser supports WebAuthn (body.passkeys-available).
So nothing extra is required for any skin. The credential-management page in Settings uses core templates and needs no skin work either.
Requirements
- Roundcube with PHP >= 8.1 (
ext-openssl,ext-mbstring,ext-sodium). - The
lbuchs/webauthnlibrary (installed automatically via Composer). - HTTPS — WebAuthn only works over a secure context.
Installation
A. Via the Roundcube plugin installer (recommended)
Run from the Roundcube root:
composer require roundcube/passkeys
The roundcube/plugin-installer (declared as a dependency) then, driven by the
extra.roundcube block in this plugin's composer.json:
- installs the plugin into
plugins/passkeys/, - pulls in the
lbuchs/webauthndependency, - creates the database table from
SQL/<driver>.initial.sql(extra.roundcube.sql-dir), - copies
config.inc.php.disttoconfig.inc.phpif no config exists yet, - adds
passkeysto thepluginslist inconfig/config.inc.php(when that file is writable and Composer runs on the CLI), - refuses to install on Roundcube older than
min-version(1.6.0).
Set the SKIP_DB_INIT=1 environment variable before composer require if you
prefer to create the schema yourself.
B. Manual installation
-
Drop this directory into
plugins/passkeys/and runcomposer installat the Roundcube root solbuchs/webauthnis available on the autoloader. -
Create the database table:
bin/updatedb.sh --dir=plugins/passkeys/SQL --package=passkeys
-
Add
passkeysto thepluginsarray inconfig/config.inc.php. -
(Optional) copy
config.inc.php.disttoconfig.inc.phpand adjustpasskeys_rp_id,passkeys_require_uv, etc.
Schema versioning note. The plugin installer seeds the schema with
db_init(a straight run of*.initial.sql) and does not write apasskeys-versionrow into thesystemtable. Future schema migrations are applied withbin/updatedb.sh --dir=plugins/passkeys/SQL --package=passkeys, which is also the recommended way to create the table for a manual install so the version bookkeeping is in place from the start.
Configuration
See config.inc.php.dist — every option has a safe default, so no
configuration is strictly required beyond enabling the plugin. The most
important option in a multi-hostname setup is passkeys_rp_id.
Status / roadmap
This plugin is being built in phases:
- Settings management (done) — a "Passkeys" tab that lists a user's enrolled passkeys and lets them remove one. Database schema and credential store in place.
- Enrolment (done) — the WebAuthn registration ceremony from the Settings tab: create a discoverable credential, validate the attestation server-side, and persist it.
- PRF wrapping (done) — at enrolment the IMAP password is encrypted with a
key derived from the passkey (WebAuthn PRF/hmac-secret) and stored as
wrapped_secret; the server never holds a key that can decrypt it. - Passwordless login (done) — a "Sign in with a passkey" button on the
login page. The assertion is verified server-side (via the
startuphook, since Roundcube short-circuits unauthenticated requests), the wrapped password is released, decrypted in the browser, and the standard login form is submitted so the normal password/2FA path completes the sign-in. - Hardening (partial) — signature-counter regression check and last-used stamping are in place. Still to do: explicit CSRF token checks on the AJAX endpoints, brute-force/lockout integration on the assert path, and a re-enrolment prompt when a stored password becomes stale (IMAP password changed out-of-band).
License
GPL-3.0-or-later.