From d0c2725b00286918f4a8d40474b5c640b192a0b4 Mon Sep 17 00:00:00 2001 From: Bernard Date: Thu, 22 May 2025 19:39:05 +0000 Subject: [PATCH 01/21] MFA authentication for webtrees. Integrates with setup, admin account user creation and user account editing to activate per account. Uses Google Authenticator. --- app/Contracts/UserInterface.php | 1 + app/Http/RequestHandlers/AccountUpdate.php | 8 +- app/Http/RequestHandlers/LoginAction.php | 18 +- app/Http/RequestHandlers/RegisterAction.php | 7 +- app/Http/RequestHandlers/SetupWizard.php | 4 +- app/Http/RequestHandlers/UserAddAction.php | 3 +- app/Http/RequestHandlers/UserEditAction.php | 2 + app/Schema/Migration0.php | 1 + app/Schema/SeedUserTable.php | 1 + app/Services/UserService.php | 6 +- app/User.php | 53 ++++ composer.json | 2 + composer.lock | 268 +++++++++++++++++- public/js/totp.js | 32 +++ resources/views/admin/users-edit.phtml | 1 + resources/views/edit-account-page.phtml | 18 +- resources/views/layouts/default.phtml | 1 + resources/views/login-page.phtml | 9 +- .../views/setup/step-5-administrator.phtml | 2 + 19 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 public/js/totp.js diff --git a/app/Contracts/UserInterface.php b/app/Contracts/UserInterface.php index 73aef7239fd..6e73f0a2df6 100644 --- a/app/Contracts/UserInterface.php +++ b/app/Contracts/UserInterface.php @@ -32,6 +32,7 @@ interface UserInterface public const string PREF_IS_EMAIL_VERIFIED = 'verified'; public const string PREF_IS_VISIBLE_ONLINE = 'visibleonline'; public const string PREF_LANGUAGE = 'language'; + public const string PREF_IS_STATUS_MFA = 'statusmfa'; public const string PREF_NEW_ACCOUNT_COMMENT = 'comment'; public const string PREF_TIMESTAMP_REGISTERED = 'reg_timestamp'; public const string PREF_TIMESTAMP_ACTIVE = 'sessiontime'; diff --git a/app/Http/RequestHandlers/AccountUpdate.php b/app/Http/RequestHandlers/AccountUpdate.php index ae73aaff771..c0ba3857b96 100644 --- a/app/Http/RequestHandlers/AccountUpdate.php +++ b/app/Http/RequestHandlers/AccountUpdate.php @@ -67,15 +67,20 @@ public function handle(ServerRequestInterface $request): ResponseInterface $language = Validator::parsedBody($request)->string('language'); $real_name = Validator::parsedBody($request)->string('real_name'); $password = Validator::parsedBody($request)->string('password'); + $secret = Validator::parsedBody($request)->string('secret'); $time_zone = Validator::parsedBody($request)->string('timezone'); $user_name = Validator::parsedBody($request)->string('user_name'); $visible_online = Validator::parsedBody($request)->boolean('visible-online', false); + $status_mfa = Validator::parsedBody($request)->boolean('status-mfa', '0'); // Change the password if ($password !== '') { $user->setPassword($password); } - + // Change the secret + if ($secret !== '' || $status_mfa === false) { + $user->setSecret($secret); + } // Change the username if ($user_name !== $user->userName()) { if ($this->user_service->findByUserName($user_name) === null) { @@ -99,6 +104,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $user->setPreference(UserInterface::PREF_LANGUAGE, $language); $user->setPreference(UserInterface::PREF_TIME_ZONE, $time_zone); $user->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, (string) $visible_online); + $user->setPreference(UserInterface::PREF_IS_STATUS_MFA, (string) $status_mfa); if ($tree instanceof Tree) { $default_xref = Validator::parsedBody($request)->string('default-xref'); diff --git a/app/Http/RequestHandlers/LoginAction.php b/app/Http/RequestHandlers/LoginAction.php index a320524c100..087a21b1fee 100644 --- a/app/Http/RequestHandlers/LoginAction.php +++ b/app/Http/RequestHandlers/LoginAction.php @@ -68,10 +68,11 @@ public function handle(ServerRequestInterface $request): ResponseInterface $default_url = route(HomePage::class); $username = Validator::parsedBody($request)->string('username'); $password = Validator::parsedBody($request)->string('password'); + $code2fa = Validator::parsedBody($request)->string('code2fa'); $url = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url); try { - $this->doLogin($username, $password); + $this->doLogin($username, $password, $code2fa); if (Auth::isAdmin() && $this->upgrade_service->isUpgradeAvailable()) { FlashMessages::addMessage(I18N::translate('A new version of webtrees is available.') . ' ' . I18N::translate('Upgrade to webtrees %s.', '' . $this->upgrade_service->latestVersion() . '') . ''); @@ -96,11 +97,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface * * @param string $username * @param string $password + * @param string $code2fa * * @return void * @throws Exception */ - private function doLogin(string $username, #[\SensitiveParameter] string $password): void + private function doLogin(string $username, #[\SensitiveParameter] string $password, string $code2fa): void { if ($_COOKIE === []) { Log::addAuthenticationLog('Login failed (no session cookies): ' . $username); @@ -128,7 +130,17 @@ private function doLogin(string $username, #[\SensitiveParameter] string $passwo Log::addAuthenticationLog('Login failed (not approved by admin): ' . $username); throw new Exception(I18N::translate('This account has not been approved. Please wait for an administrator to approve it.')); } - + if ($user->getPreference(UserInterface::PREF_IS_STATUS_MFA) !== '') { + # covers scenario where 2fa not enabled by user + if($code2fa != '') { + if (!$user->check2FAcode($code2fa)) { + throw new Exception(I18N::translate('2FA code does not match. Please try again.')); + } + } + else { + throw new Exception(I18N::translate('2FA code must be entered as you have 2FA authentication enabled. Please try again.')); + } + } Auth::login($user); Log::addAuthenticationLog('Login: ' . Auth::user()->userName() . '/' . Auth::user()->realName()); Auth::user()->setPreference(UserInterface::PREF_TIMESTAMP_ACTIVE, (string) time()); diff --git a/app/Http/RequestHandlers/RegisterAction.php b/app/Http/RequestHandlers/RegisterAction.php index 1b409a377ee..8cb4451b0ae 100644 --- a/app/Http/RequestHandlers/RegisterAction.php +++ b/app/Http/RequestHandlers/RegisterAction.php @@ -96,13 +96,14 @@ public function handle(ServerRequestInterface $request): ResponseInterface $password = Validator::parsedBody($request)->string('password'); $realname = Validator::parsedBody($request)->string('realname'); $username = Validator::parsedBody($request)->string('username'); + $secret = Validator::parsedBody($request)->string('secret'); try { if ($this->captcha_service->isRobot($request)) { throw new Exception(I18N::translate('Please try again.')); } - $this->doValidateRegistration($request, $username, $email, $realname, $comments, $password); + $this->doValidateRegistration($request, $username, $email, $realname, $comments, $password, $secret); Session::forget('register_comments'); Session::forget('register_email'); @@ -123,7 +124,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface Log::addAuthenticationLog('User registration requested for: ' . $username); - $user = $this->user_service->create($username, $realname, $email, $password); + $user = $this->user_service->create($username, $realname, $email, $password, $secret); $token = Str::random(32); $user->setPreference(UserInterface::PREF_LANGUAGE, I18N::languageTag()); @@ -135,6 +136,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $user->setPreference(UserInterface::PREF_CONTACT_METHOD, MessageService::CONTACT_METHOD_INTERNAL_AND_EMAIL); $user->setPreference(UserInterface::PREF_NEW_ACCOUNT_COMMENT, $comments); $user->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1'); + $user->setPreference(UserInterface::PREF_IS_STATUS_MFA, '0'); $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, ''); $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, ''); $user->setPreference(UserInterface::PREF_TIMESTAMP_ACTIVE, '0'); @@ -246,6 +248,7 @@ private function doValidateRegistration( string $username, string $email, string $realname, + string $secret, string $comments, #[\SensitiveParameter] string $password ): void { diff --git a/app/Http/RequestHandlers/SetupWizard.php b/app/Http/RequestHandlers/SetupWizard.php index cb5eabfdd64..1b942786cd1 100644 --- a/app/Http/RequestHandlers/SetupWizard.php +++ b/app/Http/RequestHandlers/SetupWizard.php @@ -82,6 +82,7 @@ class SetupWizard implements RequestHandlerInterface 'wtuser' => '', 'wtpass' => '', 'wtemail' => '', + 'wtsecret' => '', ]; private const array DEFAULT_PORTS = [ @@ -323,9 +324,10 @@ private function createConfigFile(array $data): void } // Create the user if ($admin === null) { - $admin = $this->user_service->create($data['wtuser'], $data['wtname'], $data['wtemail'], $data['wtpass']); + $admin = $this->user_service->create($data['wtuser'], $data['wtname'], $data['wtemail'], $data['wtpass'], $data['wtsecret']); $admin->setPreference(UserInterface::PREF_LANGUAGE, $data['lang']); $admin->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1'); + $admin->setPreference(UserInterface::PREF_IS_STATUS_MFA, '0'); } else { $admin->setPassword($_POST['wtpass']); } diff --git a/app/Http/RequestHandlers/UserAddAction.php b/app/Http/RequestHandlers/UserAddAction.php index df4cf7dd597..b05d540a9d9 100644 --- a/app/Http/RequestHandlers/UserAddAction.php +++ b/app/Http/RequestHandlers/UserAddAction.php @@ -59,6 +59,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $real_name = Validator::parsedBody($request)->string('real_name'); $email = Validator::parsedBody($request)->string('email'); $password = Validator::parsedBody($request)->string('password'); + $secret = ""; $errors = false; @@ -82,7 +83,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface return redirect($url); } - $new_user = $this->user_service->create($username, $real_name, $email, $password); + $new_user = $this->user_service->create($username, $real_name, $email, $password, $secret); $new_user->setPreference(UserInterface::PREF_IS_EMAIL_VERIFIED, '1'); $new_user->setPreference(UserInterface::PREF_IS_ACCOUNT_APPROVED, '1'); $new_user->setPreference(UserInterface::PREF_LANGUAGE, I18N::languageTag()); diff --git a/app/Http/RequestHandlers/UserEditAction.php b/app/Http/RequestHandlers/UserEditAction.php index f36548097a0..15f3b37b20e 100644 --- a/app/Http/RequestHandlers/UserEditAction.php +++ b/app/Http/RequestHandlers/UserEditAction.php @@ -75,6 +75,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $real_name = Validator::parsedBody($request)->string('real_name'); $email = Validator::parsedBody($request)->string('email'); $password = Validator::parsedBody($request)->string('password'); + $secret = Validator::parsedBody($request)->string('secret'); $theme = Validator::parsedBody($request)->string('theme'); $language = Validator::parsedBody($request)->string('language'); $timezone = Validator::parsedBody($request)->string('timezone'); @@ -84,6 +85,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $canadmin = Validator::parsedBody($request)->boolean('canadmin', false); $visible_online = Validator::parsedBody($request)->boolean('visible-online', false); $verified = Validator::parsedBody($request)->boolean('verified', false); + $status_mfa = Validator::parsedBody($request)->boolean('status-mfa', false); $approved = Validator::parsedBody($request)->boolean('approved', false); $edit_user = $this->user_service->find($user_id); diff --git a/app/Schema/Migration0.php b/app/Schema/Migration0.php index fc81373dc73..a7ae811cdfe 100644 --- a/app/Schema/Migration0.php +++ b/app/Schema/Migration0.php @@ -61,6 +61,7 @@ public function upgrade(): void $table->string('real_name', 64); $table->string('email', 64); $table->string('password', 128); + $table->string('secret', 128); $table->unique('user_name'); $table->unique('email'); diff --git a/app/Schema/SeedUserTable.php b/app/Schema/SeedUserTable.php index 4468be7b41c..d4dd5fe6f78 100644 --- a/app/Schema/SeedUserTable.php +++ b/app/Schema/SeedUserTable.php @@ -34,6 +34,7 @@ public function run(): void 'real_name' => 'DEFAULT_USER', 'email' => 'DEFAULT_USER', 'password' => 'DEFAULT_USER', + 'secret' => 'DEFAULT_USER', ]); }); } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 466b6ab7d21..2a6be1eaa3a 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -309,21 +309,23 @@ public function allLoggedIn(): Collection * @param string $real_name * @param string $email * @param string $password + * @param string $secret * * @return User */ - public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password): User + public function create(string $user_name, string $real_name, string $email, #[\SensitiveParameter] string $password, string $secret): User { DB::table('user')->insert([ 'user_name' => $user_name, 'real_name' => $real_name, 'email' => $email, 'password' => password_hash($password, PASSWORD_DEFAULT), + 'secret' => $secret, ]); $user_id = DB::lastInsertId(); - return new User($user_id, $user_name, $real_name, $email); + return new User($user_id, $user_name, $real_name, $email, $secret); } /** diff --git a/app/User.php b/app/User.php index 313533cc1e0..68f39241963 100644 --- a/app/User.php +++ b/app/User.php @@ -21,6 +21,8 @@ use Closure; use Fisharebest\Webtrees\Contracts\UserInterface; +use PragmaRX\Google2FA\Google2FA; +use chillerlan\QRCode\QRCode; use function is_string; @@ -110,6 +112,21 @@ public function realName(): string { return $this->real_name; } + /** + * Generate a QR code image based on 2FA secret and return both. + * + * @return array + */ + + public function genQRcode(): array + { + $qrinfo = array(); + $google2fa = new Google2FA(); + $qrinfo['secret'] = $google2fa->generateSecretKey(); + $data = 'otpauth://totp/' . $this->user_id . '?secret=' . $qrinfo['secret'] . '&issuer=' . $_SERVER['SERVER_NAME']; + $qrinfo['qrcode'] = (new QRCode)->render($data); + return $qrinfo; + } /** * Set the real name of this user. @@ -243,6 +260,42 @@ public function checkPassword(#[\SensitiveParameter] string $password): bool return false; } + /** + * Set the Secret of this user. + * + * @param string $secret + * + * @return User + */ + public function setSecret(#[\SensitiveParameter] string $secret): User + { + DB::table('user') + ->where('user_id', '=', $this->user_id) + ->update([ + 'secret' => $secret, + ]); + + return $this; + } + + /** + * Validate a supplied 2fa code + * + * @param string $code2fa + * + * @return bool + */ + public function check2facode(string $code2fa): bool + { + $secret = DB::table('user') + ->where('user_id', '=', $this->id()) + ->value('secret'); + $google2fa = new Google2FA; + if($google2fa->verifyKey($secret, $code2fa)) { + return true; + } + return false; + } /** * A closure which will create an object from a database row. diff --git a/composer.json b/composer.json index 1c948e5f4b4..6938ce44560 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "ext-session": "*", "ext-xml": "*", "aura/router": "3.4.2", + "chillerlan/php-qrcode": "4.4.0", "ezyang/htmlpurifier": "4.18.0", "fig/http-message-util": "1.1.5", "fisharebest/algorithm": "1.6.0", @@ -63,6 +64,7 @@ "nesbot/carbon": "3.10.0", "nyholm/psr7": "1.8.2", "nyholm/psr7-server": "1.1.0", + "pragmarx/google2fa": "^8.0", "psr/cache": "3.0.0", "psr/http-message": "2.0", "psr/http-server-handler": "1.0.2", diff --git a/composer.lock b/composer.lock index 884d5aa7606..714f29ad838 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": "b53f0881813f367ca62121b89cc97800", + "content-hash": "787d5536a02c50281feffe479403939b", "packages": [ { "name": "aura/router", @@ -188,6 +188,153 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "chillerlan/php-qrcode", + "version": "4.4.0", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-qrcode.git", + "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/52889cd7ab1b78e6a345edafe24aa74bc5becc08", + "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08", + "shasum": "" + }, + "require": { + "chillerlan/php-settings-container": "^2.1.4 || ^3.1", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phan/phan": "^5.4", + "phpmd/phpmd": "^2.13", + "phpunit/phpunit": "^9.6", + "setasign/fpdf": "^1.8.2", + "squizlabs/php_codesniffer": "^3.7" + }, + "suggest": { + "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\QRCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kazuhiko Arase", + "homepage": "https://github.com/kazuhikoarase" + }, + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + }, + { + "name": "Contributors", + "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" + } + ], + "description": "A QR code generator with a user friendly API. PHP 7.4+", + "homepage": "https://github.com/chillerlan/php-qrcode", + "keywords": [ + "phpqrcode", + "qr", + "qr code", + "qrcode", + "qrcode-generator" + ], + "support": { + "issues": "https://github.com/chillerlan/php-qrcode/issues", + "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2023-11-23T23:53:20+00:00" + }, + { + "name": "chillerlan/php-settings-container", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-settings-container.git", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\Settings\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + } + ], + "description": "A container class for immutable settings objects. Not a DI container.", + "homepage": "https://github.com/chillerlan/php-settings-container", + "keywords": [ + "Settings", + "configuration", + "container", + "helper" + ], + "support": { + "issues": "https://github.com/chillerlan/php-settings-container/issues", + "source": "https://github.com/chillerlan/php-settings-container" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2024-07-16T11:13:48+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2714,6 +2861,125 @@ ], "time": "2023-11-08T09:30:43+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + }, + "time": "2024-09-05T11:56:40+00:00" + }, { "name": "psr/cache", "version": "3.0.0", diff --git a/public/js/totp.js b/public/js/totp.js new file mode 100644 index 00000000000..37dbb869e2f --- /dev/null +++ b/public/js/totp.js @@ -0,0 +1,32 @@ +$( document ).ready(function() { +// resize the qr code and hide as default in edit user page + $('div#qrcode').css('maxWidth', '300px'); + $('div#qrcode').hide(); + +// show a link to get another qr code if 2fa enabled + if($('input#status-mfa-1').is(':checked')) { + $('input#status-mfa-1').parent().append("  - (Click to generate new QR code) ") + } + +// click to get new qr code and secret +$("a#getnewqr").click(function(){ + $('div#qrcode').show(); + $("a#getnewqr").hide(); + $('input#secret').val($('input#newsecret').val()) +}); +// deal with toggling of 2fa setting to ensure no secret saved if no 2fa required but the secret associated with any generated qr code is saved. + $('input#status-mfa-1').change(function() { + if(this.checked) { + $(this).parent().append("  - (Click to generate new QR code) ") + $('div#qrcode').show(); + $('input#secret').val($('input#newsecret').val()) + } + else { + $('div#qrcode').hide(); + $("a#getnewqr").hide(); + $('input#secret').val(''); + } + }); +}); + + diff --git a/resources/views/admin/users-edit.phtml b/resources/views/admin/users-edit.phtml index 02c5aad101a..2ba1c7e5b40 100644 --- a/resources/views/admin/users-edit.phtml +++ b/resources/views/admin/users-edit.phtml @@ -35,6 +35,7 @@ use Illuminate\Support\Collection;
+
diff --git a/resources/views/edit-account-page.phtml b/resources/views/edit-account-page.phtml index 2b9d1e0552b..dab7085e999 100644 --- a/resources/views/edit-account-page.phtml +++ b/resources/views/edit-account-page.phtml @@ -153,7 +153,23 @@ use Fisharebest\Webtrees\Tree;
- +
+ + + +
+ I18N::translate('Enable or disable 2FA status'), 'name' => 'status-mfa', 'checked' => (bool) $user->getPreference(UserInterface::PREF_IS_STATUS_MFA)]) ?> +
+ +
+
+
+
+ genQRcode(); ?> + + + QR Code +
diff --git a/resources/views/layouts/default.phtml b/resources/views/layouts/default.phtml index c01114ee6ea..d87ba81da7d 100644 --- a/resources/views/layouts/default.phtml +++ b/resources/views/layouts/default.phtml @@ -142,6 +142,7 @@ use Psr\Http\Message\ServerRequestInterface; +