Merge pull request #7135 from MrPetovan/task/two-factor-authentication
Add native two-factor authentication support
This commit is contained in:
commit
5e85bdecd0
|
@ -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
|
||||
|
|
|
@ -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
473
composer.lock
generated
|
@ -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": "*",
|
||||
|
|
|
@ -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" => [
|
||||
|
|
|
@ -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)
|
||||
|
|
60
doc/Two-Factor-Authentication.md
Normal file
60
doc/Two-Factor-Authentication.md
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Configuring two-factor authentication
|
||||
|
||||
* [Home](help)
|
||||
|
||||
You can configure two-factor authentication using a mobile app.
|
||||
A time-based one-time password (TOTP) application automatically generates an authentication code that changes after a certain period of time.
|
||||
|
||||
**Tip**: To configure authentication via TOTP on multiple devices, during setup, scan the QR code using each device at the same time.
|
||||
If 2FA is already enabled and you want to add another device, you must re-configure 2FA from your security settings.
|
||||
|
||||
## Enabling two-factor authentication
|
||||
|
||||
### 1. Download an authenticator app
|
||||
|
||||
Any authenticator app should work with Friendica.
|
||||
Notheless, we recommend:
|
||||
|
||||
- For iOS, [Matt Rubin's MIT-licensed Authenticator app](https://mattrubin.me/authenticator).
|
||||
- For Android, [andOTP](https://github.com/andOTP/andOTP).
|
||||
|
||||
### 2. Record your one-use recovery codes
|
||||
|
||||
From your [two-factor authentication user settings](/settings/2fa), enter your password and click on "Enable two-factor authentication".
|
||||
|
||||
You will be presented with a list of one-use recovery codes.
|
||||
Please save those in the same place you are saving your Friendica password (ideally, in a password manager like [KeePass](https://keepass.info)).
|
||||
|
||||
When you're done, click on "Next".
|
||||
|
||||
### 3. Setup your authenticator app
|
||||
|
||||
You have three methods to setup your authenticator app:
|
||||
|
||||
1. Scan the QR Code with your device camera.
|
||||
This will automatically configure your account on the app.
|
||||
2. Click/tap on the provided **totp://** URl.
|
||||
Ideally your authenticator app should be called with this URL and set up your account.
|
||||
3. Enter your account settings manually.
|
||||
Friendica is using default settings for token type, code digit count and hashing algorithm but you may be required to enter them in your app.
|
||||
|
||||
**Tip**: If you have multiple devices, configure them all at this point.
|
||||
|
||||
Then verify your app is correctly configured by submitting a code provided by your app.
|
||||
This will conclude two-factor authentication configuration.
|
||||
|
||||
**Note:** If you leave this screen at any point without having submitted a verification code, two-factor authentication won't be enabled on your account.
|
||||
To complete the configuration, just come back to your [two-factor authentication user settings](/settings/2fa) and click on "Finish configuration" after entering your current password.
|
||||
|
||||
## Disabling two-factor authentication
|
||||
|
||||
You can disable two-factor authentication at any time by going to your [two-factor authentication user settings](/settings/2fa) and click on "Disable two-factor authentication" after entering your current password.
|
||||
|
||||
You should remove your Friendica account from your authenticator app as it won't work again even if you reenable two-factor authentication.
|
||||
In this case you will have to configure your authenticator app again using the process above.
|
||||
|
||||
## Managing your one-time recovery codes
|
||||
|
||||
When two-factor authentication is enabled, you can show your recovery codes, including the ones you've already used.
|
||||
|
||||
You can freely regenerate a new set of fresh recovery codes, just be sure to replace the previous ones where you saved them as they won't be active anymore.
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 . "` = ?";
|
||||
|
|
125
src/Model/TwoFactorRecoveryCode.php
Normal file
125
src/Model/TwoFactorRecoveryCode.php
Normal 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);
|
||||
}
|
||||
}
|
110
src/Module/BaseSettingsModule.php
Normal file
110
src/Module/BaseSettingsModule.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace Friendica\Module;
|
||||
|
||||
use Friendica\BaseModule;
|
||||
use Friendica\Content\Feature;
|
||||
use Friendica\Core\L10n;
|
||||
use Friendica\Core\Renderer;
|
||||
|
||||
class BaseSettingsModule extends BaseModule
|
||||
{
|
||||
public static function content()
|
||||
{
|
||||
$a = self::getApp();
|
||||
|
||||
$tpl = Renderer::getMarkupTemplate('settings/head.tpl');
|
||||
$a->page['htmlhead'] .= Renderer::replaceMacros($tpl, [
|
||||
'$ispublic' => L10n::t('everybody')
|
||||
]);
|
||||
|
||||
$tabs = [];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Account'),
|
||||
'url' => 'settings',
|
||||
'selected' => (($a->argc == 1) && ($a->argv[0] === 'settings') ? 'active' : ''),
|
||||
'accesskey' => 'o',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Two-factor authentication'),
|
||||
'url' => 'settings/2fa',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === '2fa') ? 'active' : ''),
|
||||
'accesskey' => 'o',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Profiles'),
|
||||
'url' => 'profiles',
|
||||
'selected' => (($a->argc == 1) && ($a->argv[0] === 'profiles') ? 'active' : ''),
|
||||
'accesskey' => 'p',
|
||||
];
|
||||
|
||||
if (Feature::get()) {
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Additional features'),
|
||||
'url' => 'settings/features',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === 'features') ? 'active' : ''),
|
||||
'accesskey' => 't',
|
||||
];
|
||||
}
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Display'),
|
||||
'url' => 'settings/display',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === 'display') ? 'active' : ''),
|
||||
'accesskey' => 'i',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Social Networks'),
|
||||
'url' => 'settings/connectors',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === 'connectors') ? 'active' : ''),
|
||||
'accesskey' => 'w',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Addons'),
|
||||
'url' => 'settings/addon',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === 'addon') ? 'active' : ''),
|
||||
'accesskey' => 'l',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Delegations'),
|
||||
'url' => 'delegate',
|
||||
'selected' => (($a->argc == 1) && ($a->argv[0] === 'delegate') ? 'active' : ''),
|
||||
'accesskey' => 'd',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Connected apps'),
|
||||
'url' => 'settings/oauth',
|
||||
'selected' => (($a->argc > 1) && ($a->argv[1] === 'oauth') ? 'active' : ''),
|
||||
'accesskey' => 'b',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Export personal data'),
|
||||
'url' => 'uexport',
|
||||
'selected' => (($a->argc == 1) && ($a->argv[0] === 'uexport') ? 'active' : ''),
|
||||
'accesskey' => 'e',
|
||||
];
|
||||
|
||||
$tabs[] = [
|
||||
'label' => L10n::t('Remove account'),
|
||||
'url' => 'removeme',
|
||||
'selected' => (($a->argc == 1) && ($a->argv[0] === 'removeme') ? 'active' : ''),
|
||||
'accesskey' => 'r',
|
||||
];
|
||||
|
||||
|
||||
$tabtpl = Renderer::getMarkupTemplate("generic_links_widget.tpl");
|
||||
$a->page['aside'] = Renderer::replaceMacros($tabtpl, [
|
||||
'$title' => L10n::t('Settings'),
|
||||
'$class' => 'settings-widget',
|
||||
'$items' => $tabs,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
108
src/Module/Settings/TwoFactor/Index.php
Normal file
108
src/Module/Settings/TwoFactor/Index.php
Normal 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'),
|
||||
]);
|
||||
}
|
||||
}
|
86
src/Module/Settings/TwoFactor/Recovery.php
Normal file
86
src/Module/Settings/TwoFactor/Recovery.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace Friendica\Module\Settings\TwoFactor;
|
||||
|
||||
|
||||
use Friendica\Core\L10n;
|
||||
use Friendica\Core\PConfig;
|
||||
use Friendica\Core\Renderer;
|
||||
use Friendica\Model\TwoFactorRecoveryCode;
|
||||
use Friendica\Module\BaseSettingsModule;
|
||||
use Friendica\Module\Login;
|
||||
|
||||
/**
|
||||
* // Page 3: 2FA enabled but not verified, show recovery codes
|
||||
*
|
||||
* @package Friendica\Module\TwoFactor
|
||||
*/
|
||||
class Recovery extends BaseSettingsModule
|
||||
{
|
||||
public static function init()
|
||||
{
|
||||
if (!local_user()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$secret = PConfig::get(local_user(), '2fa', 'secret');
|
||||
|
||||
if (!$secret) {
|
||||
self::getApp()->internalRedirect('settings/2fa');
|
||||
}
|
||||
|
||||
if (!self::checkFormSecurityToken('settings_2fa_password', 't')) {
|
||||
notice(L10n::t('Please enter your password to access this page.'));
|
||||
self::getApp()->internalRedirect('settings/2fa');
|
||||
}
|
||||
}
|
||||
|
||||
public static function post()
|
||||
{
|
||||
if (!local_user()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!empty($_POST['action'])) {
|
||||
self::checkFormSecurityTokenRedirectOnError('settings/2fa/recovery', 'settings_2fa_recovery');
|
||||
|
||||
if ($_POST['action'] == 'regenerate') {
|
||||
TwoFactorRecoveryCode::regenerateForUser(local_user());
|
||||
notice(L10n::t('New recovery codes successfully generated.'));
|
||||
self::getApp()->internalRedirect('settings/2fa/recovery?t=' . self::getFormSecurityToken('settings_2fa_password'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function content()
|
||||
{
|
||||
if (!local_user()) {
|
||||
return Login::form('settings/2fa/recovery');
|
||||
}
|
||||
|
||||
parent::content();
|
||||
|
||||
if (!TwoFactorRecoveryCode::countValidForUser(local_user())) {
|
||||
TwoFactorRecoveryCode::generateForUser(local_user());
|
||||
}
|
||||
|
||||
$recoveryCodes = TwoFactorRecoveryCode::getListForUser(local_user());
|
||||
|
||||
$verified = PConfig::get(local_user(), '2fa', 'verified');
|
||||
|
||||
return Renderer::replaceMacros(Renderer::getMarkupTemplate('settings/twofactor/recovery.tpl'), [
|
||||
'$form_security_token' => self::getFormSecurityToken('settings_2fa_recovery'),
|
||||
'$password_security_token' => self::getFormSecurityToken('settings_2fa_password'),
|
||||
|
||||
'$title' => L10n::t('Two-factor recovery codes'),
|
||||
'$help_label' => L10n::t('Help'),
|
||||
'$message' => L10n::t('<p>Recovery codes can be used to access your account in the event you lose access to your device and cannot receive two-factor authentication codes.</p><p><strong>Put these in a safe spot!</strong> If you lose your device and don’t have the recovery codes you will lose access to your account.</p>'),
|
||||
'$recovery_codes' => $recoveryCodes,
|
||||
'$regenerate_message' => L10n::t('When you generate new recovery codes, you must copy the new codes. Your old codes won’t work anymore.'),
|
||||
'$regenerate_label' => L10n::t('Generate new recovery codes'),
|
||||
'$verified' => $verified,
|
||||
'$verify_label' => L10n::t('Next: Verification'),
|
||||
]);
|
||||
}
|
||||
}
|
130
src/Module/Settings/TwoFactor/Verify.php
Normal file
130
src/Module/Settings/TwoFactor/Verify.php
Normal 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'),
|
||||
]);
|
||||
}
|
||||
}
|
72
src/Module/TwoFactor/Recovery.php
Normal file
72
src/Module/TwoFactor/Recovery.php
Normal 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('Don’t 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'),
|
||||
]);
|
||||
}
|
||||
}
|
67
src/Module/TwoFactor/Verify.php
Normal file
67
src/Module/TwoFactor/Verify.php
Normal 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('Don’t 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'),
|
||||
]);
|
||||
}
|
||||
}
|
39
view/templates/settings/twofactor/index.tpl
Normal file
39
view/templates/settings/twofactor/index.tpl
Normal file
|
@ -0,0 +1,39 @@
|
|||
<div class="generic-page-wrapper">
|
||||
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||
<div>{{$message nofilter}}</div>
|
||||
<h2>{{$status_title}}</h2>
|
||||
<p><strong>{{$auth_app_label}}</strong>: {{$app_status}} </p>
|
||||
{{if $has_secret && $verified}}
|
||||
<div>{{$configured_message nofilter}}</div>
|
||||
{{/if}}
|
||||
{{if $has_secret && !$verified}}
|
||||
<div>{{$not_configured_message nofilter}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{if $has_secret && $verified}}
|
||||
<h2>{{$recovery_codes_title}}</h2>
|
||||
<p><strong>{{$recovery_codes_remaining}}</strong>: {{$recovery_codes_count}}</p>
|
||||
<div>{{$recovery_codes_message nofilter}}</div>
|
||||
{{/if}}
|
||||
|
||||
<form action="settings/2fa" method="post">
|
||||
<h2>{{$action_title}}</h2>
|
||||
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||
|
||||
{{include file="field_password.tpl" field=$password}}
|
||||
|
||||
<div class="form-group settings-submit-wrapper" >
|
||||
{{if !$has_secret}}
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="enable">{{$enable_label}}</button>
|
||||
{{else}}
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="disable">{{$disable_label}}</button>
|
||||
{{/if}}
|
||||
{{if $has_secret && $verified}}
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="recovery">{{$recovery_codes_label}}</button>
|
||||
{{/if}}
|
||||
{{if $has_secret && !$verified}}
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="configure">{{$configure_label}}</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
28
view/templates/settings/twofactor/recovery.tpl
Normal file
28
view/templates/settings/twofactor/recovery.tpl
Normal file
|
@ -0,0 +1,28 @@
|
|||
<div class="generic-page-wrapper">
|
||||
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||
<div>{{$message nofilter}}</div>
|
||||
|
||||
<ul class="recovery-codes">
|
||||
{{foreach $recovery_codes as $recovery_code}}
|
||||
<li>
|
||||
{{if $recovery_code.used}}<s>{{/if}}
|
||||
{{$recovery_code.code}}
|
||||
{{if $recovery_code.used}}</s>{{/if}}
|
||||
</li>
|
||||
{{/foreach}}
|
||||
</ul>
|
||||
|
||||
{{if $verified}}
|
||||
<form action="settings/2fa/recovery?t={{$password_security_token}}" method="post">
|
||||
<h2>{{$regenerate_label}}</h2>
|
||||
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||
<div>{{$regenerate_message}}</div>
|
||||
|
||||
<div class="form-group pull-right settings-submit-wrapper" >
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="regenerate">{{$regenerate_label}}</button>
|
||||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<p class="text-right"><a href="settings/2fa/verify?t={{$password_security_token}}" class="btn btn-primary">{{$verify_label}}</a></p>
|
||||
{{/if}}
|
||||
</div>
|
22
view/templates/settings/twofactor/verify.tpl
Normal file
22
view/templates/settings/twofactor/verify.tpl
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="generic-page-wrapper">
|
||||
<h1>{{$title}} <a href="help/Two-Factor-Authentication" title="{{$help_label}}" class="btn btn-default btn-sm"><i aria-hidden="true" class="fa fa-question fa-2x"></i></a></h1>
|
||||
<div>{{$message nofilter}}</div>
|
||||
|
||||
<div class="text-center">
|
||||
{{$qrcode_image nofilter}}
|
||||
</div>
|
||||
|
||||
<form action="settings/2fa/verify?t={{$password_security_token}}" method="post">
|
||||
<input type="hidden" name="form_security_token" value="{{$form_security_token}}">
|
||||
|
||||
{{include file="field_input.tpl" field=$verify_code}}
|
||||
|
||||
<div class="form-group settings-submit-wrapper" >
|
||||
<button type="submit" name="action" id="confirm-submit-button" class="btn btn-primary confirm-button" value="verify">{{$verify_label}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>{{$qrcode_url_message nofilter}}</div>
|
||||
|
||||
<div>{{$manual_message nofilter}}</div>
|
||||
</div>
|
14
view/templates/twofactor/recovery.tpl
Normal file
14
view/templates/twofactor/recovery.tpl
Normal 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>
|
15
view/templates/twofactor/verify.tpl
Normal file
15
view/templates/twofactor/verify.tpl
Normal 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>
|
Loading…
Reference in a new issue