Add two-factor authentication settings
- Add settings aside menu entry - Add two-factor authentication documentation
This commit is contained in:
parent
d7e9b91181
commit
8e885f5b97
10 changed files with 600 additions and 0 deletions
60
doc/Two-Factor-Authentication.md
Normal file
60
doc/Two-Factor-Authentication.md
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# Configuring two-factor authentication
|
||||||
|
|
||||||
|
* [Home](help)
|
||||||
|
|
||||||
|
You can configure two-factor authentication using a mobile app.
|
||||||
|
A time-based one-time password (TOTP) application automatically generates an authentication code that changes after a certain period of time.
|
||||||
|
|
||||||
|
**Tip**: To configure authentication via TOTP on multiple devices, during setup, scan the QR code using each device at the same time.
|
||||||
|
If 2FA is already enabled and you want to add another device, you must re-configure 2FA from your security settings.
|
||||||
|
|
||||||
|
## Enabling two-factor authentication
|
||||||
|
|
||||||
|
### 1. Download an authenticator app
|
||||||
|
|
||||||
|
Any authenticator app should work with Friendica.
|
||||||
|
Notheless, we recommend:
|
||||||
|
|
||||||
|
- For iOS, [Matt Rubin's MIT-licensed Authenticator app](https://mattrubin.me/authenticator).
|
||||||
|
- For Android, [andOTP](https://github.com/andOTP/andOTP).
|
||||||
|
|
||||||
|
### 2. Record your one-use recovery codes
|
||||||
|
|
||||||
|
From your [two-factor authentication user settings](/settings/2fa), enter your password and click on "Enable two-factor authentication".
|
||||||
|
|
||||||
|
You will be presented with a list of one-use recovery codes.
|
||||||
|
Please save those in the same place you are saving your Friendica password (ideally, in a password manager like [KeePass](https://keepass.info)).
|
||||||
|
|
||||||
|
When you're done, click on "Next".
|
||||||
|
|
||||||
|
### 3. Setup your authenticator app
|
||||||
|
|
||||||
|
You have three methods to setup your authenticator app:
|
||||||
|
|
||||||
|
1. Scan the QR Code with your device camera.
|
||||||
|
This will automatically configure your account on the app.
|
||||||
|
2. Click/tap on the provided **totp://** URl.
|
||||||
|
Ideally your authenticator app should be called with this URL and set up your account.
|
||||||
|
3. Enter your account settings manually.
|
||||||
|
Friendica is using default settings for token type, code digit count and hashing algorithm but you may be required to enter them in your app.
|
||||||
|
|
||||||
|
**Tip**: If you have multiple devices, configure them all at this point.
|
||||||
|
|
||||||
|
Then verify your app is correctly configured by submitting a code provided by your app.
|
||||||
|
This will conclude two-factor authentication configuration.
|
||||||
|
|
||||||
|
**Note:** If you leave this screen at any point without having submitted a verification code, two-factor authentication won't be enabled on your account.
|
||||||
|
To complete the configuration, just come back to your [two-factor authentication user settings](/settings/2fa) and click on "Finish configuration" after entering your current password.
|
||||||
|
|
||||||
|
## Disabling two-factor authentication
|
||||||
|
|
||||||
|
You can disable two-factor authentication at any time by going to your [two-factor authentication user settings](/settings/2fa) and click on "Disable two-factor authentication" after entering your current password.
|
||||||
|
|
||||||
|
You should remove your Friendica account from your authenticator app as it won't work again even if you reenable two-factor authentication.
|
||||||
|
In this case you will have to configure your authenticator app again using the process above.
|
||||||
|
|
||||||
|
## Managing your one-time recovery codes
|
||||||
|
|
||||||
|
When two-factor authentication is enabled, you can show your recovery codes, including the ones you've already used.
|
||||||
|
|
||||||
|
You can freely regenerate a new set of fresh recovery codes, just be sure to replace the previous ones where you saved them as they won't be active anymore.
|
|
@ -67,6 +67,13 @@ function settings_init(App $a)
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Two-factor authentication'),
|
||||||
|
'url' => 'settings/2fa',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''),
|
||||||
|
'accesskey' => 'o',
|
||||||
|
];
|
||||||
|
|
||||||
$tabs[] = [
|
$tabs[] = [
|
||||||
'label' => L10n::t('Profiles'),
|
'label' => L10n::t('Profiles'),
|
||||||
'url' => 'profiles',
|
'url' => 'profiles',
|
||||||
|
|
|
@ -188,6 +188,14 @@ class Router
|
||||||
$collector->addRoute(['GET'], '/{sub1}/{url}' , Module\Proxy::class);
|
$collector->addRoute(['GET'], '/{sub1}/{url}' , Module\Proxy::class);
|
||||||
$collector->addRoute(['GET'], '/{sub1}/{sub2}/{url}' , Module\Proxy::class);
|
$collector->addRoute(['GET'], '/{sub1}/{sub2}/{url}' , Module\Proxy::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->routeCollector->addGroup('/settings', function (RouteCollector $collector) {
|
||||||
|
$collector->addGroup('/2fa', function (RouteCollector $collector) {
|
||||||
|
$collector->addRoute(['GET', 'POST'], '[/]' , Module\Settings\TwoFactor\Index::class);
|
||||||
|
$collector->addRoute(['GET', 'POST'], '/recovery' , Module\Settings\TwoFactor\Recovery::class);
|
||||||
|
$collector->addRoute(['GET', 'POST'], '/verify' , Module\Settings\TwoFactor\Verify::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
$this->routeCollector->addRoute(['GET', 'POST'], '/register', Module\Register::class);
|
$this->routeCollector->addRoute(['GET', 'POST'], '/register', Module\Register::class);
|
||||||
$this->routeCollector->addRoute(['GET'], '/robots.txt', Module\RobotsTxt::class);
|
$this->routeCollector->addRoute(['GET'], '/robots.txt', Module\RobotsTxt::class);
|
||||||
$this->routeCollector->addRoute(['GET'], '/rsd.xml', Module\ReallySimpleDiscovery::class);
|
$this->routeCollector->addRoute(['GET'], '/rsd.xml', Module\ReallySimpleDiscovery::class);
|
||||||
|
|
110
src/Module/BaseSettingsModule.php
Normal file
110
src/Module/BaseSettingsModule.php
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Friendica\Module;
|
||||||
|
|
||||||
|
use Friendica\BaseModule;
|
||||||
|
use Friendica\Content\Feature;
|
||||||
|
use Friendica\Core\L10n;
|
||||||
|
use Friendica\Core\Renderer;
|
||||||
|
|
||||||
|
class BaseSettingsModule extends BaseModule
|
||||||
|
{
|
||||||
|
public static function content()
|
||||||
|
{
|
||||||
|
$a = self::getApp();
|
||||||
|
|
||||||
|
$tpl = Renderer::getMarkupTemplate('settings/head.tpl');
|
||||||
|
$a->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
|
||||||
|
'$ispublic' => L10n::t('everybody')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tabs = [];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Account'),
|
||||||
|
'url' => 'settings',
|
||||||
|
'selected' => (($a->argc == 1) && ($a->argv[0] === 'settings') ? 'active' : ''),
|
||||||
|
'accesskey' => 'o',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Two-factor authentication'),
|
||||||
|
'url' => 'settings/2fa',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''),
|
||||||
|
'accesskey' => 'o',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Profiles'),
|
||||||
|
'url' => 'profiles',
|
||||||
|
'selected' => (($a->argc == 1) && ($a->argv[0] === 'profiles') ? 'active' : ''),
|
||||||
|
'accesskey' => 'p',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (Feature::get()) {
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Additional features'),
|
||||||
|
'url' => 'settings/features',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === 'features') ? 'active' : ''),
|
||||||
|
'accesskey' => 't',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Display'),
|
||||||
|
'url' => 'settings/display',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === 'display') ? 'active' : ''),
|
||||||
|
'accesskey' => 'i',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Social Networks'),
|
||||||
|
'url' => 'settings/connectors',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === 'connectors') ? 'active' : ''),
|
||||||
|
'accesskey' => 'w',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Addons'),
|
||||||
|
'url' => 'settings/addon',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === 'addon') ? 'active' : ''),
|
||||||
|
'accesskey' => 'l',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Delegations'),
|
||||||
|
'url' => 'delegate',
|
||||||
|
'selected' => (($a->argc == 1) && ($a->argv[0] === 'delegate') ? 'active' : ''),
|
||||||
|
'accesskey' => 'd',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Connected apps'),
|
||||||
|
'url' => 'settings/oauth',
|
||||||
|
'selected' => (($a->argc > 1) && ($a->argv[1] === 'oauth') ? 'active' : ''),
|
||||||
|
'accesskey' => 'b',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Export personal data'),
|
||||||
|
'url' => 'uexport',
|
||||||
|
'selected' => (($a->argc == 1) && ($a->argv[0] === 'uexport') ? 'active' : ''),
|
||||||
|
'accesskey' => 'e',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tabs[] = [
|
||||||
|
'label' => L10n::t('Remove account'),
|
||||||
|
'url' => 'removeme',
|
||||||
|
'selected' => (($a->argc == 1) && ($a->argv[0] === 'removeme') ? 'active' : ''),
|
||||||
|
'accesskey' => 'r',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
$tabtpl = Renderer::getMarkupTemplate("generic_links_widget.tpl");
|
||||||
|
$a->page['aside'] = Renderer::replaceMacros($tabtpl, [
|
||||||
|
'$title' => L10n::t('Settings'),
|
||||||
|
'$class' => 'settings-widget',
|
||||||
|
'$items' => $tabs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
111
src/Module/Settings/TwoFactor/Index.php
Normal file
111
src/Module/Settings/TwoFactor/Index.php
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Friendica\Module\Settings\TwoFactor;
|
||||||
|
|
||||||
|
|
||||||
|
use Friendica\Core\L10n;
|
||||||
|
use Friendica\Core\PConfig;
|
||||||
|
use Friendica\Core\Renderer;
|
||||||
|
use Friendica\Core\Session;
|
||||||
|
use Friendica\Model\TwoFactorRecoveryCode;
|
||||||
|
use Friendica\Model\User;
|
||||||
|
use Friendica\Module\BaseSettingsModule;
|
||||||
|
use Friendica\Module\Login;
|
||||||
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
|
|
||||||
|
class Index extends BaseSettingsModule
|
||||||
|
{
|
||||||
|
public static function post()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa', 'settings_2fa');
|
||||||
|
|
||||||
|
try {
|
||||||
|
User::getIdFromPasswordAuthentication(local_user(), defaults($_POST, 'password', ''));
|
||||||
|
|
||||||
|
$has_secret = (bool) PConfig::get(local_user(), '2fa', 'secret');
|
||||||
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
switch (defaults($_POST, 'action', '')) {
|
||||||
|
case 'enable':
|
||||||
|
if (!$has_secret && !$verified) {
|
||||||
|
$Google2FA = new Google2FA();
|
||||||
|
|
||||||
|
PConfig::set(local_user(), '2fa', 'secret', $Google2FA->generateSecretKey(32));
|
||||||
|
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'disable':
|
||||||
|
if ($has_secret) {
|
||||||
|
TwoFactorRecoveryCode::deleteForUser(local_user());
|
||||||
|
PConfig::delete(local_user(), '2fa', 'secret');
|
||||||
|
PConfig::delete(local_user(), '2fa', 'verified');
|
||||||
|
Session::remove('2fa');
|
||||||
|
|
||||||
|
notice(L10n::t('Two-factor authentication successfully disabled.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'recovery':
|
||||||
|
if ($has_secret) {
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'configure':
|
||||||
|
if (!$verified) {
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/verify?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
notice(L10n::t('Wrong Password'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function content()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return Login::form('settings/2fa');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::content();
|
||||||
|
|
||||||
|
$has_secret = (bool) PConfig::get(local_user(), '2fa', 'secret');
|
||||||
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/index.tpl'), [
|
||||||
|
'$form_security_token' => self::getFormSecurityToken('settings_2fa'),
|
||||||
|
'$title' => L10n::t('Two-factor authentication'),
|
||||||
|
'$help_label' => L10n::t('Help'),
|
||||||
|
|
||||||
|
'$status_title' => L10n::t('Status'),
|
||||||
|
'$message' => L10n::t('<p>Use an application on a mobile device to get two-factor authentication codes when prompted on login.</p>'),
|
||||||
|
|
||||||
|
'$has_secret' => $has_secret,
|
||||||
|
'$verified' => $verified,
|
||||||
|
|
||||||
|
'$auth_app_label' => L10n::t('Authenticator app'),
|
||||||
|
'$app_status' => $has_secret ? $verified ? L10n::t('Configured') : L10n::t('Not Configured') : L10n::t('Disabled'),
|
||||||
|
'$not_configured_message' => L10n::t('<p>You haven\'t finished configuring your authenticator app.</p>'),
|
||||||
|
'$configured_message' => L10n::t('<p>Your authenticator app is correctly configured.</p>'),
|
||||||
|
|
||||||
|
'$recovery_codes_title' => L10n::t('Recovery codes'),
|
||||||
|
'$recovery_codes_remaining' => L10n::t('Remaining valid codes'),
|
||||||
|
'$recovery_codes_count' => TwoFactorRecoveryCode::countValidForUser(local_user()),
|
||||||
|
'$recovery_codes_message' => L10n::t('<p>These one-use codes can replace an authenticator app code in case you have lost access to it.</p>'),
|
||||||
|
|
||||||
|
'$action_title' => L10n::t('Actions'),
|
||||||
|
'$password' => ['password', L10n::t('Current password:'), '', L10n::t('You need to provide your current password to change two-factor authentication settings.'), 'required', 'autofocus'],
|
||||||
|
|
||||||
|
'$enable_label' => L10n::t('Enable two-factor authentication'),
|
||||||
|
'$disable_label' => L10n::t('Disable two-factor authentication'),
|
||||||
|
'$recovery_codes_label' => L10n::t('Show recovery codes'),
|
||||||
|
'$configure_label' => L10n::t('Finish app configuration'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
86
src/Module/Settings/TwoFactor/Recovery.php
Normal file
86
src/Module/Settings/TwoFactor/Recovery.php
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Friendica\Module\Settings\TwoFactor;
|
||||||
|
|
||||||
|
|
||||||
|
use Friendica\Core\L10n;
|
||||||
|
use Friendica\Core\PConfig;
|
||||||
|
use Friendica\Core\Renderer;
|
||||||
|
use Friendica\Model\TwoFactorRecoveryCode;
|
||||||
|
use Friendica\Module\BaseSettingsModule;
|
||||||
|
use Friendica\Module\Login;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* // Page 3: 2FA enabled but not verified, show recovery codes
|
||||||
|
*
|
||||||
|
* @package Friendica\Module\TwoFactor
|
||||||
|
*/
|
||||||
|
class Recovery extends BaseSettingsModule
|
||||||
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = PConfig::get(local_user(), '2fa', 'secret');
|
||||||
|
|
||||||
|
if (!$secret) {
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::checkFormSecurityToken('settings_2fa_password', 't')) {
|
||||||
|
notice(L10n::t('Please enter your password to access this page.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function post()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($_POST['action'])) {
|
||||||
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa/recovery', 'settings_2fa_recovery');
|
||||||
|
|
||||||
|
if ($_POST['action'] == 'regenerate') {
|
||||||
|
TwoFactorRecoveryCode::regenerateForUser(local_user());
|
||||||
|
notice(L10n::t('New recovery codes successfully generated.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function content()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return Login::form('settings/2fa/recovery');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::content();
|
||||||
|
|
||||||
|
if (!TwoFactorRecoveryCode::countValidForUser(local_user())) {
|
||||||
|
TwoFactorRecoveryCode::generateForUser(local_user());
|
||||||
|
}
|
||||||
|
|
||||||
|
$recoveryCodes = TwoFactorRecoveryCode::getListForUser(local_user());
|
||||||
|
|
||||||
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/recovery.tpl'), [
|
||||||
|
'$form_security_token' => self::getFormSecurityToken('settings_2fa_recovery'),
|
||||||
|
'$password_security_token' => self::getFormSecurityToken('settings_2fa_password'),
|
||||||
|
'$title' => L10n::t('Two-factor recovery codes'),
|
||||||
|
'$help_label' => L10n::t('Help'),
|
||||||
|
'$message' => L10n::t('<p>Recovery codes can be used to access your account in the event you lose access to your device and cannot receive two-factor authentication codes.</p><p><strong>Put these in a safe spot!</strong> If you lose your device and don’t have the recovery codes you will lose access to your account.</p>'),
|
||||||
|
'$recovery_codes' => $recoveryCodes,
|
||||||
|
'$password' => ['password', L10n::t('Please enter your password for verification:'), '', L10n::t('You need to provide your current password to enable or disable two-factor authentication.'), 'required', 'autofocus'],
|
||||||
|
'$regenerate_message' => L10n::t('When you generate new recovery codes, you must copy the new codes. Your old codes won’t work anymore.'),
|
||||||
|
'$regenerate_label' => L10n::t('Generate new recovery codes'),
|
||||||
|
'$verified' => $verified,
|
||||||
|
'$verify_label' => L10n::t('Next: Verification'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
129
src/Module/Settings/TwoFactor/Verify.php
Normal file
129
src/Module/Settings/TwoFactor/Verify.php
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Friendica\Module\Settings\TwoFactor;
|
||||||
|
|
||||||
|
|
||||||
|
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||||
|
use BaconQrCode\Renderer\ImageRenderer;
|
||||||
|
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||||
|
use BaconQrCode\Writer;
|
||||||
|
use Friendica\BaseModule;
|
||||||
|
use Friendica\Core\L10n;
|
||||||
|
use Friendica\Core\PConfig;
|
||||||
|
use Friendica\Core\Renderer;
|
||||||
|
use Friendica\Core\Session;
|
||||||
|
use Friendica\Module\BaseSettingsModule;
|
||||||
|
use Friendica\Module\Login;
|
||||||
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* // Page 4: 2FA enabled but not verified, QR code and verification
|
||||||
|
*
|
||||||
|
* @package Friendica\Module\TwoFactor\Settings
|
||||||
|
*/
|
||||||
|
class Verify extends BaseSettingsModule
|
||||||
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = PConfig::get(local_user(), '2fa', 'secret');
|
||||||
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
if ($secret && $verified) {
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::checkFormSecurityToken('settings_2fa_password', 't')) {
|
||||||
|
notice(L10n::t('Please enter your password to access this page.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function post()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaults($_POST, 'action', null) == 'verify') {
|
||||||
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa/verify', 'settings_2fa_verify');
|
||||||
|
|
||||||
|
$google2fa = new Google2FA();
|
||||||
|
|
||||||
|
$valid = $google2fa->verifyKey(PConfig::get(local_user(), '2fa', 'secret'), defaults($_POST, 'verify_code', ''));
|
||||||
|
|
||||||
|
if ($valid) {
|
||||||
|
PConfig::set(local_user(), '2fa', 'verified', true);
|
||||||
|
Session::set('2fa', true);
|
||||||
|
|
||||||
|
notice(L10n::t('Two-factor authentication successfully activated.'));
|
||||||
|
|
||||||
|
self::getApp()->internalRedirect('settings/2fa');
|
||||||
|
} else {
|
||||||
|
notice(L10n::t('Invalid code, please retry.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function content()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return Login::form('settings/2fa/verify');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::content();
|
||||||
|
|
||||||
|
$company = 'Friendica';
|
||||||
|
$holder = Session::get('my_address');
|
||||||
|
$secret = PConfig::get(local_user(), '2fa', 'secret');
|
||||||
|
|
||||||
|
$otpauthUrl = (new Google2FA())->getQRCodeUrl($company, $holder, $secret);
|
||||||
|
|
||||||
|
$renderer = (new \BaconQrCode\Renderer\Image\Svg())
|
||||||
|
->setHeight(256)
|
||||||
|
->setWidth(256);
|
||||||
|
|
||||||
|
$writer = new Writer($renderer);
|
||||||
|
|
||||||
|
$qrcode_image = str_replace('<?xml version="1.0" encoding="UTF-8"?>', '', $writer->writeString($otpauthUrl));
|
||||||
|
|
||||||
|
$shortOtpauthUrl = explode('?', $otpauthUrl)[0];
|
||||||
|
|
||||||
|
$manual_message = L10n::t('<p>Or you can submit the authentication settings manually:</p>
|
||||||
|
<dl>
|
||||||
|
<dt>Issuer</dt>
|
||||||
|
<dd>%s</dd>
|
||||||
|
<dt>Account Name</dt>
|
||||||
|
<dd>%s</dd>
|
||||||
|
<dt>Secret Key</dt>
|
||||||
|
<dd>%s</dd>
|
||||||
|
<dt>Type</dt>
|
||||||
|
<dd>Time-based</dd>
|
||||||
|
<dt>Number of digits</dt>
|
||||||
|
<dd>6</dd>
|
||||||
|
<dt>Hashing algorithm</dt>
|
||||||
|
<dd>SHA-1</dd>
|
||||||
|
</dl>', $company, $holder, $secret);
|
||||||
|
|
||||||
|
return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/verify.tpl'), [
|
||||||
|
'$form_security_token' => self::getFormSecurityToken('settings_2fa_verify'),
|
||||||
|
'$password_security_token' => self::getFormSecurityToken('settings_2fa_password'),
|
||||||
|
'$title' => L10n::t('Two-factor code verification'),
|
||||||
|
'$help_label' => L10n::t('Help'),
|
||||||
|
'$message' => L10n::t('<p>Please scan this QR Code with your authenticator app and submit the provided code.</p>'),
|
||||||
|
'$qrcode_image' => $qrcode_image,
|
||||||
|
'$qrcode_url_message' => L10n::t('<p>Or you can open the following URL in your mobile devicde:</p><p><a href="%s">%s</a></p>', $otpauthUrl, $shortOtpauthUrl),
|
||||||
|
'$manual_message' => $manual_message,
|
||||||
|
'$company' => $company,
|
||||||
|
'$holder' => $holder,
|
||||||
|
'$secret' => $secret,
|
||||||
|
|
||||||
|
'$verify_code' => ['verify_code', L10n::t('Please enter a code from your authentication app'), '', '', 'required', 'autofocus placeholder="000000"'],
|
||||||
|
'$verify_label' => L10n::t('Verify code and enable two-factor authentication'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
39
view/templates/settings/twofactor/index.tpl
Normal file
39
view/templates/settings/twofactor/index.tpl
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<div class="generic-page-wrapper">
|
||||||
|
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||||
|
<div>{{$message nofilter}}</div>
|
||||||
|
<h2>{{$status_title}}</h2>
|
||||||
|
<p><strong>{{$auth_app_label}}</strong>: {{$app_status}} </p>
|
||||||
|
{{if $has_secret && $verified}}
|
||||||
|
<div>{{$configured_message nofilter}}</div>
|
||||||
|
{{/if}}
|
||||||
|
{{if $has_secret && !$verified}}
|
||||||
|
<div>{{$not_configured_message nofilter}}</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{if $has_secret && $verified}}
|
||||||
|
<h2>{{$recovery_codes_title}}</h2>
|
||||||
|
<p><strong>{{$recovery_codes_remaining}}</strong>: {{$recovery_codes_count}}</p>
|
||||||
|
<div>{{$recovery_codes_message nofilter}}</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<form action="settings/2fa" method="post">
|
||||||
|
<h2>{{$action_title}}</h2>
|
||||||
|
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||||
|
|
||||||
|
{{include file="field_password.tpl" field=$password}}
|
||||||
|
|
||||||
|
<div class="form-group settings-submit-wrapper" >
|
||||||
|
{{if !$has_secret}}
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="enable">{{$enable_label}}</button>
|
||||||
|
{{else}}
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="disable">{{$disable_label}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{if $has_secret && $verified}}
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="recovery">{{$recovery_codes_label}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{if $has_secret && !$verified}}
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="configure">{{$configure_label}}</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
28
view/templates/settings/twofactor/recovery.tpl
Normal file
28
view/templates/settings/twofactor/recovery.tpl
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="generic-page-wrapper">
|
||||||
|
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||||
|
<div>{{$message nofilter}}</div>
|
||||||
|
|
||||||
|
<ul class="recovery-codes">
|
||||||
|
{{foreach $recovery_codes as $recovery_code}}
|
||||||
|
<li>
|
||||||
|
{{if $recovery_code.used}}<s>{{/if}}
|
||||||
|
{{$recovery_code.code}}
|
||||||
|
{{if $recovery_code.used}}</s>{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/foreach}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{if $verified}}
|
||||||
|
<form action="settings/2fa/recovery?t={{$password_security_token}}" method="post">
|
||||||
|
<h2>{{$regenerate_label}}</h2>
|
||||||
|
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||||
|
<div>{{$regenerate_message}}</div>
|
||||||
|
|
||||||
|
<div class="form-group pull-right settings-submit-wrapper" >
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="regenerate">{{$regenerate_label}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<p class="text-right"><a href="settings/2fa/verify?t={{$password_security_token}}" class="btn btn-primary">{{$verify_label}}</a></p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
22
view/templates/settings/twofactor/verify.tpl
Normal file
22
view/templates/settings/twofactor/verify.tpl
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="generic-page-wrapper">
|
||||||
|
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||||
|
<div>{{$message nofilter}}</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
{{$qrcode_image nofilter}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="settings/2fa/verify?t={{$password_security_token}}" method="post">
|
||||||
|
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||||
|
|
||||||
|
{{include file="field_input.tpl" field=$verify_code}}
|
||||||
|
|
||||||
|
<div class="form-group settings-submit-wrapper" >
|
||||||
|
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="verify">{{$verify_label}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div>{{$qrcode_url_message nofilter}}</div>
|
||||||
|
|
||||||
|
<div>{{$manual_message nofilter}}</div>
|
||||||
|
</div>
|
Loading…
Reference in a new issue