Merge pull request #7135 from MrPetovan/task/two-factor-authentication

Add native two-factor authentication support
This commit is contained in:
Philipp 2019-05-14 07:07:03 +02:00 committed by GitHub
commit 5e85bdecd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1497 additions and 211 deletions

View file

@ -29,7 +29,7 @@ is self-signed).
## 1. Requirements
- Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file
- PHP 5.6.1+ (PHP 7.1+ recommended for performance and official support).
- PHP 7+ (PHP 7.1+ recommended for performance and official support).
- PHP *command line* with `register_argc_argv = true` in php.ini
- curl, gd (with at least jpeg support), mysql, mbstring, xml, zip and openssl extensions
- Some form of email server or email gateway such that PHP mail() works

View file

@ -13,7 +13,7 @@
"issues": "https://github.com/friendica/friendica/issues"
},
"require": {
"php": ">=5.6.1",
"php": ">=7.0",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",
@ -27,6 +27,7 @@
"ext-simplexml": "*",
"ext-xml": "*",
"asika/simple-console": "^1.0",
"bacon/bacon-qr-code": "^1.0",
"divineomega/password_exposed": "^2.4",
"ezyang/htmlpurifier": "~4.7.0",
"friendica/json-ld": "^1.0",
@ -36,8 +37,9 @@
"mobiledetect/mobiledetectlib": "2.8.*",
"monolog/monolog": "^1.24",
"nikic/fast-route": "^1.3",
"paragonie/random_compat": "^2.0",
"pear/text_languagedetect": "1.*",
"pragmarx/google2fa": "^5.0",
"pragmarx/recovery": "^0.1.0",
"psr/container": "^1.0",
"seld/cli-prompt": "^1.0",
"smarty/smarty": "^3.1",

473
composer.lock generated
View file

@ -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": "7fc5e358b089ca47cdb5cac0e22d15d8",
"content-hash": "d7302553201de079b72871c0b2922ce7",
"packages": [
{
"name": "asika/simple-console",
@ -39,6 +39,52 @@
"description": "One file console framework to help you write build scripts.",
"time": "2018-03-08T12:05:40+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/5a91b62b9d37cee635bbf8d553f4546057250bee",
"reference": "5a91b62b9d37cee635bbf8d553f4546057250bee",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": "^5.4|^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8"
},
"suggest": {
"ext-gd": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-0": {
"BaconQrCode": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"time": "2017-10-17T09:59:25+00:00"
},
{
"name": "bower-asset/Chart-js",
"version": "v2.7.2",
@ -1129,22 +1175,6 @@
"require": {
"npm-asset/ev-emitter": ">=1.0.0,<2.0.0"
},
"require-dev": {
"npm-asset/chalk": ">=1.1.1,<2.0.0",
"npm-asset/cheerio": ">=0.19.0,<0.20.0",
"npm-asset/gulp": ">=3.9.0,<4.0.0",
"npm-asset/gulp-jshint": ">=1.11.2,<2.0.0",
"npm-asset/gulp-json-lint": ">=0.1.0,<0.2.0",
"npm-asset/gulp-rename": ">=1.2.2,<2.0.0",
"npm-asset/gulp-replace": ">=0.5.4,<0.6.0",
"npm-asset/gulp-requirejs-optimize": "dev-github:metafizzy/gulp-requirejs-optimize",
"npm-asset/gulp-uglify": ">=1.4.2,<2.0.0",
"npm-asset/gulp-util": ">=3.0.7,<4.0.0",
"npm-asset/highlight.js": ">=8.9.1,<9.0.0",
"npm-asset/marked": ">=0.3.5,<0.4.0",
"npm-asset/minimist": ">=1.2.0,<2.0.0",
"npm-asset/transfob": ">=1.0.0,<2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1190,14 +1220,6 @@
"reference": null,
"shasum": "2736e332aaee73ccf0a14a5f0066391a0a13f4a3"
},
"require-dev": {
"npm-asset/grunt": "~0.4.2",
"npm-asset/grunt-contrib-cssmin": "~0.9.0",
"npm-asset/grunt-contrib-jshint": "~0.6.3",
"npm-asset/grunt-contrib-less": "~0.11.0",
"npm-asset/grunt-contrib-uglify": "~0.4.0",
"npm-asset/grunt-contrib-watch": "~0.6.1"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1231,32 +1253,6 @@
"reference": null,
"shasum": "2c89d6889b5eac522a7eea32c14521559c6cbf02"
},
"require-dev": {
"npm-asset/commitplease": "2.0.0",
"npm-asset/core-js": "0.9.17",
"npm-asset/grunt": "0.4.5",
"npm-asset/grunt-babel": "5.0.1",
"npm-asset/grunt-cli": "0.1.13",
"npm-asset/grunt-compare-size": "0.4.0",
"npm-asset/grunt-contrib-jshint": "0.11.2",
"npm-asset/grunt-contrib-uglify": "0.9.2",
"npm-asset/grunt-contrib-watch": "0.6.1",
"npm-asset/grunt-git-authors": "2.0.1",
"npm-asset/grunt-jscs": "2.1.0",
"npm-asset/grunt-jsonlint": "1.0.4",
"npm-asset/grunt-npmcopy": "0.1.0",
"npm-asset/gzip-js": "0.3.2",
"npm-asset/jsdom": "5.6.1",
"npm-asset/load-grunt-tasks": "1.0.0",
"npm-asset/qunit-assert-step": "1.0.3",
"npm-asset/qunitjs": "1.17.1",
"npm-asset/requirejs": "2.1.17",
"npm-asset/sinon": "1.10.3",
"npm-asset/sizzle": "2.2.1",
"npm-asset/strip-json-comments": "1.0.3",
"npm-asset/testswarm": "1.1.0",
"npm-asset/win-spawn": "2.0.0"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1407,12 +1403,6 @@
"reference": null,
"shasum": "06f0335f16e353a695e7206bf50503cb523a6ee5"
},
"require-dev": {
"npm-asset/grunt": "~0.4.1",
"npm-asset/grunt-contrib-connect": "~0.5.0",
"npm-asset/grunt-contrib-jshint": "~0.7.1",
"npm-asset/grunt-contrib-uglify": "~0.2.7"
},
"type": "npm-asset-library",
"extra": {
"npm-asset-bugs": {
@ -1695,33 +1685,29 @@
},
{
"name": "paragonie/random_compat",
"version": "v2.0.17",
"version": "v9.99.99",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d"
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/29af24f25bab834fcbb38ad2a69fa93b867e070d",
"reference": "29af24f25bab834fcbb38ad2a69fa93b867e070d",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
"php": "^7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
@ -1740,7 +1726,7 @@
"pseudorandom",
"random"
],
"time": "2018-07-04T16:31:37+00:00"
"time": "2018-07-02T15:55:56+00:00"
},
{
"name": "paragonie/sodium_compat",
@ -1923,6 +1909,189 @@
"homepage": "http://pear.php.net/package/Text_LanguageDetect",
"time": "2017-03-02T16:14:08+00:00"
},
{
"name": "pragmarx/google2fa",
"version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "17c969c82f427dd916afe4be50bafc6299aef1b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/17c969c82f427dd916afe4be50bafc6299aef1b4",
"reference": "17c969c82f427dd916afe4be50bafc6299aef1b4",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "~1.0|~2.0",
"paragonie/random_compat": ">=1",
"php": ">=5.4",
"symfony/polyfill-php56": "~1.2"
},
"require-dev": {
"phpunit/phpunit": "~4|~5|~6"
},
"type": "library",
"extra": {
"component": "package",
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/",
"PragmaRX\\Google2FA\\Tests\\": "tests/"
}
},
"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"
],
"time": "2019-03-19T22:44:16+00:00"
},
{
"name": "pragmarx/random",
"version": "v0.2.2",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/random.git",
"reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7",
"reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"fzaninotto/faker": "~1.7",
"phpunit/phpunit": "~6.4",
"pragmarx/trivia": "~0.1",
"squizlabs/php_codesniffer": "^2.3"
},
"suggest": {
"fzaninotto/faker": "Allows you to get dozens of randomized types",
"pragmarx/trivia": "For the trivia database"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Random\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com",
"role": "Developer"
}
],
"description": "Create random chars, numbers, strings",
"homepage": "https://github.com/antonioribeiro/random",
"keywords": [
"Randomize",
"faker",
"pragmarx",
"random",
"random number",
"random pattern",
"random string"
],
"time": "2017-11-21T05:26:22+00:00"
},
{
"name": "pragmarx/recovery",
"version": "v0.1.0",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/recovery.git",
"reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/recovery/zipball/e16573a1ae5345cc3b100eec6d0296a1a15a90fe",
"reference": "e16573a1ae5345cc3b100eec6d0296a1a15a90fe",
"shasum": ""
},
"require": {
"php": "~7.0",
"pragmarx/random": "~0.1"
},
"require-dev": {
"phpunit/phpunit": ">=5.4.3",
"squizlabs/php_codesniffer": "^2.3",
"tightenco/collect": "^5"
},
"suggest": {
"tightenco/collect": "Allows to generate recovery codes as collections"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Recovery\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"homepage": "https://antoniocarlosribeiro.com",
"role": "Developer"
}
],
"description": "Create recovery codes for two factor auth",
"homepage": "https://github.com/antonioribeiro/recovery",
"keywords": [
"2fa",
"account recovery",
"auth",
"backup codes",
"google2fa",
"pragmarx",
"recovery",
"recovery codes",
"two factor auth"
],
"time": "2017-09-19T16:58:00+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
@ -2215,9 +2384,159 @@
"templating"
],
"time": "2018-09-12T20:54:16+00:00"
},
{
"name": "symfony/polyfill-php56",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php56.git",
"reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/f4dddbc5c3471e1b700a147a20ae17cdb72dbe42",
"reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"symfony/polyfill-util": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php56\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
},
{
"name": "symfony/polyfill-util",
"version": "v1.11.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-util.git",
"reference": "b46c6cae28a3106735323f00a0c38eccf2328897"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-util/zipball/b46c6cae28a3106735323f00a0c38eccf2328897",
"reference": "b46c6cae28a3106735323f00a0c38eccf2328897",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Util\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony utilities for portability of PHP codes",
"homepage": "https://symfony.com",
"keywords": [
"compat",
"compatibility",
"polyfill",
"shim"
],
"time": "2019-02-08T14:16:39+00:00"
}
],
"packages-dev": [
{
"name": "dasprid/enum",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b",
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^6.4",
"squizlabs/php_codesniffer": "^3.1"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"time": "2017-10-25T22:45:27+00:00"
},
{
"name": "doctrine/instantiator",
"version": "1.0.5",
@ -2373,12 +2692,12 @@
"version": "v1.6.5",
"source": {
"type": "git",
"url": "https://github.com/mikey179/vfsStream.git",
"url": "https://github.com/bovigo/vfsStream.git",
"reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mikey179/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
"url": "https://api.github.com/repos/bovigo/vfsStream/zipball/d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
"reference": "d5fec95f541d4d71c4823bb5e30cf9b9e5b96145",
"shasum": ""
},
@ -3281,7 +3600,7 @@
}
],
"description": "Provides the functionality to compare PHP values for equality",
"homepage": "https://github.com/sebastianbergmann/comparator",
"homepage": "http://www.github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
@ -3383,7 +3702,7 @@
}
],
"description": "Provides functionality to handle HHVM/PHP environments",
"homepage": "https://github.com/sebastianbergmann/environment",
"homepage": "http://www.github.com/sebastianbergmann/environment",
"keywords": [
"Xdebug",
"environment",
@ -3451,7 +3770,7 @@
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"homepage": "https://github.com/sebastianbergmann/exporter",
"homepage": "http://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
@ -3503,7 +3822,7 @@
}
],
"description": "Snapshotting of global state",
"homepage": "https://github.com/sebastianbergmann/global-state",
"homepage": "http://www.github.com/sebastianbergmann/global-state",
"keywords": [
"global state"
],
@ -3605,7 +3924,7 @@
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"time": "2016-11-19T07:33:16+00:00"
},
{
@ -3869,7 +4188,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=5.6.1",
"php": ">=7.0",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",

View file

@ -34,10 +34,22 @@
use Friendica\Database\DBA;
if (!defined('DB_UPDATE_VERSION')) {
define('DB_UPDATE_VERSION', 1311);
define('DB_UPDATE_VERSION', 1312);
}
return [
"2fa_recovery_codes" => [
"comment" => "Two-factor authentication recovery codes",
"fields" => [
"uid" => ["type" => "int unsigned", "not null" => "1", "comment" => "User ID"],
"code" => ["type" => "varchar(50)", "not null" => "1", "comment" => "Recovery code string"],
"generated" => ["type" => "datetime", "not null" => "1", "comment" => "Datetime the code was generated"],
"used" => ["type" => "datetime", "comment" => "Datetime the code was used"],
],
"indexes" => [
"PRIMARY" => ["uid", "code"]
]
],
"addon" => [
"comment" => "registered addons",
"fields" => [

View file

@ -25,7 +25,7 @@ Requirements
---
* Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file
* PHP 5.6.1+ (PHP 7.1+ is recommended for performance and official support)
* PHP 7+ (PHP 7.1+ is recommended for performance and official support)
* PHP *command line* access with register_argc_argv set to true in the php.ini file
* Curl, GD, PDO, MySQLi, hash, xml, zip and OpenSSL extensions
* The POSIX module of PHP needs to be activated (e.g. [RHEL, CentOS](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) have disabled it)

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

@ -28,7 +28,7 @@ Requirements
---
* Apache mit einer aktiverten mod-rewrite-Funktion und dem Eintrag "Options All", so dass du die lokale .htaccess-Datei nutzen kannst
* PHP 5.6.1+ (PHP 7.1+ wird für Performance und offiziellen Support empfohlen)
* PHP 7+ (PHP 7.1+ wird für Performance und offiziellen Support empfohlen)
* PHP *Kommandozeilen*-Zugang mit register_argc_argv auf "true" gesetzt in der php.ini-Datei
* Curl, GD, PDO, MySQLi, xml, zip und OpenSSL-Erweiterung
* Das POSIX Modul muss aktiviert sein ([CentOS, RHEL](http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7http://www.bigsoft.co.uk/blog/index.php/2014/12/08/posix-php-commands-not-working-under-centos-7) haben dies z.B. deaktiviert)

View file

@ -11,7 +11,6 @@ use Friendica\Content\ContactSelector;
use Friendica\Content\Feature;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Authentication;
use Friendica\Core\Config;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
@ -19,6 +18,7 @@ use Friendica\Core\Logger;
use Friendica\Core\NotificationsManager;
use Friendica\Core\PConfig;
use Friendica\Core\Protocol;
use Friendica\Core\Session;
use Friendica\Core\System;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
@ -250,7 +250,7 @@ function api_login(App $a)
throw new UnauthorizedException("This API requires login");
}
Authentication::setAuthenticatedSessionForUser($record);
Session::setAuthenticatedForUser($a, $record);
$_SESSION["allow_api"] = true;

View file

@ -2,11 +2,12 @@
/**
* @file mod/manage.php
*/
use Friendica\App;
use Friendica\Core\Authentication;
use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\Database\DBA;
function manage_post(App $a) {
@ -108,7 +109,7 @@ function manage_post(App $a) {
unset($_SESSION['sysmsg_info']);
}
Authentication::setAuthenticatedSessionForUser($r[0], true, true);
Session::setAuthenticatedForUser($a, $r[0], true, true);
if ($limited_id) {
$_SESSION['submanage'] = $original_id;

View file

@ -4,10 +4,10 @@
*/
use Friendica\App;
use Friendica\Core\Authentication;
use Friendica\Core\Config;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Session;
use Friendica\Database\DBA;
use Friendica\Util\Strings;
@ -52,7 +52,7 @@ function openid_content(App $a) {
unset($_SESSION['openid']);
Authentication::setAuthenticatedSessionForUser($r[0],true,true);
Session::setAuthenticatedForUser($a, $r[0],true,true);
// just in case there was no return url set
// and we fell through

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[] = [
'label' => L10n::t('Profiles'),
'url' => 'profiles',

View file

@ -47,6 +47,10 @@ class Router
$collector->addRoute(['GET'], '/webfinger' , Module\Xrd::class);
$collector->addRoute(['GET'], '/x-social-relay' , Module\WellKnown\XSocialRelay::class);
});
$this->routeCollector->addGroup('/2fa', function (RouteCollector $collector) {
$collector->addRoute(['GET', 'POST'], '[/]' , Module\TwoFactor\Verify::class);
$collector->addRoute(['GET', 'POST'], '/recovery' , Module\TwoFactor\Recovery::class);
});
$this->routeCollector->addGroup('/admin', function (RouteCollector $collector) {
$collector->addRoute(['GET'] , '[/]' , Module\Admin\Summary::class);
@ -184,6 +188,14 @@ class Router
$collector->addRoute(['GET'], '/{sub1}/{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'], '/robots.txt', Module\RobotsTxt::class);
$this->routeCollector->addRoute(['GET'], '/rsd.xml', Module\ReallySimpleDiscovery::class);

View file

@ -5,11 +5,9 @@
namespace Friendica\Core;
use Friendica\App;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Model\User;
use Friendica\Util\BaseURL;
use Friendica\Util\DateTimeFormat;
/**
* Handle Authentification, Session and Cookies
@ -55,112 +53,6 @@ class Authentication extends BaseObject
setcookie("Friendica", $value, $time, "/", "", (Config::get('system', 'ssl_policy') == BaseUrl::SSL_POLICY_FULL), true);
}
/**
* @brief Sets the provided user's authenticated session
*
* @todo Should be moved to Friendica\Core\Session once it's created
*
* @param array $user_record
* @param bool $login_initial
* @param bool $interactive
* @param bool $login_refresh
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function setAuthenticatedSessionForUser($user_record, $login_initial = false, $interactive = false, $login_refresh = false)
{
$a = self::getApp();
$_SESSION['uid'] = $user_record['uid'];
$_SESSION['theme'] = $user_record['theme'];
$_SESSION['mobile-theme'] = PConfig::get($user_record['uid'], 'system', 'mobile_theme');
$_SESSION['authenticated'] = 1;
$_SESSION['page_flags'] = $user_record['page-flags'];
$_SESSION['my_url'] = $a->getbaseUrl() . '/profile/' . $user_record['nickname'];
$_SESSION['my_address'] = $user_record['nickname'] . '@' . substr($a->getbaseUrl(), strpos($a->getbaseUrl(), '://') + 3);
$_SESSION['addr'] = defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0');
$a->user = $user_record;
if ($interactive) {
if ($a->user['login_date'] <= DBA::NULL_DATETIME) {
$_SESSION['return_path'] = 'profile_photo/new';
$a->module = 'profile_photo';
info(L10n::t("Welcome ") . $a->user['username'] . EOL);
info(L10n::t('Please upload a profile photo.') . EOL);
} else {
info(L10n::t("Welcome back ") . $a->user['username'] . EOL);
}
}
$member_since = strtotime($a->user['register_date']);
if (time() < ($member_since + ( 60 * 60 * 24 * 14))) {
$_SESSION['new_member'] = true;
} else {
$_SESSION['new_member'] = false;
}
if (strlen($a->user['timezone'])) {
date_default_timezone_set($a->user['timezone']);
$a->timezone = $a->user['timezone'];
}
$masterUid = $user_record['uid'];
if (!empty($_SESSION['submanage'])) {
$user = DBA::selectFirst('user', ['uid'], ['uid' => $_SESSION['submanage']]);
if (DBA::isResult($user)) {
$masterUid = $user['uid'];
}
}
$a->identities = User::identities($masterUid);
if ($login_initial) {
Logger::log('auth_identities: ' . print_r($a->identities, true), Logger::DEBUG);
}
if ($login_refresh) {
Logger::log('auth_identities refresh: ' . print_r($a->identities, true), Logger::DEBUG);
}
$contact = DBA::selectFirst('contact', [], ['uid' => $_SESSION['uid'], 'self' => true]);
if (DBA::isResult($contact)) {
$a->contact = $contact;
$a->cid = $contact['id'];
$_SESSION['cid'] = $a->cid;
}
header('X-Account-Management-Status: active; name="' . $a->user['username'] . '"; id="' . $a->user['nickname'] . '"');
if ($login_initial || $login_refresh) {
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $_SESSION['uid']]);
// Set the login date for all identities of the user
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()],
['parent-uid' => $masterUid, 'account_removed' => false]);
}
if ($login_initial) {
/*
* If the user specified to remember the authentication, then set a cookie
* that expires after one week (the default is when the browser is closed).
* The cookie will be renewed automatically.
* The week ensures that sessions will expire after some inactivity.
*/
if (!empty($_SESSION['remember'])) {
Logger::log('Injecting cookie for remembered user ' . $a->user['nickname']);
self::setCookie(604800, $user_record);
unset($_SESSION['remember']);
}
}
if ($login_initial) {
Hook::callAll('logged_in', $a->user);
if (($a->module !== 'home') && isset($_SESSION['return_path'])) {
$a->internalRedirect($_SESSION['return_path']);
}
}
}
/**
* @brief Kills the "Friendica" cookie and all session data
*/
@ -170,5 +62,26 @@ class Authentication extends BaseObject
session_unset();
session_destroy();
}
public static function twoFactorCheck($uid, App $a)
{
// Check user setting, if 2FA disabled return
if (!PConfig::get($uid, '2fa', 'verified')) {
return;
}
// Check current path, if 2fa authentication module return
if ($a->argc > 0 && in_array($a->argv[0], ['ping', '2fa', 'view', 'help', 'logout'])) {
return;
}
// Case 1: 2FA session present and valid: return
if (Session::get('2fa')) {
return;
}
// Case 2: No valid 2FA session: redirect to code verification page
$a->internalRedirect('2fa');
}
}

View file

@ -5,9 +5,14 @@
*/
namespace Friendica\Core;
use Friendica\App;
use Friendica\Core\Session\CacheSessionHandler;
use Friendica\Core\Session\DatabaseSessionHandler;
use Friendica\Database\DBA;
use Friendica\Model\Contact;
use Friendica\Model\User;
use Friendica\Util\BaseURL;
use Friendica\Util\DateTimeFormat;
/**
* High-level Session service class
@ -25,7 +30,7 @@ class Session
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
if (Config::get('system', 'ssl_policy') == BaseUrl::SSL_POLICY_FULL) {
if (Config::get('system', 'ssl_policy') == BaseURL::SSL_POLICY_FULL) {
ini_set('session.cookie_secure', 1);
}
@ -66,8 +71,142 @@ class Session
return $return;
}
/**
* Sets a single session variable.
* Overrides value of existing key.
*
* @param string $name
* @param mixed $value
*/
public static function set($name, $value)
{
$_SESSION[$name] = $value;
}
/**
* Sets multiple session variables.
* Overrides values for existing keys.
*
* @param array $values
*/
public static function setMultiple(array $values)
{
$_SESSION = $values + $_SESSION;
}
/**
* Removes a session variable.
* Ignores missing keys.
*
* @param $name
*/
public static function remove($name)
{
unset($_SESSION[$name]);
}
/**
* @brief Sets the provided user's authenticated session
*
* @param App $a
* @param array $user_record
* @param bool $login_initial
* @param bool $interactive
* @param bool $login_refresh
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function setAuthenticatedForUser(App $a, array $user_record, $login_initial = false, $interactive = false, $login_refresh = false)
{
self::setMultiple([
'uid' => $user_record['uid'],
'theme' => $user_record['theme'],
'mobile-theme' => PConfig::get($user_record['uid'], 'system', 'mobile_theme'),
'authenticated' => 1,
'page_flags' => $user_record['page-flags'],
'my_url' => $a->getBaseURL() . '/profile/' . $user_record['nickname'],
'my_address' => $user_record['nickname'] . '@' . substr($a->getBaseURL(), strpos($a->getBaseURL(), '://') + 3),
'addr' => defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0'),
]);
$member_since = strtotime($user_record['register_date']);
self::set('new_member', time() < ($member_since + ( 60 * 60 * 24 * 14)));
if (strlen($user_record['timezone'])) {
date_default_timezone_set($user_record['timezone']);
$a->timezone = $user_record['timezone'];
}
$masterUid = $user_record['uid'];
if (self::get('submanage')) {
$user = DBA::selectFirst('user', ['uid'], ['uid' => self::get('submanage')]);
if (DBA::isResult($user)) {
$masterUid = $user['uid'];
}
}
$a->identities = User::identities($masterUid);
if ($login_initial) {
$a->getLogger()->info('auth_identities: ' . print_r($a->identities, true));
}
if ($login_refresh) {
$a->getLogger()->info('auth_identities refresh: ' . print_r($a->identities, true));
}
$contact = DBA::selectFirst('contact', [], ['uid' => $user_record['uid'], 'self' => true]);
if (DBA::isResult($contact)) {
$a->contact = $contact;
$a->cid = $contact['id'];
self::set('cid', $a->cid);
}
header('X-Account-Management-Status: active; name="' . $user_record['username'] . '"; id="' . $user_record['nickname'] . '"');
if ($login_initial || $login_refresh) {
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $user_record['uid']]);
// Set the login date for all identities of the user
DBA::update('user', ['login_date' => DateTimeFormat::utcNow()],
['parent-uid' => $masterUid, 'account_removed' => false]);
}
if ($login_initial) {
/*
* If the user specified to remember the authentication, then set a cookie
* that expires after one week (the default is when the browser is closed).
* The cookie will be renewed automatically.
* The week ensures that sessions will expire after some inactivity.
*/
;
if (self::get('remember')) {
$a->getLogger()->info('Injecting cookie for remembered user ' . $user_record['nickname']);
Authentication::setCookie(604800, $user_record);
self::remove('remember');
}
}
Authentication::twoFactorCheck($user_record['uid'], $a);
if ($interactive) {
if ($user_record['login_date'] <= DBA::NULL_DATETIME) {
info(L10n::t('Welcome %s', $user_record['username']));
info(L10n::t('Please upload a profile photo.'));
$a->internalRedirect('profile_photo/new');
} else {
info(L10n::t("Welcome back %s", $user_record['username']));
}
}
$a->user = $user_record;
if ($login_initial) {
Hook::callAll('logged_in', $a->user);
if ($a->module !== 'home' && self::exists('return_path')) {
$a->internalRedirect(self::get('return_path'));
}
}
}
}

View file

@ -1484,6 +1484,8 @@ class DBA
$new_values = array_merge($new_values, array_values($value));
$placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
$condition_string .= "`" . $field . "` IN (" . $placeholders . ")";
} elseif (is_null($value)) {
$condition_string .= "`" . $field . "` IS NULL";
} else {
$new_values[$field] = $value;
$condition_string .= "`" . $field . "` = ?";

View file

@ -0,0 +1,125 @@
<?php
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
use PragmaRX\Random\Random;
use PragmaRX\Recovery\Recovery;
/**
* Manages users' two-factor recovery codes in the 2fa_recovery_codes table
*
* @package Friendica\Model
*/
class TwoFactorRecoveryCode extends BaseObject
{
/**
* Returns the number of code the provided users can still use to replace a TOTP code
*
* @param int $uid User ID
* @return int
* @throws \Exception
*/
public static function countValidForUser($uid)
{
return DBA::count('2fa_recovery_codes', ['uid' => $uid, 'used' => null]);
}
/**
* Checks the provided code is available to use for login by the provided user
*
* @param int $uid User ID
* @param string $code
* @return bool
* @throws \Exception
*/
public static function existsForUser($uid, $code)
{
return DBA::exists('2fa_recovery_codes', ['uid' => $uid, 'code' => $code, 'used' => null]);
}
/**
* Returns a complete list of all recovery codes for the provided user, including the used status
*
* @param int $uid User ID
* @return array
* @throws \Exception
*/
public static function getListForUser($uid)
{
$codesStmt = DBA::select('2fa_recovery_codes', ['code', 'used'], ['uid' => $uid]);
return DBA::toArray($codesStmt);
}
/**
* Marks the provided code as used for the provided user.
* Returns false if the code doesn't exist for the user or it has been used already.
*
* @param int $uid User ID
* @param string $code
* @return bool
* @throws \Exception
*/
public static function markUsedForUser($uid, $code)
{
DBA::update('2fa_recovery_codes', ['used' => DateTimeFormat::utcNow()], ['uid' => $uid, 'code' => $code, 'used' => null]);
return DBA::affectedRows() > 0;
}
/**
* Generates a fresh set of recovery codes for the provided user.
* Generates 12 codes constituted of 2 blocks of 6 characters separated by a dash.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function generateForUser($uid)
{
$Random = (new Random())->pattern('[a-z0-9]');
$RecoveryGenerator = new Recovery($Random);
$codes = $RecoveryGenerator
->setCount(12)
->setBlocks(2)
->setChars(6)
->lowercase(true)
->toArray();
$generated = DateTimeFormat::utcNow();
foreach ($codes as $code) {
DBA::insert('2fa_recovery_codes', [
'uid' => $uid,
'code' => $code,
'generated' => $generated
]);
}
}
/**
* Deletes all the recovery codes for the provided user.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function deleteForUser($uid)
{
DBA::delete('2fa_recovery_codes', ['uid' => $uid]);
}
/**
* Replaces the existing recovery codes for the provided user by a freshly generated set.
*
* @param int $uid User ID
* @throws \Exception
*/
public static function regenerateForUser($uid)
{
self::deleteForUser($uid);
self::generateForUser($uid);
}
}

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

@ -21,35 +21,35 @@ class Help extends BaseModule
$text = '';
$filename = '';
$app = self::getApp();
$config = $app->getConfig();
$a = self::getApp();
$config = $a->getConfig();
$lang = $config->get('system', 'language');
// @TODO: Replace with parameter from router
if ($app->argc > 1) {
if ($a->argc > 1) {
$path = '';
// looping through the argv keys bigger than 0 to build
// a path relative to /help
for ($x = 1; $x < $app->argc; $x ++) {
for ($x = 1; $x < $a->argc; $x ++) {
if (strlen($path)) {
$path .= '/';
}
$path .= $app->getArgumentValue($x);
$path .= $a->getArgumentValue($x);
}
$title = basename($path);
$filename = $path;
$text = self::loadDocFile('doc/' . $path . '.md', $lang);
$app->page['title'] = L10n::t('Help:') . ' ' . str_replace('-', ' ', Strings::escapeTags($title));
$a->page['title'] = L10n::t('Help:') . ' ' . str_replace('-', ' ', Strings::escapeTags($title));
}
$home = self::loadDocFile('doc/Home.md', $lang);
if (!$text) {
$text = $home;
$filename = "Home";
$app->page['title'] = L10n::t('Help');
$a->page['title'] = L10n::t('Help');
} else {
$app->page['aside'] = Markdown::convert($home, false);
$a->page['aside'] = Markdown::convert($home, false);
}
if (!strlen($text)) {
@ -85,7 +85,7 @@ class Help extends BaseModule
$idNum[$level] ++;
$id = implode("_", array_slice($idNum, 1, $level));
$href = $app->getBaseURL() . "/help/{$filename}#{$id}";
$href = $a->getBaseURL() . "/help/{$filename}#{$id}";
$toc .= "<li><a href='{$href}'>" . strip_tags($line) . "</a></li>";
$line = "<a name='{$id}'></a>" . $line;
$lastLevel = $level;

View file

@ -12,6 +12,7 @@ use Friendica\Core\Hook;
use Friendica\Core\L10n;
use Friendica\Core\Logger;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Model\User;
@ -160,7 +161,8 @@ class Login extends BaseModule
// if we haven't failed up this point, log them in.
$_SESSION['remember'] = $remember;
$_SESSION['last_login_date'] = DateTimeFormat::utcNow();
Authentication::setAuthenticatedSessionForUser($record, true, true);
Session::setAuthenticatedForUser($a, $record, true, true);
if (!empty($_SESSION['return_path'])) {
$return_path = $_SESSION['return_path'];
@ -210,7 +212,7 @@ class Login extends BaseModule
// Do the authentification if not done by now
if (!isset($_SESSION) || !isset($_SESSION['authenticated'])) {
Authentication::setAuthenticatedSessionForUser($user);
Session::setAuthenticatedForUser($a, $user);
if (Config::get('system', 'paranoia')) {
$_SESSION['addr'] = $data->ip;
@ -263,7 +265,8 @@ class Login extends BaseModule
$_SESSION['last_login_date'] = DateTimeFormat::utcNow();
$login_refresh = true;
}
Authentication::setAuthenticatedSessionForUser($user, false, false, $login_refresh);
Session::setAuthenticatedForUser($a, $user, false, false, $login_refresh);
}
}
}

View file

@ -0,0 +1,108 @@
<?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,
'$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,130 @@
<?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,72 @@
<?php
namespace Friendica\Module\TwoFactor;
use Friendica\BaseModule;
use Friendica\Core\L10n;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use Friendica\Model\TwoFactorRecoveryCode;
/**
* // Page 1a: Recovery code verification
*
* @package Friendica\Module\TwoFactor
*/
class Recovery extends BaseModule
{
public static function init()
{
if (!local_user()) {
return;
}
}
public static function post()
{
if (!local_user()) {
return;
}
if (defaults($_POST, 'action', null) == 'recover') {
self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_recovery');
$a = self::getApp();
$recovery_code = defaults($_POST, 'recovery_code', '');
if (TwoFactorRecoveryCode::existsForUser(local_user(), $recovery_code)) {
TwoFactorRecoveryCode::markUsedForUser(local_user(), $recovery_code);
Session::set('2fa', true);
notice(L10n::t('Remaining recovery codes: %d', TwoFactorRecoveryCode::countValidForUser(local_user())));
// Resume normal login workflow
Session::setAuthenticatedForUser($a, $a->user, true, true);
} else {
notice(L10n::t('Invalid code, please retry.'));
}
}
}
public static function content()
{
if (!local_user()) {
self::getApp()->internalRedirect();
}
// Already authenticated with 2FA token
if (Session::get('2fa')) {
self::getApp()->internalRedirect();
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/recovery.tpl'), [
'$form_security_token' => self::getFormSecurityToken('twofactor_recovery'),
'$title' => L10n::t('Two-factor recovery'),
'$message' => L10n::t('<p>You can enter one of your one-time recovery codes in case you lost access to your mobile device.</p>'),
'$recovery_message' => L10n::t('Dont have your phone? <a href="%s">Enter a two-factor recovery code</a>', '2fa/recovery'),
'$recovery_code' => ['recovery_code', L10n::t('Please enter a recovery code'), '', '', '', 'placeholder="000000-000000"'],
'$recovery_label' => L10n::t('Submit recovery code and complete login'),
]);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Friendica\Module\TwoFactor;
use Friendica\BaseModule;
use Friendica\Core\L10n;
use Friendica\Core\PConfig;
use Friendica\Core\Renderer;
use Friendica\Core\Session;
use PragmaRX\Google2FA\Google2FA;
/**
* Page 1: Authenticator code verification
*
* @package Friendica\Module\TwoFactor
*/
class Verify extends BaseModule
{
public static function post()
{
if (!local_user()) {
return;
}
if (defaults($_POST, 'action', null) == 'verify') {
self::checkFormSecurityTokenRedirectOnError('2fa', 'twofactor_verify');
$a = self::getApp();
$code = defaults($_POST, 'verify_code', '');
$valid = (new Google2FA())->verifyKey(PConfig::get(local_user(), '2fa', 'secret'), $code);
// The same code can't be used twice even if it's valid
if ($valid && Session::get('2fa') !== $code) {
Session::set('2fa', $code);
// Resume normal login workflow
Session::setAuthenticatedForUser($a, $a->user, true, true);
} else {
notice(L10n::t('Invalid code, please retry.'));
}
}
}
public static function content()
{
if (!local_user()) {
self::getApp()->internalRedirect();
}
// Already authenticated with 2FA token
if (Session::get('2fa')) {
self::getApp()->internalRedirect();
}
return Renderer::replaceMacros(Renderer::getMarkupTemplate('twofactor/verify.tpl'), [
'$form_security_token' => self::getFormSecurityToken('twofactor_verify'),
'$title' => L10n::t('Two-factor authentication'),
'$message' => L10n::t('<p>Open the two-factor authentication app on your device to get an authentication code and verify your identity.</p>'),
'$recovery_message' => L10n::t('Dont have your phone? <a href="%s">Enter a two-factor recovery code</a>', '2fa/recovery'),
'$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 complete login'),
]);
}
}

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>

View file

@ -0,0 +1,14 @@
<div class="generic-page-wrapper">
<h1>{{$title}}</h1>
<div>{{$message nofilter}}</div>
<form action="" method="post">
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
{{include file="field_input.tpl" field=$recovery_code}}
<div class="form-group settings-submit-wrapper">
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="recover">{{$recovery_label}}</button>
</div>
</form>
</div>

View file

@ -0,0 +1,15 @@
<div class="generic-page-wrapper">
<h1>{{$title}}</h1>
<div>{{$message nofilter}}</div>
<form action="" 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>{{$recovery_message nofilter}}</div>
</div>