Add two-factor authentication settings

- Add settings aside menu entry
- Add two-factor authentication documentation
This commit is contained in:
Hypolite Petovan 2019-05-13 01:38:15 -04:00
parent d7e9b91181
commit 8e885f5b97
10 changed files with 600 additions and 0 deletions

View 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.

View File

@ -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',

View File

@ -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);

View 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,
]);
}
}

View 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'),
]);
}
}

View 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 dont 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 wont work anymore.'),
'$regenerate_label' => L10n::t('Generate new recovery codes'),
'$verified' => $verified,
'$verify_label' => L10n::t('Next: Verification'),
]);
}
}

View 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'),
]);
}
}

View 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>

View 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>

View 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>