From 12b9fdb79b12727480642e630d82b53e8101fd81 Mon Sep 17 00:00:00 2001 From: t2d Date: Wed, 21 Oct 2026 17:38:05 +0200 Subject: [PATCH 1/8] Add login with two-factor authentication The plugin exchanges the password, and for users with 2FA a TOTP or backup code entered in a second step, for a webmail credential from the Userli API. Roundcube uses the credential for IMAP and SMTP, the session ends when it expires, and logging out revokes it. With a Userli version without the login endpoint, the plugin keeps logging in with the password. Co-Authored-By: Claude Opus 5.5 --- README.md | 7 +- localization/de_DE.inc | 9 ++ localization/en_US.inc | 9 ++ tests/UserliAliasesTest.php | 10 -- tests/UserliLoginTest.php | 262 ++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 101 ++++++++++++++ userli.php | 216 +++++++++++++++++++++++++---- 7 files changed, 576 insertions(+), 38 deletions(-) create mode 100644 localization/de_DE.inc create mode 100644 localization/en_US.inc create mode 100644 tests/UserliLoginTest.php diff --git a/README.md b/README.md index ef83b10..cddcf09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Roundcube Userli Plugin -A Roundcube plugin that automatically synchronizes user identities from a userli aliases API at login. +A Roundcube plugin for [Userli](https://github.com/systemli/userli): + +* **Login with two-factor authentication:** the plugin checks the password and, for users with 2FA, asks for the TOTP or backup code in a second step. Userli then issues a webmail credential, which Roundcube uses for IMAP and SMTP instead of the password. It is valid for 12 hours; the Roundcube session ends with it. Logging out revokes it. +* **Alias identities:** at login, the plugin creates a Roundcube identity for each alias of the user. + +With a Userli version without the webmail login endpoint, the plugin keeps logging in with the password. ## Installation diff --git a/localization/de_DE.inc b/localization/de_DE.inc new file mode 100644 index 0000000..69dbf30 --- /dev/null +++ b/localization/de_DE.inc @@ -0,0 +1,9 @@ +assertArrayHasKey('login_after', $hooks); } - public function testAuthenticateStoresPassword(): void - { - $args = ['pass' => 'test_password_123']; - - $this->plugin->authenticate_store_pass($args); - - // We can't directly test the private property, but we can verify no errors occur - $this->assertTrue(true); - } - public function testLoginAfterWithSuccessfulApiResponse(): void { // Mock configuration diff --git a/tests/UserliLoginTest.php b/tests/UserliLoginTest.php new file mode 100644 index 0000000..9bfca7d --- /dev/null +++ b/tests/UserliLoginTest.php @@ -0,0 +1,262 @@ +status; + } + + public function getBody(): string + { + return json_encode($this->body); + } +} + +class FakeClient +{ + public array $calls = []; + + /** @param array $responses */ + public function __construct(private array $responses) + { + } + + public function request(string $method, string $url, array $options) + { + $this->calls[] = ['url' => $url, 'json' => $options['json'], 'http_errors' => $options['http_errors']]; + $response = array_shift($this->responses); + if ($response instanceof Exception) { + throw $response; + } + return $response; + } +} + +class UserliLoginTest extends TestCase +{ + private userli $plugin; + private rcmail $rcmail; + + protected function setUp(): void + { + $_SESSION = []; + $_POST = []; + + $this->plugin = new userli(); + $this->rcmail = rcmail::get_instance(); + $this->rcmail->requestValid = true; + $this->rcmail->sessionKilled = false; + + $config = $this->getMockBuilder(stdClass::class)->addMethods(['get'])->getMock(); + $config->method('get')->willReturnCallback(static fn ($key) => [ + 'userli_url' => 'https://users.example.org', + 'userli_ssl_verify' => true, + 'userli_token' => 'token', + ][$key] ?? null); + $this->rcmail->config = $config; + } + + private function client(FakeResponse|Exception ...$responses): FakeClient + { + $client = new FakeClient($responses); + $this->rcmail->set_http_client($client); + return $client; + } + + private function login(string $user, string $pass): array + { + return $this->plugin->authenticate(['user' => $user, 'pass' => $pass, 'abort' => false, 'error' => null]); + } + + public function testInitRegistersLoginHooks(): void + { + $this->plugin->init(); + + $this->assertArrayHasKey('startup', $this->plugin->getHooks()); + $this->assertArrayHasKey('loginform_content', $this->plugin->getHooks()); + } + + public function testPasswordIsExchangedForWebmailCredential(): void + { + $client = $this->client(new FakeResponse(200, ['password' => 'credential', 'expiresAt' => '2030-01-01T00:00:00+00:00'])); + + $args = $this->login('user@example.org', 'password'); + + $this->assertSame('credential', $args['pass']); + $this->assertFalse($args['abort']); + $this->assertSame('https://users.example.org/api/roundcube/login', $client->calls[0]['url']); + $this->assertSame(['email' => 'user@example.org', 'password' => 'password', 'code' => ''], $client->calls[0]['json']); + $this->assertFalse($client->calls[0]['http_errors']); + } + + public function testExpiryIsStoredAndAliasesUseThePassword(): void + { + $client = $this->client( + new FakeResponse(200, ['password' => 'credential', 'expiresAt' => '2030-01-01T00:00:00+00:00']), + new FakeResponse(200, []), + ); + $user = $this->getMockBuilder(stdClass::class)->addMethods(['get_username', 'list_emails'])->getMock(); + $user->method('get_username')->willReturn('user@example.org'); + $user->method('list_emails')->willReturn([]); + $this->rcmail->user = $user; + + $this->login('user@example.org', 'password'); + $this->plugin->login_after_update_identities(); + + $this->assertSame(strtotime('2030-01-01T00:00:00+00:00'), $_SESSION['userli_expires']); + $this->assertSame('https://users.example.org/api/roundcube/aliases', $client->calls[1]['url']); + $this->assertSame('password', $client->calls[1]['json']['password']); + } + + public function testOldUserliWithoutLoginEndpointKeepsPassword(): void + { + $this->client(new FakeResponse(404)); + + $args = $this->login('user@example.org', 'password'); + + $this->assertSame('password', $args['pass']); + $this->assertFalse($args['abort']); + } + + public function testWrongPasswordFailsLikeBefore(): void + { + $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $args = $this->login('user@example.org', 'wrong'); + + $this->assertTrue($args['abort']); + $this->assertNull($args['error']); + } + + public function testUnreachableUserliAbortsLogin(): void + { + $this->client(new Exception('Connection refused')); + + $args = $this->login('user@example.org', 'password'); + + $this->assertTrue($args['abort']); + $this->assertSame('userli.login_unavailable', $args['error']); + } + + public function testTwofactorRequiredShowsCodeForm(): void + { + $this->client(new FakeResponse(401, ['message' => 'twofactor required'])); + + $args = $this->login('totp@example.org', 'password'); + $form = $this->plugin->modify_login_form(['hidden' => [], 'inputs' => ['user' => [], 'password' => []], 'buttons' => []]); + + $this->assertTrue($args['abort']); + $this->assertSame('userli.twofactor_required', $args['error']); + $this->assertSame(['code'], array_keys($form['inputs'])); + $this->assertStringContainsString('name="_userli_code"', $form['inputs']['code']['content']); + $this->assertStringContainsString('value="totp@example.org"', $form['hidden']['user']); + $this->assertSame('totp@example.org', $_SESSION['userli_2fa']['user']); + $this->assertNotSame('password', $_SESSION['userli_2fa']['pass']); + } + + public function testCodeStepSendsPendingPasswordAndCode(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; + $_POST['_userli_code'] = '123456'; + $client = $this->client(new FakeResponse(200, ['password' => 'credential'])); + + $this->plugin->startup(['task' => 'login', 'action' => 'login']); + $args = $this->login('totp@example.org', ''); + + $this->assertSame('credential', $args['pass']); + $this->assertSame(['email' => 'totp@example.org', 'password' => 'password', 'code' => '123456'], $client->calls[0]['json']); + $this->assertArrayNotHasKey('userli_2fa', $_SESSION); + } + + public function testWrongCodeKeepsCodeForm(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; + $_POST['_userli_code'] = '000000'; + $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $this->plugin->startup(['task' => 'login', 'action' => 'login']); + $args = $this->login('totp@example.org', ''); + $this->plugin->modify_login_form(['hidden' => [], 'inputs' => [], 'buttons' => []]); + + $this->assertSame('userli.twofactor_invalid', $args['error']); + $this->assertSame('totp@example.org', $_SESSION['userli_2fa']['user']); + } + + public function testExpiredPendingLoginIsIgnored(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time() - 301]; + $_POST['_userli_code'] = '123456'; + $client = $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $this->plugin->startup(['task' => 'login', 'action' => 'login']); + $args = $this->login('totp@example.org', ''); + + $this->assertTrue($args['abort']); + $this->assertSame(['email' => 'totp@example.org', 'password' => '', 'code' => ''], $client->calls[0]['json']); + } + + public function testPendingLoginOfAnotherUserIsIgnored(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; + $_POST['_userli_code'] = '123456'; + $client = $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $this->plugin->startup(['task' => 'login', 'action' => 'login']); + $this->login('other@example.org', 'guess'); + + $this->assertSame(['email' => 'other@example.org', 'password' => 'guess', 'code' => ''], $client->calls[0]['json']); + } + + public function testLogoutRevokesWebmailCredential(): void + { + $_SESSION = ['user_id' => 1, 'username' => 'user@example.org', 'password' => $this->rcmail->encrypt('credential')]; + $client = $this->client(new FakeResponse(204)); + + $this->plugin->startup(['task' => 'logout', 'action' => '']); + + $this->assertSame('https://users.example.org/api/roundcube/logout', $client->calls[0]['url']); + $this->assertSame(['email' => 'user@example.org', 'password' => 'credential'], $client->calls[0]['json']); + } + + public function testLogoutWithoutValidTokenDoesNotRevoke(): void + { + $_SESSION = ['user_id' => 1, 'username' => 'user@example.org', 'password' => $this->rcmail->encrypt('credential')]; + $this->rcmail->requestValid = false; + $client = $this->client(); + + $this->plugin->startup(['task' => 'logout', 'action' => '']); + + $this->assertSame([], $client->calls); + } + + public function testSessionEndsWhenCredentialExpired(): void + { + $_SESSION = ['user_id' => 1, 'userli_expires' => time() - 1]; + + $this->plugin->startup(['task' => 'mail', 'action' => '']); + + $this->assertTrue($this->rcmail->sessionKilled); + } + + public function testSessionContinuesBeforeCredentialExpired(): void + { + $_SESSION = ['user_id' => 1, 'userli_expires' => time() + 60]; + + $this->plugin->startup(['task' => 'mail', 'action' => '']); + + $this->assertFalse($this->rcmail->sessionKilled); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4624306..4c8489e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -11,8 +11,28 @@ class rcube_plugin { public $task; + public $api; private $hooks = []; + public function __construct() + { + $this->api = new stdClass(); + $this->api->output = new class { + public function add_script($script) + { + } + }; + } + + protected function add_texts($dir) + { + } + + public function gettext($name) + { + return $name; + } + protected function add_hook($hook, $callback) { $this->hooks[$hook] = $callback; @@ -36,8 +56,41 @@ class rcmail private static $instance; public $user; public $config; + public $requestValid = true; + public $sessionKilled = false; private $httpClient; + public function encrypt($clear) + { + return 'encrypted:' . base64_encode($clear); + } + + public function decrypt($cipher) + { + return base64_decode(substr($cipher, strlen('encrypted:'))); + } + + public function check_request($mode) + { + return $this->requestValid; + } + + public function get_user_name() + { + return $_SESSION['username'] ?? null; + } + + public function get_user_password() + { + return isset($_SESSION['password']) ? $this->decrypt($_SESSION['password']) : null; + } + + public function kill_session() + { + $this->sessionKilled = true; + $_SESSION = []; + } + public static function get_instance() { if (!self::$instance) { @@ -67,3 +120,51 @@ public static function raise_error($args, $log = false, $fatal = false) } } } + +if (!class_exists('rcube_utils')) { + class rcube_utils + { + public const INPUT_GET = 1; + public const INPUT_POST = 2; + + public static function get_input_string($name, $source) + { + return $_POST[$name] ?? ''; + } + } +} + +if (!class_exists('html')) { + class html + { + public static function label($for, $text) + { + return ''; + } + + public static function quote($str) + { + return htmlspecialchars((string) $str); + } + } + + class html_inputfield + { + public function __construct(private array $attrib = []) + { + } + + public function show() + { + $attrs = ''; + foreach ($this->attrib as $key => $value) { + $attrs .= ' ' . $key . '="' . htmlspecialchars((string) $value) . '"'; + } + return ''; + } + } + + class html_hiddenfield extends html_inputfield + { + } +} diff --git a/userli.php b/userli.php index b39c97e..b4426a9 100644 --- a/userli.php +++ b/userli.php @@ -1,11 +1,12 @@ add_texts("localization/"); + // Before the requested action runs, also before the login action resets the session + $this->add_hook("startup", [$this, "startup"]); // Alter loginform content $this->add_hook("loginform_content", [$this, "modify_login_form"]); // Before the user login on the IMAP server is performed. - $this->add_hook("authenticate", [$this, "authenticate_store_pass"]); + $this->add_hook("authenticate", [$this, "authenticate"]); // Triggered after a user successfully logged in $this->add_hook("login_after", [ $this, @@ -29,10 +37,42 @@ public function init(): void ]); } + public function startup(array $args): array + { + $rcmail = rcmail::get_instance(); + + if ($args["task"] === "login") { + $this->pending = $_SESSION["userli_2fa"] ?? null; + unset($_SESSION["userli_2fa"]); + } + + if (empty($_SESSION["user_id"])) { + return $args; + } + + if ($args["task"] === "logout") { + if ($rcmail->check_request(rcube_utils::INPUT_GET | rcube_utils::INPUT_POST)) { + $this->request_api("logout", [ + "email" => $rcmail->get_user_name(), + "password" => $rcmail->get_user_password(), + ]); + } + + return $args; + } + + // Dovecot rejects the webmail credential after it expired, so end the session with it + if (isset($_SESSION["userli_expires"]) && $_SESSION["userli_expires"] <= time()) { + $rcmail->kill_session(); + } + + return $args; + } + /** - * Store user password during authentication + * Change the username placeholder and, while a 2FA login is pending, ask for the code */ - public function modify_login_form($args): void + public function modify_login_form(array $args): array { $this->api->output->add_script( "document.addEventListener('DOMContentLoaded', function() { @@ -42,14 +82,95 @@ public function modify_login_form($args): void } });" ); + + if ($this->pending === null) { + return $args; + } + + // Written here because the login action resets the session after the authenticate hook + $_SESSION["userli_2fa"] = $this->pending; + + $user = new html_hiddenfield(["name" => "_user", "value" => $this->pending["user"]]); + $code = new html_inputfield([ + "name" => "_userli_code", + "id" => "rcmloginuserlicode", + "required" => "required", + "autocomplete" => "one-time-code", + "inputmode" => "numeric", + "autofocus" => "autofocus", + ]); + + $args["hidden"]["user"] = $user->show(); + $args["inputs"] = [ + "code" => [ + "title" => html::label("rcmloginuserlicode", html::quote($this->gettext("twofactor_code"))), + "content" => $code->show(), + ], + ]; + + return $args; } /** - * Store user password during authentication + * Exchange the password (and 2FA code) for a webmail credential, which is used for IMAP and SMTP */ - public function authenticate_store_pass($args): void + public function authenticate(array $args): array { + $rcmail = rcmail::get_instance(); + $code = trim((string) rcube_utils::get_input_string("_userli_code", rcube_utils::INPUT_POST)); + $pending = $this->pending; + $this->pending = null; + + if ( + $code !== "" && $pending !== null && $pending["user"] === $args["user"] + && $pending["time"] + self::PENDING_LIFETIME > time() + ) { + $args["pass"] = (string) $rcmail->decrypt($pending["pass"]); + } else { + $code = ""; + $pending = null; + } + + // The aliases API needs the password, the credential is rejected there $this->pass = $args["pass"]; + + $response = $this->request_api("login", [ + "email" => $args["user"], + "password" => $args["pass"], + "code" => $code, + ]); + + // Userli without webmail login: Dovecot checks the password as before + if ($response !== null && $response["status"] === 404) { + return $args; + } + + if ($response !== null && $response["status"] === 200 && !empty($response["body"]["password"])) { + $args["pass"] = $response["body"]["password"]; + $expires = strtotime((string) ($response["body"]["expiresAt"] ?? "")); + $this->expires = $expires === false ? null : $expires; + + return $args; + } + + $args["abort"] = true; + + if ($response === null || $response["status"] !== 401) { + $args["error"] = "userli.login_unavailable"; + } elseif ($pending !== null) { + // Wrong code: let the user try again until the pending login expires + $this->pending = $pending; + $args["error"] = "userli.twofactor_invalid"; + } elseif (($response["body"]["message"] ?? "") === "twofactor required") { + $this->pending = [ + "user" => $args["user"], + "pass" => $rcmail->encrypt($args["pass"]), + "time" => time(), + ]; + $args["error"] = "userli.twofactor_required"; + } + + return $args; } /** @@ -58,27 +179,17 @@ public function authenticate_store_pass($args): void public function login_after_update_identities(): void { $rcmail = rcmail::get_instance(); - $this->load_config(); + + if ($this->expires !== null) { + $_SESSION["userli_expires"] = $this->expires; + } // Get list of userli aliases - $client = $rcmail->get_http_client(); try { - $response = $client->request( - 'POST', - $rcmail->config->get("userli_url") . '/api/roundcube/aliases', - [ - "verify" => $rcmail->config->get( - "userli_ssl_verify", - ), - "json" => [ - "email" => $rcmail->user->get_username(), - "password" => $this->pass, - ], - "headers" => [ - 'X-API-Token' => $rcmail->config->get("userli_token") - ], - ], - ); + $response = $this->request("aliases", [ + "email" => $rcmail->user->get_username(), + "password" => $this->pass, + ]); $response_code = $response->getStatusCode(); $result = (string) $response->getBody(); } catch (Exception $e) { @@ -166,4 +277,55 @@ public function login_after_update_identities(): void ]); } } + + /** + * POST to the userli Roundcube API, without throwing on 4xx/5xx responses + */ + private function request(string $path, array $data) + { + $rcmail = rcmail::get_instance(); + $this->load_config(); + + return $rcmail->get_http_client()->request( + 'POST', + $rcmail->config->get("userli_url") . '/api/roundcube/' . $path, + [ + "verify" => $rcmail->config->get("userli_ssl_verify"), + "http_errors" => false, + "json" => $data, + "headers" => [ + 'X-API-Token' => $rcmail->config->get("userli_token"), + ], + ], + ); + } + + /** + * Like request(), but returns status and decoded body, or null and logs the error if userli is unreachable + */ + private function request_api(string $path, array $data): ?array + { + try { + $response = $this->request($path, $data); + } catch (Exception $e) { + rcube::raise_error( + [ + "code" => 600, + "file" => __FILE__, + "line" => __LINE__, + "message" => "Userli plugin: " . $e->getMessage(), + ], + true, + false, + ); + return null; + } + + $body = json_decode((string) $response->getBody(), true); + + return [ + "status" => $response->getStatusCode(), + "body" => is_array($body) ? $body : [], + ]; + } } From bb5e1795c199db513e96216b69a97476bd16979d Mon Sep 17 00:00:00 2001 From: t2d Date: Wed, 21 Oct 2026 18:05:07 +0200 Subject: [PATCH 2/8] Address review feedback on the 2FA login - Skip requests that Roundcube rejects anyway (no CSRF token or session) instead of sending the password to Userli - Drop an expired pending login before showing the code form, and do not call Userli for a code without a valid pending login - Remove the pending login with rcube_session::remove(), so it does not stay in the stored session - Revoke the credential in session_destroy, after Roundcube's own logout actions, and when the IMAP login fails after Userli issued one - Show messages for too many attempts and a required password change - Log the fallback to password login when Userli has no login endpoint - Validate the credential and expiry types in the Userli response - Drop the inputmode attribute, which Roundcube filters out - Require Roundcube 1.5 (get_input_string, get_http_client) Co-Authored-By: Claude Opus 5.5 --- composer.json | 2 +- localization/de_DE.inc | 3 + localization/en_US.inc | 3 + tests/UserliLoginTest.php | 105 ++++++++++++++++++++++++++++++----- tests/bootstrap.php | 19 ++++--- userli.php | 112 +++++++++++++++++++++++++++----------- 6 files changed, 188 insertions(+), 56 deletions(-) diff --git a/composer.json b/composer.json index 9d4ca6f..5d67b25 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "roundcube": { - "min-version": "1.4.0" + "min-version": "1.5.0" } }, "config": { diff --git a/localization/de_DE.inc b/localization/de_DE.inc index 69dbf30..b6bb703 100644 --- a/localization/de_DE.inc +++ b/localization/de_DE.inc @@ -6,4 +6,7 @@ $labels['twofactor_code'] = 'Zwei-Faktor-Code'; $messages = []; $messages['twofactor_required'] = 'Gib den Code aus deiner Authenticator-App oder einen Backup-Code ein.'; $messages['twofactor_invalid'] = 'Der Code ist ungültig. Bitte versuche es erneut.'; +$messages['twofactor_expired'] = 'Die Anmeldung hat zu lange gedauert. Bitte gib dein Passwort erneut ein.'; +$messages['too_many_attempts'] = 'Zu viele Anmeldeversuche. Bitte versuche es in 15 Minuten erneut.'; +$messages['password_change_required'] = 'Bitte ändere zuerst dein Passwort in deinen Kontoeinstellungen.'; $messages['login_unavailable'] = 'Die Anmeldung ist gerade nicht möglich. Bitte versuche es später erneut.'; diff --git a/localization/en_US.inc b/localization/en_US.inc index a9dce40..089353a 100644 --- a/localization/en_US.inc +++ b/localization/en_US.inc @@ -6,4 +6,7 @@ $labels['twofactor_code'] = 'Two-factor code'; $messages = []; $messages['twofactor_required'] = 'Enter the code from your authenticator app or a backup code.'; $messages['twofactor_invalid'] = 'The code is invalid. Please try again.'; +$messages['twofactor_expired'] = 'The login took too long. Please enter your password again.'; +$messages['too_many_attempts'] = 'Too many login attempts. Please try again in 15 minutes.'; +$messages['password_change_required'] = 'Please change your password in your account settings first.'; $messages['login_unavailable'] = 'Login is currently not possible. Please try again later.'; diff --git a/tests/UserliLoginTest.php b/tests/UserliLoginTest.php index 9bfca7d..a05eaf3 100644 --- a/tests/UserliLoginTest.php +++ b/tests/UserliLoginTest.php @@ -57,7 +57,6 @@ protected function setUp(): void $this->plugin = new userli(); $this->rcmail = rcmail::get_instance(); - $this->rcmail->requestValid = true; $this->rcmail->sessionKilled = false; $config = $this->getMockBuilder(stdClass::class)->addMethods(['get'])->getMock(); @@ -78,7 +77,7 @@ private function client(FakeResponse|Exception ...$responses): FakeClient private function login(string $user, string $pass): array { - return $this->plugin->authenticate(['user' => $user, 'pass' => $pass, 'abort' => false, 'error' => null]); + return $this->plugin->authenticate(['user' => $user, 'pass' => $pass, 'valid' => true, 'abort' => false, 'error' => null]); } public function testInitRegistersLoginHooks(): void @@ -87,6 +86,8 @@ public function testInitRegistersLoginHooks(): void $this->assertArrayHasKey('startup', $this->plugin->getHooks()); $this->assertArrayHasKey('loginform_content', $this->plugin->getHooks()); + $this->assertArrayHasKey('login_failed', $this->plugin->getHooks()); + $this->assertArrayHasKey('session_destroy', $this->plugin->getHooks()); } public function testPasswordIsExchangedForWebmailCredential(): void @@ -195,49 +196,123 @@ public function testWrongCodeKeepsCodeForm(): void $this->assertSame('totp@example.org', $_SESSION['userli_2fa']['user']); } - public function testExpiredPendingLoginIsIgnored(): void + public function testInvalidRequestIsNotSentToUserli(): void + { + $client = $this->client(); + + $args = $this->plugin->authenticate(['user' => 'user@example.org', 'pass' => 'password', 'valid' => false, 'abort' => false, 'error' => null]); + + $this->assertSame([], $client->calls); + $this->assertSame('password', $args['pass']); + } + + public function testExpiredPendingLoginEndsCodeStep(): void { $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time() - 301]; $_POST['_userli_code'] = '123456'; - $client = $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + $client = $this->client(); $this->plugin->startup(['task' => 'login', 'action' => 'login']); $args = $this->login('totp@example.org', ''); + $form = $this->plugin->modify_login_form(['hidden' => [], 'inputs' => ['password' => []], 'buttons' => []]); + $this->assertSame([], $client->calls); $this->assertTrue($args['abort']); - $this->assertSame(['email' => 'totp@example.org', 'password' => '', 'code' => ''], $client->calls[0]['json']); + $this->assertSame('userli.twofactor_expired', $args['error']); + $this->assertArrayHasKey('password', $form['inputs']); + $this->assertArrayNotHasKey('userli_2fa', $_SESSION); } - public function testPendingLoginOfAnotherUserIsIgnored(): void + public function testPendingLoginOfAnotherUserIsNotUsed(): void { $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; $_POST['_userli_code'] = '123456'; - $client = $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + $client = $this->client(); $this->plugin->startup(['task' => 'login', 'action' => 'login']); - $this->login('other@example.org', 'guess'); + $args = $this->login('other@example.org', ''); + + $this->assertSame([], $client->calls); + $this->assertSame('userli.twofactor_expired', $args['error']); + } + + public function testTooManyAttempts(): void + { + $this->client(new FakeResponse(429, ['message' => 'too many attempts'])); + + $args = $this->login('user@example.org', 'password'); + + $this->assertTrue($args['abort']); + $this->assertSame('userli.too_many_attempts', $args['error']); + } + + public function testPasswordChangeRequired(): void + { + $this->client(new FakeResponse(403, ['message' => 'user password change required'])); + + $args = $this->login('user@example.org', 'password'); - $this->assertSame(['email' => 'other@example.org', 'password' => 'guess', 'code' => ''], $client->calls[0]['json']); + $this->assertSame('userli.password_change_required', $args['error']); } - public function testLogoutRevokesWebmailCredential(): void + public function testDisabledUserFailsLikeWrongPassword(): void + { + $this->client(new FakeResponse(403, ['message' => 'user disabled due to spam role'])); + + $args = $this->login('spam@example.org', 'password'); + + $this->assertTrue($args['abort']); + $this->assertNull($args['error']); + } + + public function testUnexpectedCredentialFormatAbortsLogin(): void + { + $this->client(new FakeResponse(200, ['password' => ['not', 'a', 'string'], 'expiresAt' => []])); + + $args = $this->login('user@example.org', 'password'); + + $this->assertTrue($args['abort']); + $this->assertSame('userli.login_unavailable', $args['error']); + } + + public function testFailedImapLoginRevokesCredential(): void + { + $client = $this->client(new FakeResponse(200, ['password' => 'credential']), new FakeResponse(204)); + + $this->login('user@example.org', 'password'); + $this->plugin->login_failed(['user' => 'user@example.org', 'code' => 1]); + + $this->assertSame('https://users.example.org/api/roundcube/logout', $client->calls[1]['url']); + $this->assertSame(['email' => 'user@example.org', 'password' => 'credential'], $client->calls[1]['json']); + } + + public function testFailedLoginWithoutCredentialRevokesNothing(): void + { + $client = $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $this->login('user@example.org', 'wrong'); + $this->plugin->login_failed(['user' => 'user@example.org', 'code' => 1]); + + $this->assertCount(1, $client->calls); + } + + public function testSessionDestroyRevokesWebmailCredential(): void { $_SESSION = ['user_id' => 1, 'username' => 'user@example.org', 'password' => $this->rcmail->encrypt('credential')]; $client = $this->client(new FakeResponse(204)); - $this->plugin->startup(['task' => 'logout', 'action' => '']); + $this->plugin->session_destroy([]); $this->assertSame('https://users.example.org/api/roundcube/logout', $client->calls[0]['url']); $this->assertSame(['email' => 'user@example.org', 'password' => 'credential'], $client->calls[0]['json']); } - public function testLogoutWithoutValidTokenDoesNotRevoke(): void + public function testSessionDestroyWithoutLoginRevokesNothing(): void { - $_SESSION = ['user_id' => 1, 'username' => 'user@example.org', 'password' => $this->rcmail->encrypt('credential')]; - $this->rcmail->requestValid = false; + $_SESSION = ['temp' => true]; $client = $this->client(); - $this->plugin->startup(['task' => 'logout', 'action' => '']); + $this->plugin->session_destroy([]); $this->assertSame([], $client->calls); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4c8489e..4c7f000 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -56,10 +56,20 @@ class rcmail private static $instance; public $user; public $config; - public $requestValid = true; public $sessionKilled = false; + public $session; private $httpClient; + public function __construct() + { + $this->session = new class { + public function remove($var) + { + unset($_SESSION[$var]); + } + }; + } + public function encrypt($clear) { return 'encrypted:' . base64_encode($clear); @@ -70,11 +80,6 @@ public function decrypt($cipher) return base64_decode(substr($cipher, strlen('encrypted:'))); } - public function check_request($mode) - { - return $this->requestValid; - } - public function get_user_name() { return $_SESSION['username'] ?? null; @@ -88,7 +93,7 @@ public function get_user_password() public function kill_session() { $this->sessionKilled = true; - $_SESSION = []; + $_SESSION = ['language' => 'en_US', 'temp' => true]; } public static function get_instance() diff --git a/userli.php b/userli.php index b4426a9..d5add76 100644 --- a/userli.php +++ b/userli.php @@ -20,6 +20,7 @@ class userli extends rcube_plugin private string $pass = ""; private ?array $pending = null; private ?int $expires = null; + private ?string $credential = null; public function init(): void { @@ -35,6 +36,10 @@ public function init(): void $this, "login_after_update_identities", ]); + // IMAP login failed after userli issued a credential + $this->add_hook("login_failed", [$this, "login_failed"]); + // Logout, session timeout or a new login, after Roundcube's own logout actions + $this->add_hook("session_destroy", [$this, "session_destroy"]); } public function startup(array $args): array @@ -42,27 +47,13 @@ public function startup(array $args): array $rcmail = rcmail::get_instance(); if ($args["task"] === "login") { - $this->pending = $_SESSION["userli_2fa"] ?? null; - unset($_SESSION["userli_2fa"]); - } - - if (empty($_SESSION["user_id"])) { - return $args; - } - - if ($args["task"] === "logout") { - if ($rcmail->check_request(rcube_utils::INPUT_GET | rcube_utils::INPUT_POST)) { - $this->request_api("logout", [ - "email" => $rcmail->get_user_name(), - "password" => $rcmail->get_user_password(), - ]); - } - - return $args; + $pending = $_SESSION["userli_2fa"] ?? null; + $rcmail->session->remove("userli_2fa"); + $this->pending = is_array($pending) && $pending["time"] + self::PENDING_LIFETIME > time() ? $pending : null; } // Dovecot rejects the webmail credential after it expired, so end the session with it - if (isset($_SESSION["userli_expires"]) && $_SESSION["userli_expires"] <= time()) { + if (!empty($_SESSION["user_id"]) && isset($_SESSION["userli_expires"]) && $_SESSION["userli_expires"] <= time()) { $rcmail->kill_session(); } @@ -96,7 +87,6 @@ public function modify_login_form(array $args): array "id" => "rcmloginuserlicode", "required" => "required", "autocomplete" => "one-time-code", - "inputmode" => "numeric", "autofocus" => "autofocus", ]); @@ -116,18 +106,26 @@ public function modify_login_form(array $args): array */ public function authenticate(array $args): array { + // Roundcube rejects the request anyway (missing CSRF token or session) + if (empty($args["valid"])) { + return $args; + } + $rcmail = rcmail::get_instance(); $code = trim((string) rcube_utils::get_input_string("_userli_code", rcube_utils::INPUT_POST)); $pending = $this->pending; $this->pending = null; - if ( - $code !== "" && $pending !== null && $pending["user"] === $args["user"] - && $pending["time"] + self::PENDING_LIFETIME > time() - ) { + if ($code !== "") { + if ($pending === null || $pending["user"] !== $args["user"]) { + $args["abort"] = true; + $args["error"] = "userli.twofactor_expired"; + + return $args; + } + $args["pass"] = (string) $rcmail->decrypt($pending["pass"]); } else { - $code = ""; $pending = null; } @@ -139,35 +137,83 @@ public function authenticate(array $args): array "password" => $args["pass"], "code" => $code, ]); + $status = $response["status"] ?? null; + $body = $response["body"] ?? []; + + if ($status === 404) { + rcube::raise_error( + [ + "code" => 600, + "file" => __FILE__, + "line" => __LINE__, + "message" => "Userli plugin: login endpoint not found, logging in without 2FA", + ], + true, + false, + ); - // Userli without webmail login: Dovecot checks the password as before - if ($response !== null && $response["status"] === 404) { return $args; } - if ($response !== null && $response["status"] === 200 && !empty($response["body"]["password"])) { - $args["pass"] = $response["body"]["password"]; - $expires = strtotime((string) ($response["body"]["expiresAt"] ?? "")); + if ($status === 200 && is_string($body["password"] ?? null) && $body["password"] !== "") { + $this->credential = $body["password"]; + $args["pass"] = $body["password"]; + $expires = is_string($body["expiresAt"] ?? null) ? strtotime($body["expiresAt"]) : false; $this->expires = $expires === false ? null : $expires; return $args; } $args["abort"] = true; + $message = $body["message"] ?? ""; - if ($response === null || $response["status"] !== 401) { - $args["error"] = "userli.login_unavailable"; - } elseif ($pending !== null) { + if ($status === 401 && $pending !== null) { // Wrong code: let the user try again until the pending login expires $this->pending = $pending; $args["error"] = "userli.twofactor_invalid"; - } elseif (($response["body"]["message"] ?? "") === "twofactor required") { + } elseif ($status === 401 && $message === "twofactor required") { $this->pending = [ "user" => $args["user"], "pass" => $rcmail->encrypt($args["pass"]), "time" => time(), ]; $args["error"] = "userli.twofactor_required"; + } elseif ($status === 429) { + $args["error"] = "userli.too_many_attempts"; + } elseif ($status === 403 && $message === "user password change required") { + $args["error"] = "userli.password_change_required"; + } elseif ($status !== 401 && $status !== 403) { + $args["error"] = "userli.login_unavailable"; + } + + return $args; + } + + /** + * Revoke a credential that Roundcube could not use + */ + public function login_failed(array $args): array + { + if ($this->credential !== null) { + $this->request_api("logout", ["email" => $args["user"], "password" => $this->credential]); + $this->credential = null; + } + + return $args; + } + + /** + * Revoke the webmail credential of the ending session + */ + public function session_destroy(array $args): array + { + $rcmail = rcmail::get_instance(); + + if (!empty($_SESSION["user_id"])) { + $this->request_api("logout", [ + "email" => $rcmail->get_user_name(), + "password" => $rcmail->get_user_password(), + ]); } return $args; From 44fe4f218fe9d37ee65460b349131a5a7fd67213 Mon Sep 17 00:00:00 2001 From: t2d Date: Wed, 21 Oct 2026 18:24:55 +0200 Subject: [PATCH 3/8] Offer another account in the code step and do not count it as a failed login The code step gets a "Use another account" link that drops the pending login. It is answered with HTTP 200 instead of 401, because the password was right: brute-force detection on the proxy access log (CrowdSec) must not count it. Wrong codes stay 401. Co-Authored-By: Claude Opus 5.5 --- localization/de_DE.inc | 1 + localization/en_US.inc | 1 + tests/UserliLoginTest.php | 44 +++++++++++++++++++++++++++++++++++++++ tests/bootstrap.php | 7 ++++++- userli.php | 24 ++++++++++++++++++++- 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/localization/de_DE.inc b/localization/de_DE.inc index b6bb703..d10f369 100644 --- a/localization/de_DE.inc +++ b/localization/de_DE.inc @@ -2,6 +2,7 @@ $labels = []; $labels['twofactor_code'] = 'Zwei-Faktor-Code'; +$labels['use_another_account'] = 'Anderes Konto verwenden'; $messages = []; $messages['twofactor_required'] = 'Gib den Code aus deiner Authenticator-App oder einen Backup-Code ein.'; diff --git a/localization/en_US.inc b/localization/en_US.inc index 089353a..bd4e137 100644 --- a/localization/en_US.inc +++ b/localization/en_US.inc @@ -2,6 +2,7 @@ $labels = []; $labels['twofactor_code'] = 'Two-factor code'; +$labels['use_another_account'] = 'Use another account'; $messages = []; $messages['twofactor_required'] = 'Enter the code from your authenticator app or a backup code.'; diff --git a/tests/UserliLoginTest.php b/tests/UserliLoginTest.php index a05eaf3..39aa1da 100644 --- a/tests/UserliLoginTest.php +++ b/tests/UserliLoginTest.php @@ -54,6 +54,7 @@ protected function setUp(): void { $_SESSION = []; $_POST = []; + $_GET = []; $this->plugin = new userli(); $this->rcmail = rcmail::get_instance(); @@ -196,6 +197,49 @@ public function testWrongCodeKeepsCodeForm(): void $this->assertSame('totp@example.org', $_SESSION['userli_2fa']['user']); } + public function testCodeFormOffersAnotherAccount(): void + { + $this->client(new FakeResponse(401, ['message' => 'twofactor required'])); + + $this->login('totp@example.org', 'password'); + $form = $this->plugin->modify_login_form(['hidden' => [], 'inputs' => [], 'buttons' => []]); + + $this->assertStringContainsString('_userli_cancel=1', $form['buttons']['userli_cancel']['content']); + } + + public function testCancelDropsPendingLogin(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; + $_GET['_userli_cancel'] = '1'; + + $this->plugin->startup(['task' => 'login', 'action' => '']); + $form = $this->plugin->modify_login_form(['hidden' => [], 'inputs' => ['password' => []], 'buttons' => []]); + + $this->assertArrayHasKey('password', $form['inputs']); + $this->assertArrayNotHasKey('userli_2fa', $_SESSION); + } + + public function testCodeStepIsNotAFailedLogin(): void + { + $this->client(new FakeResponse(401, ['message' => 'twofactor required'])); + + $this->login('totp@example.org', 'password'); + + $this->assertSame(200, $this->plugin->unauthenticated(['task' => 'login', 'http_code' => 401])['http_code']); + } + + public function testWrongCodeStaysAFailedLogin(): void + { + $_SESSION['userli_2fa'] = ['user' => 'totp@example.org', 'pass' => $this->rcmail->encrypt('password'), 'time' => time()]; + $_POST['_userli_code'] = '000000'; + $this->client(new FakeResponse(401, ['message' => 'authentication failed'])); + + $this->plugin->startup(['task' => 'login', 'action' => 'login']); + $this->login('totp@example.org', ''); + + $this->assertSame(401, $this->plugin->unauthenticated(['task' => 'login', 'http_code' => 401])['http_code']); + } + public function testInvalidRequestIsNotSentToUserli(): void { $client = $this->client(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4c7f000..68e9d00 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -134,7 +134,7 @@ class rcube_utils public static function get_input_string($name, $source) { - return $_POST[$name] ?? ''; + return ($source === self::INPUT_GET ? $_GET[$name] ?? '' : $_POST[$name] ?? ''); } } } @@ -147,6 +147,11 @@ public static function label($for, $text) return ''; } + public static function a($attrib, $content) + { + return '' . $content . ''; + } + public static function quote($str) { return htmlspecialchars((string) $str); diff --git a/userli.php b/userli.php index d5add76..4ac0dbf 100644 --- a/userli.php +++ b/userli.php @@ -21,6 +21,7 @@ class userli extends rcube_plugin private ?array $pending = null; private ?int $expires = null; private ?string $credential = null; + private bool $twofactor_required = false; public function init(): void { @@ -40,6 +41,8 @@ public function init(): void $this->add_hook("login_failed", [$this, "login_failed"]); // Logout, session timeout or a new login, after Roundcube's own logout actions $this->add_hook("session_destroy", [$this, "session_destroy"]); + // Before the login page is sent, decides between HTTP 401 and 200 + $this->add_hook("unauthenticated", [$this, "unauthenticated"]); } public function startup(array $args): array @@ -49,7 +52,9 @@ public function startup(array $args): array if ($args["task"] === "login") { $pending = $_SESSION["userli_2fa"] ?? null; $rcmail->session->remove("userli_2fa"); - $this->pending = is_array($pending) && $pending["time"] + self::PENDING_LIFETIME > time() ? $pending : null; + $valid = is_array($pending) && $pending["time"] + self::PENDING_LIFETIME > time(); + $cancelled = rcube_utils::get_input_string("_userli_cancel", rcube_utils::INPUT_GET) !== ""; + $this->pending = $valid && !$cancelled ? $pending : null; } // Dovecot rejects the webmail credential after it expired, so end the session with it @@ -97,6 +102,10 @@ public function modify_login_form(array $args): array "content" => $code->show(), ], ]; + $args["buttons"]["userli_cancel"] = [ + "outterclass" => "formbuttons", + "content" => html::a(["href" => "./?_task=login&_userli_cancel=1"], html::quote($this->gettext("use_another_account"))), + ]; return $args; } @@ -178,6 +187,7 @@ public function authenticate(array $args): array "time" => time(), ]; $args["error"] = "userli.twofactor_required"; + $this->twofactor_required = true; } elseif ($status === 429) { $args["error"] = "userli.too_many_attempts"; } elseif ($status === 403 && $message === "user password change required") { @@ -189,6 +199,18 @@ public function authenticate(array $args): array return $args; } + /** + * Answer the code step with 200: the password was right, so brute-force detection must not count it + */ + public function unauthenticated(array $args): array + { + if ($this->twofactor_required) { + $args["http_code"] = 200; + } + + return $args; + } + /** * Revoke a credential that Roundcube could not use */ From b54f43dd6b653b3130b5eb0566c9eddc0dde1e16 Mon Sep 17 00:00:00 2001 From: t2d Date: Sun, 25 Oct 2026 20:58:20 +0100 Subject: [PATCH 4/8] Log the missing login endpoint to the userli log, not as an error Co-Authored-By: Claude Opus 5.5 --- tests/bootstrap.php | 4 ++++ userli.php | 11 +---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 68e9d00..6421256 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -123,6 +123,10 @@ public static function raise_error($args, $log = false, $fatal = false) { // Mock implementation - do nothing in tests } + + public static function write_log($name, $line) + { + } } } diff --git a/userli.php b/userli.php index 4ac0dbf..7c54b3d 100644 --- a/userli.php +++ b/userli.php @@ -150,16 +150,7 @@ public function authenticate(array $args): array $body = $response["body"] ?? []; if ($status === 404) { - rcube::raise_error( - [ - "code" => 600, - "file" => __FILE__, - "line" => __LINE__, - "message" => "Userli plugin: login endpoint not found, logging in without 2FA", - ], - true, - false, - ); + rcube::write_log("userli", "Login endpoint not found, logging in without 2FA"); return $args; } From fc34bf0aff2abf754820f514371d5455d776ed9e Mon Sep 17 00:00:00 2001 From: t2d Date: Sun, 25 Oct 2026 20:58:20 +0100 Subject: [PATCH 5/8] Drop the fixed wait time from the too many attempts message Co-Authored-By: Claude Opus 5.5 --- localization/de_DE.inc | 2 +- localization/en_US.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/localization/de_DE.inc b/localization/de_DE.inc index d10f369..a1a2aae 100644 --- a/localization/de_DE.inc +++ b/localization/de_DE.inc @@ -8,6 +8,6 @@ $messages = []; $messages['twofactor_required'] = 'Gib den Code aus deiner Authenticator-App oder einen Backup-Code ein.'; $messages['twofactor_invalid'] = 'Der Code ist ungültig. Bitte versuche es erneut.'; $messages['twofactor_expired'] = 'Die Anmeldung hat zu lange gedauert. Bitte gib dein Passwort erneut ein.'; -$messages['too_many_attempts'] = 'Zu viele Anmeldeversuche. Bitte versuche es in 15 Minuten erneut.'; +$messages['too_many_attempts'] = 'Zu viele Anmeldeversuche. Bitte versuche es später erneut.'; $messages['password_change_required'] = 'Bitte ändere zuerst dein Passwort in deinen Kontoeinstellungen.'; $messages['login_unavailable'] = 'Die Anmeldung ist gerade nicht möglich. Bitte versuche es später erneut.'; diff --git a/localization/en_US.inc b/localization/en_US.inc index bd4e137..b68c44f 100644 --- a/localization/en_US.inc +++ b/localization/en_US.inc @@ -8,6 +8,6 @@ $messages = []; $messages['twofactor_required'] = 'Enter the code from your authenticator app or a backup code.'; $messages['twofactor_invalid'] = 'The code is invalid. Please try again.'; $messages['twofactor_expired'] = 'The login took too long. Please enter your password again.'; -$messages['too_many_attempts'] = 'Too many login attempts. Please try again in 15 minutes.'; +$messages['too_many_attempts'] = 'Too many login attempts. Please try again later.'; $messages['password_change_required'] = 'Please change your password in your account settings first.'; $messages['login_unavailable'] = 'Login is currently not possible. Please try again later.'; From 50b78b44bea066eaece025e548b9ef0aa884c4ba Mon Sep 17 00:00:00 2001 From: t2d Date: Sun, 25 Oct 2026 20:58:20 +0100 Subject: [PATCH 6/8] Require Roundcube 1.6, the first version supporting PHP 8.1 Co-Authored-By: Claude Opus 5.5 --- composer.json | 2 +- composer.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5d67b25..ccbecec 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "extra": { "roundcube": { - "min-version": "1.5.0" + "min-version": "1.6.0" } }, "config": { diff --git a/composer.lock b/composer.lock index 5560730..448503a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3c8f7412c2582f69a613f2162b371ccf", + "content-hash": "bed9c5de82682eb1aa599fa3b36afce8", "packages": [ { "name": "roundcube/plugin-installer", @@ -1871,5 +1871,5 @@ "php": "^8.1" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } From 7d285d115859449f337ad2b1ef136377a614548c Mon Sep 17 00:00:00 2001 From: t2d Date: Sun, 25 Oct 2026 20:58:20 +0100 Subject: [PATCH 7/8] Add screenshots to the README and group the fallback note with the login Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++-- screenshots/aliases.png | Bin 0 -> 32647 bytes screenshots/twofactor.png | Bin 0 -> 21351 bytes 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 screenshots/aliases.png create mode 100644 screenshots/twofactor.png diff --git a/README.md b/README.md index cddcf09..c5f4dbe 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ A Roundcube plugin for [Userli](https://github.com/systemli/userli): -* **Login with two-factor authentication:** the plugin checks the password and, for users with 2FA, asks for the TOTP or backup code in a second step. Userli then issues a webmail credential, which Roundcube uses for IMAP and SMTP instead of the password. It is valid for 12 hours; the Roundcube session ends with it. Logging out revokes it. +* **Login with two-factor authentication:** the plugin checks the password and, for users with 2FA, asks for the TOTP or backup code in a second step. Userli then issues a webmail credential, which Roundcube uses for IMAP and SMTP instead of the password. It is valid for 12 hours; the Roundcube session ends with it. Logging out revokes it. With a Userli version without the webmail login endpoint, the plugin keeps logging in with the password. + + ![Two-factor code step](screenshots/twofactor.png) * **Alias identities:** at login, the plugin creates a Roundcube identity for each alias of the user. -With a Userli version without the webmail login endpoint, the plugin keeps logging in with the password. + ![Alias identities](screenshots/aliases.png) ## Installation diff --git a/screenshots/aliases.png b/screenshots/aliases.png new file mode 100644 index 0000000000000000000000000000000000000000..f2e340bd0ab09612e2df6b33cd6892430a963ca6 GIT binary patch literal 32647 zcmaI82UJtt*Y1n`ML=FbRJuqL5D*a|RFw|WdyPnw8fl>yML?wY4k84kOYfi{y-Dw( zgr`;qfZ-ogG5p{OFx&-b|#Q&=E;`m1UAIQazy*HU| zfP5G$9y>iGPM#3(I$tmwg z6126oty|O_hr>^2sxA9_=FU%TMj3rM{+=kBa}z=VtAlSORH&yw_e#n)NF5_i2o1M!(RNTi7g~Z3ZO5o+><&(}=QgliTwO$u0ju+p(Tb(vR zp;?(PO3LfHGr{1)ZFos_gW9fcYMVm&cDK-VyvMO77~I;mKY6AvDZ+nLSHni@!rpqU zTUkvC8@*%@Hqy3`$+`UX@e^g%+@2zBnnz8^qXmlnCXE8%qw%k-RBB59{Q0wy2wZ_1 zv5BAE+S=N;FDot8evL*q3-uB8MmoIer)SHjstwKBm@a2`CAmiQ({OvP{y4$BCpITV(Ib-3Cq_j*^c3Ner4->`_O z@hXu5g!vQQQnBEB{`eR$<~i|EQ4S~bFDt!Ky9*T+&#VkxSQ}!^jBVci8#b&}wnt@wBJH|!D*MFYf z@vcj*EGo){N+Ys`B>1QE9qE_T^R?=aWHWsBRS7{<7c&*1rS>Wufk31L>0IMwRO}HM zwJt0S)9$)0P;def6Cd6C;BdDX+1>bn*s|rf&g{$#!^Rv`VdG+!`>E%>A0B;nmqyl7 z=Z2+anBy^Gk)Qu5l}MXzf9|pr+h#I%VZMO!^s(hsIMr#PGa~ytaYsAbs(4>Ld@D3` zK3Q%Fvv*ioS{fc4EU)modwQ?m?k;RB>e#tb(pBCv?pPEk&l-9;HlQlFn zFu0v$a;H4c5O-0yL=;;Hrx&iNtJBlf?IetF61`yw_y=-yD?HrZUZ14MQN^l48_v!6 zBX63=eE66;7aspJKp0EWpG$IgtyZ}6sztAdtWPKHI83+Vy$SN%@9T>$ChhouCZFEE z*+$nnalNFZ4O{d*nMTyw+B+LdL4v<~#4WGhsdkIQ3}0z!X)%7c_xS6Fo1+ECL!I~T z5czQ+vRPI)9pG^L;o@!X8|>`4k61gqhr0XE=bHPw3A(x~#`?A~ikDQ!?l{*a2SKG1 zZZZh6r$+8Z*GP_GpKW@H-BqTd!-YG#!;9a(1%KJvvb|>hq1iVMSMQ1*&5bzSM@tZH zHpA`u<_ZwA^)Top%bxsH0pXNiRP?Mw;vB3}Ha3Eakhpl$1LkFdDQvnYROAC%n~Aw!mXCWzv& z*d1ggu3D?$^5x6v_n*nlnr~EC%{|uF%zPE_`!@@W5#7PS_$68Y6hrmobQ^O8^>4$i z$MWS{YIPXAz20OrdBb*mQKcQH;(t`^a3`nF-%Y hn)H@~p@Z;dn?xKPTkt~meA(fVfD14GY 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 literal 0 HcmV?d00001 From 02d36f61b19a6181cef3105ba0a28330be629a0b Mon Sep 17 00:00:00 2001 From: t2d Date: Sun, 25 Oct 2026 21:02:17 +0100 Subject: [PATCH 8/8] Show a key icon in the two-factor code field Co-Authored-By: Claude Opus 5.5 --- screenshots/twofactor.png | Bin 21351 -> 22016 bytes userli.php | 1 + 2 files changed, 1 insertion(+) diff --git a/screenshots/twofactor.png b/screenshots/twofactor.png index d920355580a00fc1ccc8469eaaa5bacb32599e1e..e248769265bcf2bcf2f5644426a88a62fe294e78 100644 GIT binary patch literal 22016 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 diff --git a/userli.php b/userli.php index 7c54b3d..d59ba0f 100644 --- a/userli.php +++ b/userli.php @@ -93,6 +93,7 @@ public function modify_login_form(array $args): array "required" => "required", "autocomplete" => "one-time-code", "autofocus" => "autofocus", + "data-icon" => "key", ]); $args["hidden"]["user"] = $user->show();