Merge pull request #7419 from MrPetovan/task/7338-app-specific-password
Add app-specific passwords feature
This commit is contained in:
commit
2d027d91d7
|
@ -58,3 +58,17 @@ In this case you will have to configure your authenticator app again using the p
|
||||||
When two-factor authentication is enabled, you can show your recovery codes, including the ones you've already used.
|
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.
|
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.
|
||||||
|
|
||||||
|
## Third-party applications and API
|
||||||
|
|
||||||
|
Third-party applications using the Friendica API can't accept two-factor time-based authentication codes.
|
||||||
|
Instead, if you enabled two-factor authentication, you have to generate app-specific randomly generated long passwords to use in your apps instead of your regular account password.
|
||||||
|
|
||||||
|
**Note**: Your regular password won't work at all when prompted in third-party apps if you enabled two-factor authentication.
|
||||||
|
|
||||||
|
You can generate as many app-specific passwords as you want, they will be shown once to you just after you generated it.
|
||||||
|
Just copy and paste it in your third-party app in the Friendica account password input field at this point.
|
||||||
|
We recommend generating a single app-specific password for each separate third-party app you are using, using a meaningul description of the target app (like "Frienqa on my Fairphone 2").
|
||||||
|
|
||||||
|
You can also revoke any and all app-specific password you generated this way.
|
||||||
|
This may log you out of the third-party application(s) you used the revoked app-specific password to log in with.
|
|
@ -236,7 +236,7 @@ function api_login(App $a)
|
||||||
if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) {
|
if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) {
|
||||||
$record = $addon_auth['user_record'];
|
$record = $addon_auth['user_record'];
|
||||||
} else {
|
} else {
|
||||||
$user_id = User::authenticate(trim($user), trim($password));
|
$user_id = User::authenticate(trim($user), trim($password), true);
|
||||||
if ($user_id !== false) {
|
if ($user_id !== false) {
|
||||||
$record = DBA::selectFirst('user', [], ['uid' => $user_id]);
|
$record = DBA::selectFirst('user', [], ['uid' => $user_id]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,6 +202,7 @@ class Router
|
||||||
$collector->addGroup('/2fa', function (RouteCollector $collector) {
|
$collector->addGroup('/2fa', function (RouteCollector $collector) {
|
||||||
$collector->addRoute(['GET', 'POST'], '[/]' , Module\Settings\TwoFactor\Index::class);
|
$collector->addRoute(['GET', 'POST'], '[/]' , Module\Settings\TwoFactor\Index::class);
|
||||||
$collector->addRoute(['GET', 'POST'], '/recovery' , Module\Settings\TwoFactor\Recovery::class);
|
$collector->addRoute(['GET', 'POST'], '/recovery' , Module\Settings\TwoFactor\Recovery::class);
|
||||||
|
$collector->addRoute(['GET', 'POST'], '/app_specific' , Module\Settings\TwoFactor\AppSpecific::class);
|
||||||
$collector->addRoute(['GET', 'POST'], '/verify' , Module\Settings\TwoFactor\Verify::class);
|
$collector->addRoute(['GET', 'POST'], '/verify' , Module\Settings\TwoFactor\Verify::class);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
137
src/Model/TwoFactor/AppSpecificPassword.php
Normal file
137
src/Model/TwoFactor/AppSpecificPassword.php
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Friendica\Model\TwoFactor;
|
||||||
|
|
||||||
|
use Friendica\BaseObject;
|
||||||
|
use Friendica\Database\DBA;
|
||||||
|
use Friendica\Model\User;
|
||||||
|
use Friendica\Util\DateTimeFormat;
|
||||||
|
use Friendica\Util\Temporal;
|
||||||
|
use PragmaRX\Random\Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages users' two-factor recovery hashed_passwords in the 2fa_app_specific_passwords table
|
||||||
|
*
|
||||||
|
* @package Friendica\Model
|
||||||
|
*/
|
||||||
|
class AppSpecificPassword extends BaseObject
|
||||||
|
{
|
||||||
|
public static function countForUser($uid)
|
||||||
|
{
|
||||||
|
return DBA::count('2fa_app_specific_password', ['uid' => $uid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function checkDuplicateForUser($uid, $description)
|
||||||
|
{
|
||||||
|
return DBA::exists('2fa_app_specific_password', ['uid' => $uid, 'description' => $description]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the provided hashed_password is available to use for login by the provided user
|
||||||
|
*
|
||||||
|
* @param int $uid User ID
|
||||||
|
* @param string $plaintextPassword
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function authenticateUser($uid, $plaintextPassword)
|
||||||
|
{
|
||||||
|
$appSpecificPasswords = self::getListForUser($uid);
|
||||||
|
|
||||||
|
$return = false;
|
||||||
|
|
||||||
|
foreach ($appSpecificPasswords as $appSpecificPassword) {
|
||||||
|
if (password_verify($plaintextPassword, $appSpecificPassword['hashed_password'])) {
|
||||||
|
$fields = ['last_used' => DateTimeFormat::utcNow()];
|
||||||
|
if (password_needs_rehash($appSpecificPassword['hashed_password'], PASSWORD_DEFAULT)) {
|
||||||
|
$fields['hashed_password'] = User::hashPassword($plaintextPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::update($appSpecificPassword['id'], $fields);
|
||||||
|
|
||||||
|
$return |= true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a complete list of all recovery hashed_passwords for the provided user, including the used status
|
||||||
|
*
|
||||||
|
* @param int $uid User ID
|
||||||
|
* @return array
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function getListForUser($uid)
|
||||||
|
{
|
||||||
|
$appSpecificPasswordsStmt = DBA::select('2fa_app_specific_password', ['id', 'description', 'hashed_password', 'last_used'], ['uid' => $uid]);
|
||||||
|
|
||||||
|
$appSpecificPasswords = DBA::toArray($appSpecificPasswordsStmt);
|
||||||
|
|
||||||
|
array_walk($appSpecificPasswords, function (&$value) {
|
||||||
|
$value['ago'] = Temporal::getRelativeDate($value['last_used']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $appSpecificPasswords;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new app specific password for the provided user and hashes it in the database.
|
||||||
|
*
|
||||||
|
* @param int $uid User ID
|
||||||
|
* @param string $description Password description
|
||||||
|
* @return array The new app-specific password data structure with the plaintext password added
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function generateForUser(int $uid, $description)
|
||||||
|
{
|
||||||
|
$Random = (new Random())->size(40);
|
||||||
|
|
||||||
|
$plaintextPassword = $Random->get();
|
||||||
|
|
||||||
|
$generated = DateTimeFormat::utcNow();
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'uid' => $uid,
|
||||||
|
'description' => $description,
|
||||||
|
'hashed_password' => User::hashPassword($plaintextPassword),
|
||||||
|
'generated' => $generated,
|
||||||
|
];
|
||||||
|
|
||||||
|
DBA::insert('2fa_app_specific_password', $fields);
|
||||||
|
|
||||||
|
$fields['id'] = DBA::lastInsertId();
|
||||||
|
$fields['plaintext_password'] = $plaintextPassword;
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function update($appSpecificPasswordId, $fields)
|
||||||
|
{
|
||||||
|
return DBA::update('2fa_app_specific_password', $fields, ['id' => $appSpecificPasswordId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all the recovery hashed_passwords for the provided user.
|
||||||
|
*
|
||||||
|
* @param int $uid User ID
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function deleteAllForUser(int $uid)
|
||||||
|
{
|
||||||
|
return DBA::delete('2fa_app_specific_password', ['uid' => $uid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $uid
|
||||||
|
* @param int $app_specific_password_id
|
||||||
|
* @return bool
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public static function deleteForUser(int $uid, int $app_specific_password_id)
|
||||||
|
{
|
||||||
|
return DBA::delete('2fa_app_specific_password', ['id' => $app_specific_password_id, 'uid' => $uid]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Friendica\Model;
|
namespace Friendica\Model\TwoFactor;
|
||||||
|
|
||||||
use Friendica\BaseObject;
|
use Friendica\BaseObject;
|
||||||
use Friendica\Database\DBA;
|
use Friendica\Database\DBA;
|
||||||
|
@ -13,7 +13,7 @@ use PragmaRX\Recovery\Recovery;
|
||||||
*
|
*
|
||||||
* @package Friendica\Model
|
* @package Friendica\Model
|
||||||
*/
|
*/
|
||||||
class TwoFactorRecoveryCode extends BaseObject
|
class RecoveryCode extends BaseObject
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Returns the number of code the provided users can still use to replace a TOTP code
|
* Returns the number of code the provided users can still use to replace a TOTP code
|
|
@ -17,6 +17,7 @@ use Friendica\Core\System;
|
||||||
use Friendica\Core\Worker;
|
use Friendica\Core\Worker;
|
||||||
use Friendica\Database\DBA;
|
use Friendica\Database\DBA;
|
||||||
use Friendica\Model\Photo;
|
use Friendica\Model\Photo;
|
||||||
|
use Friendica\Model\TwoFactor\AppSpecificPassword;
|
||||||
use Friendica\Object\Image;
|
use Friendica\Object\Image;
|
||||||
use Friendica\Util\Crypto;
|
use Friendica\Util\Crypto;
|
||||||
use Friendica\Util\DateTimeFormat;
|
use Friendica\Util\DateTimeFormat;
|
||||||
|
@ -270,14 +271,15 @@ class User
|
||||||
* @brief Authenticate a user with a clear text password
|
* @brief Authenticate a user with a clear text password
|
||||||
* @param mixed $user_info
|
* @param mixed $user_info
|
||||||
* @param string $password
|
* @param string $password
|
||||||
|
* @param bool $third_party
|
||||||
* @return int|boolean
|
* @return int|boolean
|
||||||
* @deprecated since version 3.6
|
* @deprecated since version 3.6
|
||||||
* @see User::getIdFromPasswordAuthentication()
|
* @see User::getIdFromPasswordAuthentication()
|
||||||
*/
|
*/
|
||||||
public static function authenticate($user_info, $password)
|
public static function authenticate($user_info, $password, $third_party = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return self::getIdFromPasswordAuthentication($user_info, $password);
|
return self::getIdFromPasswordAuthentication($user_info, $password, $third_party);
|
||||||
} catch (Exception $ex) {
|
} catch (Exception $ex) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -289,14 +291,20 @@ class User
|
||||||
* @brief Authenticate a user with a clear text password
|
* @brief Authenticate a user with a clear text password
|
||||||
* @param mixed $user_info
|
* @param mixed $user_info
|
||||||
* @param string $password
|
* @param string $password
|
||||||
|
* @param bool $third_party
|
||||||
* @return int User Id if authentication is successful
|
* @return int User Id if authentication is successful
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public static function getIdFromPasswordAuthentication($user_info, $password)
|
public static function getIdFromPasswordAuthentication($user_info, $password, $third_party = false)
|
||||||
{
|
{
|
||||||
$user = self::getAuthenticationInfo($user_info);
|
$user = self::getAuthenticationInfo($user_info);
|
||||||
|
|
||||||
if (strpos($user['password'], '$') === false) {
|
if ($third_party && PConfig::get($user['uid'], '2fa', 'verified')) {
|
||||||
|
// Third-party apps can't verify two-factor authentication, we use app-specific passwords instead
|
||||||
|
if (AppSpecificPassword::authenticateUser($user['uid'], $password)) {
|
||||||
|
return $user['uid'];
|
||||||
|
}
|
||||||
|
} elseif (strpos($user['password'], '$') === false) {
|
||||||
//Legacy hash that has not been replaced by a new hash yet
|
//Legacy hash that has not been replaced by a new hash yet
|
||||||
if (self::hashPasswordLegacy($password) === $user['password']) {
|
if (self::hashPasswordLegacy($password) === $user['password']) {
|
||||||
self::updatePasswordHashed($user['uid'], self::hashPassword($password));
|
self::updatePasswordHashed($user['uid'], self::hashPassword($password));
|
||||||
|
|
116
src/Module/Settings/TwoFactor/AppSpecific.php
Normal file
116
src/Module/Settings/TwoFactor/AppSpecific.php
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Friendica\Module\Settings\TwoFactor;
|
||||||
|
|
||||||
|
|
||||||
|
use Friendica\Core\L10n;
|
||||||
|
use Friendica\Core\PConfig;
|
||||||
|
use Friendica\Core\Renderer;
|
||||||
|
use Friendica\Model\TwoFactor\AppSpecificPassword;
|
||||||
|
use Friendica\Module\BaseSettingsModule;
|
||||||
|
use Friendica\Module\Login;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* // Page 5: 2FA enabled, app-specific password generation
|
||||||
|
*
|
||||||
|
* @package Friendica\Module\TwoFactor
|
||||||
|
*/
|
||||||
|
class AppSpecific extends BaseSettingsModule
|
||||||
|
{
|
||||||
|
private static $appSpecificPassword = null;
|
||||||
|
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
if (!$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 (!empty($_POST['action'])) {
|
||||||
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa/app_specific', 'settings_2fa_app_specific');
|
||||||
|
|
||||||
|
switch ($_POST['action']) {
|
||||||
|
case 'generate':
|
||||||
|
$description = $_POST['description'] ?? '';
|
||||||
|
if (empty($description)) {
|
||||||
|
notice(L10n::t('App-specific password generation failed: The description is empty.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
} elseif (AppSpecificPassword::checkDuplicateForUser(local_user(), $description)) {
|
||||||
|
notice(L10n::t('App-specific password generation failed: This description already exists.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
} else {
|
||||||
|
self::$appSpecificPassword = AppSpecificPassword::generateForUser(local_user(), $_POST['description'] ?? '');
|
||||||
|
notice(L10n::t('New app-specific password generated.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'revoke_all' :
|
||||||
|
AppSpecificPassword::deleteAllForUser(local_user());
|
||||||
|
notice(L10n::t('App-specific passwords successfully revoked.'));
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($_POST['revoke_id'])) {
|
||||||
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa/app_specific', 'settings_2fa_app_specific');
|
||||||
|
|
||||||
|
if (AppSpecificPassword::deleteForUser(local_user(), $_POST['revoke_id'])) {
|
||||||
|
notice(L10n::t('App-specific password successfully revoked.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function content()
|
||||||
|
{
|
||||||
|
if (!local_user()) {
|
||||||
|
return Login::form('settings/2fa/app_specific');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::content();
|
||||||
|
|
||||||
|
$appSpecificPasswords = AppSpecificPassword::getListForUser(local_user());
|
||||||
|
|
||||||
|
return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/app_specific.tpl'), [
|
||||||
|
'$form_security_token' => self::getFormSecurityToken('settings_2fa_app_specific'),
|
||||||
|
'$password_security_token' => self::getFormSecurityToken('settings_2fa_password'),
|
||||||
|
|
||||||
|
'$title' => L10n::t('Two-factor app-specific passwords'),
|
||||||
|
'$help_label' => L10n::t('Help'),
|
||||||
|
'$message' => L10n::t('<p>App-specific passwords are randomly generated passwords used instead your regular password to authenticate your account on third-party applications that don\'t support two-factor authentication.</p>'),
|
||||||
|
'$generated_message' => L10n::t('Make sure to copy your new app-specific password now. You won’t be able to see it again!'),
|
||||||
|
'$generated_app_specific_password' => self::$appSpecificPassword,
|
||||||
|
|
||||||
|
'$description_label' => L10n::t('Description'),
|
||||||
|
'$last_used_label' => L10n::t('Last Used'),
|
||||||
|
'$revoke_label' => L10n::t('Revoke'),
|
||||||
|
'$revoke_all_label' => L10n::t('Revoke All'),
|
||||||
|
|
||||||
|
'$app_specific_passwords' => $appSpecificPasswords,
|
||||||
|
'$generate_message' => L10n::t('When you generate a new app-specific password, you must use it right away, it will be shown to you once after you generate it.'),
|
||||||
|
'$generate_title' => L10n::t('Generate new app-specific password'),
|
||||||
|
'$description_placeholder_label' => L10n::t('Friendiqa on my Fairphone 2...'),
|
||||||
|
'$generate_label' => L10n::t('Generate'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,8 @@ use Friendica\Core\L10n;
|
||||||
use Friendica\Core\PConfig;
|
use Friendica\Core\PConfig;
|
||||||
use Friendica\Core\Renderer;
|
use Friendica\Core\Renderer;
|
||||||
use Friendica\Core\Session;
|
use Friendica\Core\Session;
|
||||||
use Friendica\Model\TwoFactorRecoveryCode;
|
use Friendica\Model\TwoFactor\AppSpecificPassword;
|
||||||
|
use Friendica\Model\TwoFactor\RecoveryCode;
|
||||||
use Friendica\Model\User;
|
use Friendica\Model\User;
|
||||||
use Friendica\Module\BaseSettingsModule;
|
use Friendica\Module\BaseSettingsModule;
|
||||||
use Friendica\Module\Login;
|
use Friendica\Module\Login;
|
||||||
|
@ -42,7 +43,7 @@ class Index extends BaseSettingsModule
|
||||||
break;
|
break;
|
||||||
case 'disable':
|
case 'disable':
|
||||||
if ($has_secret) {
|
if ($has_secret) {
|
||||||
TwoFactorRecoveryCode::deleteForUser(local_user());
|
RecoveryCode::deleteForUser(local_user());
|
||||||
PConfig::delete(local_user(), '2fa', 'secret');
|
PConfig::delete(local_user(), '2fa', 'secret');
|
||||||
PConfig::delete(local_user(), '2fa', 'verified');
|
PConfig::delete(local_user(), '2fa', 'verified');
|
||||||
Session::remove('2fa');
|
Session::remove('2fa');
|
||||||
|
@ -56,6 +57,11 @@ class Index extends BaseSettingsModule
|
||||||
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'app_specific':
|
||||||
|
if ($has_secret) {
|
||||||
|
self::getApp()->internalRedirect('settings/2fa/app_specific?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'configure':
|
case 'configure':
|
||||||
if (!$verified) {
|
if (!$verified) {
|
||||||
self::getApp()->internalRedirect('settings/2fa/verify?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
self::getApp()->internalRedirect('settings/2fa/verify?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
|
@ -94,14 +100,20 @@ class Index extends BaseSettingsModule
|
||||||
|
|
||||||
'$recovery_codes_title' => L10n::t('Recovery codes'),
|
'$recovery_codes_title' => L10n::t('Recovery codes'),
|
||||||
'$recovery_codes_remaining' => L10n::t('Remaining valid codes'),
|
'$recovery_codes_remaining' => L10n::t('Remaining valid codes'),
|
||||||
'$recovery_codes_count' => TwoFactorRecoveryCode::countValidForUser(local_user()),
|
'$recovery_codes_count' => RecoveryCode::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>'),
|
'$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>'),
|
||||||
|
|
||||||
|
'$app_specific_passwords_title' => L10n::t('App-specific passwords'),
|
||||||
|
'$app_specific_passwords_remaining' => L10n::t('Generated app-specific passwords'),
|
||||||
|
'$app_specific_passwords_count' => AppSpecificPassword::countForUser(local_user()),
|
||||||
|
'$app_specific_passwords_message' => L10n::t('<p>These randomly generated passwords allow you to authenticate on apps not supporting two-factor authentication.</p>'),
|
||||||
|
|
||||||
'$action_title' => L10n::t('Actions'),
|
'$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'],
|
'$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'),
|
'$enable_label' => L10n::t('Enable two-factor authentication'),
|
||||||
'$disable_label' => L10n::t('Disable two-factor authentication'),
|
'$disable_label' => L10n::t('Disable two-factor authentication'),
|
||||||
'$recovery_codes_label' => L10n::t('Show recovery codes'),
|
'$recovery_codes_label' => L10n::t('Show recovery codes'),
|
||||||
|
'$app_specific_passwords_label' => L10n::t('Manage app-specific passwords'),
|
||||||
'$configure_label' => L10n::t('Finish app configuration'),
|
'$configure_label' => L10n::t('Finish app configuration'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace Friendica\Module\Settings\TwoFactor;
|
||||||
use Friendica\Core\L10n;
|
use Friendica\Core\L10n;
|
||||||
use Friendica\Core\PConfig;
|
use Friendica\Core\PConfig;
|
||||||
use Friendica\Core\Renderer;
|
use Friendica\Core\Renderer;
|
||||||
use Friendica\Model\TwoFactorRecoveryCode;
|
use Friendica\Model\TwoFactor\RecoveryCode;
|
||||||
use Friendica\Module\BaseSettingsModule;
|
use Friendica\Module\BaseSettingsModule;
|
||||||
use Friendica\Module\Login;
|
use Friendica\Module\Login;
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class Recovery extends BaseSettingsModule
|
||||||
self::checkFormSecurityTokenRedirectOnError('settings/2fa/recovery', 'settings_2fa_recovery');
|
self::checkFormSecurityTokenRedirectOnError('settings/2fa/recovery', 'settings_2fa_recovery');
|
||||||
|
|
||||||
if ($_POST['action'] == 'regenerate') {
|
if ($_POST['action'] == 'regenerate') {
|
||||||
TwoFactorRecoveryCode::regenerateForUser(local_user());
|
RecoveryCode::regenerateForUser(local_user());
|
||||||
notice(L10n::t('New recovery codes successfully generated.'));
|
notice(L10n::t('New recovery codes successfully generated.'));
|
||||||
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||||
}
|
}
|
||||||
|
@ -61,11 +61,11 @@ class Recovery extends BaseSettingsModule
|
||||||
|
|
||||||
parent::content();
|
parent::content();
|
||||||
|
|
||||||
if (!TwoFactorRecoveryCode::countValidForUser(local_user())) {
|
if (!RecoveryCode::countValidForUser(local_user())) {
|
||||||
TwoFactorRecoveryCode::generateForUser(local_user());
|
RecoveryCode::generateForUser(local_user());
|
||||||
}
|
}
|
||||||
|
|
||||||
$recoveryCodes = TwoFactorRecoveryCode::getListForUser(local_user());
|
$recoveryCodes = RecoveryCode::getListForUser(local_user());
|
||||||
|
|
||||||
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use Friendica\BaseModule;
|
||||||
use Friendica\Core\L10n;
|
use Friendica\Core\L10n;
|
||||||
use Friendica\Core\Renderer;
|
use Friendica\Core\Renderer;
|
||||||
use Friendica\Core\Session;
|
use Friendica\Core\Session;
|
||||||
use Friendica\Model\TwoFactorRecoveryCode;
|
use Friendica\Model\TwoFactor\RecoveryCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* // Page 1a: Recovery code verification
|
* // Page 1a: Recovery code verification
|
||||||
|
@ -35,10 +35,10 @@ class Recovery extends BaseModule
|
||||||
|
|
||||||
$recovery_code = defaults($_POST, 'recovery_code', '');
|
$recovery_code = defaults($_POST, 'recovery_code', '');
|
||||||
|
|
||||||
if (TwoFactorRecoveryCode::existsForUser(local_user(), $recovery_code)) {
|
if (RecoveryCode::existsForUser(local_user(), $recovery_code)) {
|
||||||
TwoFactorRecoveryCode::markUsedForUser(local_user(), $recovery_code);
|
RecoveryCode::markUsedForUser(local_user(), $recovery_code);
|
||||||
Session::set('2fa', true);
|
Session::set('2fa', true);
|
||||||
notice(L10n::t('Remaining recovery codes: %d', TwoFactorRecoveryCode::countValidForUser(local_user())));
|
notice(L10n::t('Remaining recovery codes: %d', RecoveryCode::countValidForUser(local_user())));
|
||||||
|
|
||||||
// Resume normal login workflow
|
// Resume normal login workflow
|
||||||
Session::setAuthenticatedForUser($a, $a->user, true, true);
|
Session::setAuthenticatedForUser($a, $a->user, true, true);
|
||||||
|
|
|
@ -34,10 +34,25 @@
|
||||||
use Friendica\Database\DBA;
|
use Friendica\Database\DBA;
|
||||||
|
|
||||||
if (!defined('DB_UPDATE_VERSION')) {
|
if (!defined('DB_UPDATE_VERSION')) {
|
||||||
define('DB_UPDATE_VERSION', 1319);
|
define('DB_UPDATE_VERSION', 1320);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
"2fa_app_specific_password" => [
|
||||||
|
"comment" => "Two-factor app-specific _password",
|
||||||
|
"fields" => [
|
||||||
|
"id" => ["type" => "mediumint unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "Password ID for revocation"],
|
||||||
|
"uid" => ["type" => "mediumint unsigned", "not null" => "1", "relation" => ["user" => "uid"], "comment" => "User ID"],
|
||||||
|
"description" => ["type" => "varchar(255)", "comment" => "Description of the usage of the password"],
|
||||||
|
"hashed_password" => ["type" => "varchar(255)", "not null" => "1", "primary" => "1", "comment" => "Hashed password"],
|
||||||
|
"generated" => ["type" => "datetime", "not null" => "1", "comment" => "Datetime the password was generated"],
|
||||||
|
"last_used" => ["type" => "datetime", "comment" => "Datetime the password was last used"],
|
||||||
|
],
|
||||||
|
"indexes" => [
|
||||||
|
"PRIMARY" => ["id"],
|
||||||
|
"uid_description" => ["uid", "description"],
|
||||||
|
]
|
||||||
|
],
|
||||||
"2fa_recovery_codes" => [
|
"2fa_recovery_codes" => [
|
||||||
"comment" => "Two-factor authentication recovery codes",
|
"comment" => "Two-factor authentication recovery codes",
|
||||||
"fields" => [
|
"fields" => [
|
||||||
|
|
57
view/templates/settings/twofactor/app_specific.tpl
Normal file
57
view/templates/settings/twofactor/app_specific.tpl
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{if $generated_app_specific_password}}
|
||||||
|
<div class="panel panel-success">
|
||||||
|
<div class="panel-heading">
|
||||||
|
✅ {{$generated_app_specific_password.plaintext_password}}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{$generated_message}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<form action="settings/2fa/app_specific?t={{$password_security_token}}" method="post">
|
||||||
|
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||||
|
<table class="app-specific-passwords table table-hover table-condensed table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{$description_label}}</th>
|
||||||
|
<th>{{$last_used_label}}</th>
|
||||||
|
<th><button type="submit" name="action" class="btn btn-primary btn-small" value="revoke_all">{{$revoke_all_label}}</button></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{foreach $app_specific_passwords as $app_specific_password}}
|
||||||
|
<tr{{if $generated_app_specific_password && $app_specific_password.id == $generated_app_specific_password.id}} class="success"{{/if}}>
|
||||||
|
<td>
|
||||||
|
{{$app_specific_password.description}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="time" title="{{$app_specific_password.last_used}}" data-toggle="tooltip">
|
||||||
|
<time datetime="{{$app_specific_password.last_used}}">{{$app_specific_password.ago}}</time>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="submit" name="revoke_id" class="btn btn-default btn-small" value="{{$app_specific_password.id}}">{{$revoke_label}}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/foreach}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<form action="settings/2fa/app_specific?t={{$password_security_token}}" method="post">
|
||||||
|
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||||
|
<h3>{{$generate_title}}</h3>
|
||||||
|
<p>{{$generate_message}}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="app-specific-password-description">{{$description_label}}</label>
|
||||||
|
<input type="text" maxlength="255" name="description" id="app-specific-password-description" class="form-control" placeholder="{{$description_placeholder_label}}" required/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button type="submit" name="action" class="btn btn-large btn-primary" value="generate">{{$generate_label}}</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -22,18 +22,17 @@
|
||||||
|
|
||||||
{{include file="field_password.tpl" field=$password}}
|
{{include file="field_password.tpl" field=$password}}
|
||||||
|
|
||||||
<div class="form-group settings-submit-wrapper" >
|
|
||||||
{{if !$has_secret}}
|
{{if !$has_secret}}
|
||||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="enable">{{$enable_label}}</button>
|
<p><button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="enable">{{$enable_label}}</button></p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="disable">{{$disable_label}}</button>
|
<p><button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="disable">{{$disable_label}}</button></p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{if $has_secret && $verified}}
|
{{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>
|
<p><button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="recovery">{{$recovery_codes_label}}</button></p>
|
||||||
|
<p><button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="app_specific">{{$app_specific_passwords_label}}</button></p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{if $has_secret && !$verified}}
|
{{if $has_secret && !$verified}}
|
||||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="configure">{{$configure_label}}</button>
|
<p><button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="configure">{{$configure_label}}</button></p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue