diff --git a/.env b/.env
index 9e2cfacc..c75a6113 100644
--- a/.env
+++ b/.env
@@ -15,8 +15,10 @@
APP_NAME="Userli"
APP_URL="https://users.example.org"
+APP_DOMAIN="users.example.org"
PROJECT_NAME="example.org"
PROJECT_URL="https://www.example.org"
+PROJECT_LOGO_URL="https://www.example.org/logo.png"
SENDER_ADDRESS="admin@example.org"
NOTIFICATION_ADDRESS="monitoring@example.org"
SEND_MAIL=1
diff --git a/UPGRADE.md b/UPGRADE.md
index bab8b320..7ec66904 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -1,5 +1,15 @@
# Upgrade documentation
+## Upgrade from 3.0.0 or lower
+
+The new twofactor authentication (2FA) feature requires the database schema to
+be updated:
+
+ ALTER TABLE virtual_users
+ ADD totp_confirmed TINYINT(1) DEFAULT 0 NOT NULL,
+ ADD totp_secret VARCHAR(255) DEFAULT NULL;
+ ADD totp_backup_codes LONGTEXT NOT NULL;
+
## Upgrade from 2.6.1 or lower
The new OpenPGP WKD feature requires GnuPG (>=2.1.14) to be installed.
diff --git a/assets/css/app.css b/assets/css/app.css
index c0ecc7de..5dc98615 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -84,6 +84,10 @@ body {
margin-bottom: 10px;
}
+.alert-header {
+ text-align: center;
+}
+
.flash-notification {
position: absolute;
right: 10px;
@@ -96,3 +100,8 @@ body {
.ascii {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
+
+.twofactor-backup-codes {
+ white-space: pre-line;
+ padding-left: 15px;
+}
diff --git a/composer.json b/composer.json
index cc881b70..b59a43eb 100644
--- a/composer.json
+++ b/composer.json
@@ -19,6 +19,10 @@
"nelmio/security-bundle": "^2.5",
"pear/crypt_gpg": "^1.6",
"ramsey/uuid": "^4.1",
+ "scheb/2fa-backup-code": "^5.13",
+ "scheb/2fa-bundle": "^5.13",
+ "scheb/2fa-qr-code": "^5.13",
+ "scheb/2fa-totp": "^5.13",
"sensio/framework-extra-bundle": "^5.0.0",
"sonata-project/admin-bundle": "3.75.0",
"sonata-project/doctrine-orm-admin-bundle": "^3.24",
diff --git a/composer.lock b/composer.lock
index 80fa5ec8..b6912ca4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,129 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e3b8286b96a428bf53060a7569ba379f",
+ "content-hash": "cf6ece46cbcabba74de66e66ee6722d4",
"packages": [
+ {
+ "name": "bacon/bacon-qr-code",
+ "version": "2.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Bacon/BaconQrCode.git",
+ "reference": "d70c840f68657ce49094b8d91f9ee0cc07fbf66c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/d70c840f68657ce49094b8d91f9ee0cc07fbf66c",
+ "reference": "d70c840f68657ce49094b8d91f9ee0cc07fbf66c",
+ "shasum": ""
+ },
+ "require": {
+ "dasprid/enum": "^1.0.3",
+ "ext-iconv": "*",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phly/keep-a-changelog": "^2.1",
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "spatie/phpunit-snapshot-assertions": "^4.2.9",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "suggest": {
+ "ext-imagick": "to generate QR code images"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BaconQrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "BaconQrCode is a QR code generator for PHP.",
+ "homepage": "https://github.com/Bacon/BaconQrCode",
+ "support": {
+ "issues": "https://github.com/Bacon/BaconQrCode/issues",
+ "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.7"
+ },
+ "time": "2022-03-14T02:02:36+00:00"
+ },
+ {
+ "name": "beberlei/assert",
+ "version": "v3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/beberlei/assert.git",
+ "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655",
+ "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "php": "^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "*",
+ "phpstan/phpstan": "*",
+ "phpunit/phpunit": ">=6.0.0",
+ "yoast/phpunit-polyfills": "^0.1.0"
+ },
+ "suggest": {
+ "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Assert/functions.php"
+ ],
+ "psr-4": {
+ "Assert\\": "lib/Assert"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Richard Quadling",
+ "email": "rquadling@gmail.com",
+ "role": "Collaborator"
+ }
+ ],
+ "description": "Thin assertion library for input validation in business models.",
+ "keywords": [
+ "assert",
+ "assertion",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/beberlei/assert/issues",
+ "source": "https://github.com/beberlei/assert/tree/v3.3.2"
+ },
+ "time": "2021-12-16T21:41:27+00:00"
+ },
{
"name": "brick/math",
"version": "0.9.3",
@@ -142,6 +263,53 @@
],
"time": "2022-05-24T11:56:16+00:00"
},
+ {
+ "name": "dasprid/enum",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DASPRiD/Enum.git",
+ "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/5abf82f213618696dda8e3bf6f64dd042d8542b2",
+ "reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2",
+ "shasum": ""
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "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/",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP 7.1 enum implementation",
+ "keywords": [
+ "enum",
+ "map"
+ ],
+ "support": {
+ "issues": "https://github.com/DASPRiD/Enum/issues",
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.3"
+ },
+ "time": "2020-10-02T16:03:48+00:00"
+ },
{
"name": "doctrine/annotations",
"version": "1.13.2",
@@ -1658,6 +1826,81 @@
],
"time": "2022-06-18T20:57:19+00:00"
},
+ {
+ "name": "endroid/qr-code",
+ "version": "3.9.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/endroid/qr-code.git",
+ "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/endroid/qr-code/zipball/94563d7b3105288e6ac53a67ae720e3669fac1f6",
+ "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6",
+ "shasum": ""
+ },
+ "require": {
+ "bacon/bacon-qr-code": "^2.0",
+ "khanamiryan/qrcode-detector-decoder": "^1.0.5",
+ "myclabs/php-enum": "^1.5",
+ "php": "^7.3||^8.0",
+ "symfony/options-resolver": "^3.4||^4.4||^5.0",
+ "symfony/property-access": "^3.4||^4.4||^5.0"
+ },
+ "require-dev": {
+ "endroid/quality": "^1.5.2",
+ "setasign/fpdf": "^1.8"
+ },
+ "suggest": {
+ "ext-gd": "Required for generating PNG images",
+ "roave/security-advisories": "Avoids installation of package versions with vulnerabilities",
+ "setasign/fpdf": "Required to use the FPDF writer.",
+ "symfony/security-checker": "Checks your composer.lock for vulnerabilities"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Endroid\\QrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeroen van den Enden",
+ "email": "info@endroid.nl"
+ }
+ ],
+ "description": "Endroid QR Code",
+ "homepage": "https://github.com/endroid/qr-code",
+ "keywords": [
+ "bundle",
+ "code",
+ "endroid",
+ "php",
+ "qr",
+ "qrcode"
+ ],
+ "support": {
+ "issues": "https://github.com/endroid/qr-code/issues",
+ "source": "https://github.com/endroid/qr-code/tree/3.9.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/endroid",
+ "type": "github"
+ }
+ ],
+ "time": "2021-04-20T19:10:54+00:00"
+ },
{
"name": "ircmaxell/password-compat",
"version": "v1.0.4",
@@ -1862,6 +2105,61 @@
],
"time": "2021-12-28T20:59:55+00:00"
},
+ {
+ "name": "khanamiryan/qrcode-detector-decoder",
+ "version": "1.0.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git",
+ "reference": "04fdd58d86a387065f707dc6d3cc304c719910c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/khanamiryan/php-qrcode-detector-decoder/zipball/04fdd58d86a387065f707dc6d3cc304c719910c1",
+ "reference": "04fdd58d86a387065f707dc6d3cc304c719910c1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 | ^7.5 | ^8.0 | ^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Common/customFunctions.php"
+ ],
+ "psr-4": {
+ "Zxing\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT",
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Ashot Khanamiryan",
+ "email": "a.khanamiryan@gmail.com",
+ "homepage": "https://github.com/khanamiryan",
+ "role": "Developer"
+ }
+ ],
+ "description": "QR code decoder / reader",
+ "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/",
+ "keywords": [
+ "barcode",
+ "qr",
+ "zxing"
+ ],
+ "support": {
+ "issues": "https://github.com/khanamiryan/php-qrcode-detector-decoder/issues",
+ "source": "https://github.com/khanamiryan/php-qrcode-detector-decoder/tree/1.0.5.2"
+ },
+ "time": "2021-07-13T18:46:38+00:00"
+ },
{
"name": "knplabs/knp-menu",
"version": "v3.3.0",
@@ -2217,6 +2515,66 @@
},
"time": "2015-10-01T19:20:19+00:00"
},
+ {
+ "name": "myclabs/php-enum",
+ "version": "1.8.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/php-enum.git",
+ "reference": "b942d263c641ddb5190929ff840c68f78713e937"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937",
+ "reference": "b942d263c641ddb5190929ff840c68f78713e937",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "1.*",
+ "vimeo/psalm": "^4.6.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MyCLabs\\Enum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP Enum contributors",
+ "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+ }
+ ],
+ "description": "PHP Enum implementation",
+ "homepage": "http://github.com/myclabs/php-enum",
+ "keywords": [
+ "enum"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/php-enum/issues",
+ "source": "https://github.com/myclabs/php-enum/tree/1.8.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-05T08:18:36+00:00"
+ },
{
"name": "nelmio/security-bundle",
"version": "v2.12.0",
@@ -2285,6 +2643,73 @@
},
"time": "2022-02-23T06:10:58+00:00"
},
+ {
+ "name": "paragonie/constant_time_encoding",
+ "version": "v2.6.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/constant_time_encoding.git",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
+ "reference": "58c3f47f650c94ec05a151692652a868995d2938",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7|^8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParagonIE\\ConstantTime\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com",
+ "role": "Maintainer"
+ },
+ {
+ "name": "Steve 'Sc00bz' Thomas",
+ "email": "steve@tobtu.com",
+ "homepage": "https://www.tobtu.com",
+ "role": "Original Developer"
+ }
+ ],
+ "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+ "keywords": [
+ "base16",
+ "base32",
+ "base32_decode",
+ "base32_encode",
+ "base64",
+ "base64_decode",
+ "base64_encode",
+ "bin2hex",
+ "encoding",
+ "hex",
+ "hex2bin",
+ "rfc4648"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/constant_time_encoding/issues",
+ "source": "https://github.com/paragonie/constant_time_encoding"
+ },
+ "time": "2022-06-14T06:56:20+00:00"
+ },
{
"name": "pear/console_commandline",
"version": "v1.2.4",
@@ -2843,6 +3268,221 @@
],
"time": "2021-09-25T23:10:38+00:00"
},
+ {
+ "name": "scheb/2fa-backup-code",
+ "version": "v5.13.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/scheb/2fa-backup-code.git",
+ "reference": "5584eb7a2c3deb80635c7173ad77858e51129c35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/scheb/2fa-backup-code/zipball/5584eb7a2c3deb80635c7173ad77858e51129c35",
+ "reference": "5584eb7a2c3deb80635c7173ad77858e51129c35",
+ "shasum": ""
+ },
+ "require": {
+ "scheb/2fa-bundle": "self.version"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Scheb\\TwoFactorBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Scheb",
+ "email": "me@christianscheb.de"
+ }
+ ],
+ "description": "Extends scheb/2fa-bundle with backup codes support",
+ "homepage": "https://github.com/scheb/2fa",
+ "keywords": [
+ "2fa",
+ "Authentication",
+ "backup-codes",
+ "symfony",
+ "two-factor",
+ "two-step"
+ ],
+ "support": {
+ "source": "https://github.com/scheb/2fa-backup-code/tree/v5.13.2"
+ },
+ "time": "2022-01-03T10:21:24+00:00"
+ },
+ {
+ "name": "scheb/2fa-bundle",
+ "version": "v5.13.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/scheb/2fa-bundle.git",
+ "reference": "dc575cc7bc94fa3a52b547698086f2ef015d2e81"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/scheb/2fa-bundle/zipball/dc575cc7bc94fa3a52b547698086f2ef015d2e81",
+ "reference": "dc575cc7bc94fa3a52b547698086f2ef015d2e81",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": ">=7.2.5",
+ "symfony/config": "^4.4|^5.0",
+ "symfony/dependency-injection": "^4.4|^5.0",
+ "symfony/event-dispatcher": "^4.4|^5.0",
+ "symfony/framework-bundle": "^4.4|^5.0",
+ "symfony/http-foundation": "^4.4|^5.0",
+ "symfony/http-kernel": "^4.4|^5.0",
+ "symfony/property-access": "^4.4|^5.0",
+ "symfony/security-bundle": "^4.4.1|^5.0",
+ "symfony/twig-bundle": "^4.4|^5.0"
+ },
+ "conflict": {
+ "scheb/two-factor-bundle": "*"
+ },
+ "suggest": {
+ "scheb/2fa-backup-code": "Emergency codes when you have no access to other methods",
+ "scheb/2fa-email": "Send codes by email",
+ "scheb/2fa-google-authenticator": "Google Authenticator support",
+ "scheb/2fa-qr-code": "Generate QR codes for Google Authenticator / TOTP",
+ "scheb/2fa-totp": "Temporary one-time password (TOTP) support (Google Authenticator compatible)",
+ "scheb/2fa-trusted-device": "Trusted devices support"
+ },
+ "type": "symfony-bundle",
+ "autoload": {
+ "psr-4": {
+ "Scheb\\TwoFactorBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Scheb",
+ "email": "me@christianscheb.de"
+ }
+ ],
+ "description": "A generic interface to implement two-factor authentication in Symfony applications",
+ "homepage": "https://github.com/scheb/2fa",
+ "keywords": [
+ "2fa",
+ "Authentication",
+ "symfony",
+ "two-factor",
+ "two-step"
+ ],
+ "support": {
+ "source": "https://github.com/scheb/2fa-bundle/tree/v5.13.2"
+ },
+ "time": "2022-04-16T10:18:34+00:00"
+ },
+ {
+ "name": "scheb/2fa-qr-code",
+ "version": "v5.13.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/scheb/2fa-qr-code.git",
+ "reference": "1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/scheb/2fa-qr-code/zipball/1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf",
+ "reference": "1b30d3f32c443c20ddbd4830c7b41dfd17e4dcaf",
+ "shasum": ""
+ },
+ "require": {
+ "endroid/qr-code": "^3.0",
+ "scheb/2fa-bundle": "self.version"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Scheb\\TwoFactorBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Scheb",
+ "email": "me@christianscheb.de"
+ }
+ ],
+ "description": "Extends scheb/2fa-bundle with the ability to render QR-codes for Google Authenticator or TOTP",
+ "homepage": "https://github.com/scheb/2fa",
+ "keywords": [
+ "2fa",
+ "Authentication",
+ "qr-code",
+ "symfony",
+ "two-factor",
+ "two-step"
+ ],
+ "support": {
+ "source": "https://github.com/scheb/2fa-qr-code/tree/v5.13.2"
+ },
+ "time": "2022-01-03T10:21:24+00:00"
+ },
+ {
+ "name": "scheb/2fa-totp",
+ "version": "v5.13.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/scheb/2fa-totp.git",
+ "reference": "6b03afbfeedd3e6fab491690a9702410e2770244"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/scheb/2fa-totp/zipball/6b03afbfeedd3e6fab491690a9702410e2770244",
+ "reference": "6b03afbfeedd3e6fab491690a9702410e2770244",
+ "shasum": ""
+ },
+ "require": {
+ "paragonie/constant_time_encoding": "^2.2",
+ "scheb/2fa-bundle": "self.version",
+ "spomky-labs/otphp": "^9.1|^10.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Scheb\\TwoFactorBundle\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Scheb",
+ "email": "me@christianscheb.de"
+ }
+ ],
+ "description": "Extends scheb/2fa-bundle with two-factor authentication using TOTP",
+ "homepage": "https://github.com/scheb/2fa",
+ "keywords": [
+ "2fa",
+ "Authentication",
+ "symfony",
+ "totp",
+ "two-factor",
+ "two-step"
+ ],
+ "support": {
+ "source": "https://github.com/scheb/2fa-totp/tree/v5.13.2"
+ },
+ "time": "2022-01-03T10:21:24+00:00"
+ },
{
"name": "sensio/framework-extra-bundle",
"version": "v5.6.1",
@@ -3823,6 +4463,81 @@
],
"time": "2022-05-21T21:48:39+00:00"
},
+ {
+ "name": "spomky-labs/otphp",
+ "version": "v10.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Spomky-Labs/otphp.git",
+ "reference": "9784d9f7c790eed26e102d6c78f12c754036c366"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/9784d9f7c790eed26e102d6c78f12c754036c366",
+ "reference": "9784d9f7c790eed26e102d6c78f12c754036c366",
+ "shasum": ""
+ },
+ "require": {
+ "beberlei/assert": "^3.0",
+ "ext-mbstring": "*",
+ "paragonie/constant_time_encoding": "^2.0",
+ "php": "^7.2|^8.0",
+ "thecodingmachine/safe": "^0.1.14|^1.0|^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-beberlei-assert": "^0.12",
+ "phpstan/phpstan-deprecation-rules": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpstan/phpstan-strict-rules": "^0.12",
+ "phpunit/phpunit": "^8.0",
+ "thecodingmachine/phpstan-safe-rule": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "v10.0": "10.0.x-dev",
+ "v9.0": "9.0.x-dev",
+ "v8.3": "8.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OTPHP\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Florent Morselli",
+ "homepage": "https://github.com/Spomky"
+ },
+ {
+ "name": "All contributors",
+ "homepage": "https://github.com/Spomky-Labs/otphp/contributors"
+ }
+ ],
+ "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator",
+ "homepage": "https://github.com/Spomky-Labs/otphp",
+ "keywords": [
+ "FreeOTP",
+ "RFC 4226",
+ "RFC 6238",
+ "google authenticator",
+ "hotp",
+ "otp",
+ "totp"
+ ],
+ "support": {
+ "issues": "https://github.com/Spomky-Labs/otphp/issues",
+ "source": "https://github.com/Spomky-Labs/otphp/tree/v10.0.3"
+ },
+ "time": "2022-03-17T08:00:35+00:00"
+ },
{
"name": "swiftmailer/swiftmailer",
"version": "v6.3.0",
@@ -8818,6 +9533,145 @@
],
"time": "2022-06-20T08:31:17+00:00"
},
+ {
+ "name": "thecodingmachine/safe",
+ "version": "v1.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thecodingmachine/safe.git",
+ "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
+ "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12",
+ "squizlabs/php_codesniffer": "^3.2",
+ "thecodingmachine/phpstan-strict-rules": "^0.12"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.1-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "deprecated/apc.php",
+ "deprecated/libevent.php",
+ "deprecated/mssql.php",
+ "deprecated/stats.php",
+ "lib/special_cases.php",
+ "generated/apache.php",
+ "generated/apcu.php",
+ "generated/array.php",
+ "generated/bzip2.php",
+ "generated/calendar.php",
+ "generated/classobj.php",
+ "generated/com.php",
+ "generated/cubrid.php",
+ "generated/curl.php",
+ "generated/datetime.php",
+ "generated/dir.php",
+ "generated/eio.php",
+ "generated/errorfunc.php",
+ "generated/exec.php",
+ "generated/fileinfo.php",
+ "generated/filesystem.php",
+ "generated/filter.php",
+ "generated/fpm.php",
+ "generated/ftp.php",
+ "generated/funchand.php",
+ "generated/gmp.php",
+ "generated/gnupg.php",
+ "generated/hash.php",
+ "generated/ibase.php",
+ "generated/ibmDb2.php",
+ "generated/iconv.php",
+ "generated/image.php",
+ "generated/imap.php",
+ "generated/info.php",
+ "generated/ingres-ii.php",
+ "generated/inotify.php",
+ "generated/json.php",
+ "generated/ldap.php",
+ "generated/libxml.php",
+ "generated/lzf.php",
+ "generated/mailparse.php",
+ "generated/mbstring.php",
+ "generated/misc.php",
+ "generated/msql.php",
+ "generated/mysql.php",
+ "generated/mysqli.php",
+ "generated/mysqlndMs.php",
+ "generated/mysqlndQc.php",
+ "generated/network.php",
+ "generated/oci8.php",
+ "generated/opcache.php",
+ "generated/openssl.php",
+ "generated/outcontrol.php",
+ "generated/password.php",
+ "generated/pcntl.php",
+ "generated/pcre.php",
+ "generated/pdf.php",
+ "generated/pgsql.php",
+ "generated/posix.php",
+ "generated/ps.php",
+ "generated/pspell.php",
+ "generated/readline.php",
+ "generated/rpminfo.php",
+ "generated/rrd.php",
+ "generated/sem.php",
+ "generated/session.php",
+ "generated/shmop.php",
+ "generated/simplexml.php",
+ "generated/sockets.php",
+ "generated/sodium.php",
+ "generated/solr.php",
+ "generated/spl.php",
+ "generated/sqlsrv.php",
+ "generated/ssdeep.php",
+ "generated/ssh2.php",
+ "generated/stream.php",
+ "generated/strings.php",
+ "generated/swoole.php",
+ "generated/uodbc.php",
+ "generated/uopz.php",
+ "generated/url.php",
+ "generated/var.php",
+ "generated/xdiff.php",
+ "generated/xml.php",
+ "generated/xmlrpc.php",
+ "generated/yaml.php",
+ "generated/yaz.php",
+ "generated/zip.php",
+ "generated/zlib.php"
+ ],
+ "psr-4": {
+ "Safe\\": [
+ "lib/",
+ "deprecated/",
+ "generated/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
+ "support": {
+ "issues": "https://github.com/thecodingmachine/safe/issues",
+ "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3"
+ },
+ "time": "2020-10-28T17:51:34+00:00"
+ },
{
"name": "tuupola/base32",
"version": "1.0.0",
diff --git a/config/bundles.php b/config/bundles.php
index 7990d619..4e72d160 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -7,6 +7,7 @@
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
Mopa\Bundle\BootstrapBundle\MopaBootstrapBundle::class => ['all' => true],
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
+ Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
Sonata\BlockBundle\SonataBlockBundle::class => ['all' => true],
Sonata\AdminBundle\SonataAdminBundle::class => ['all' => true],
diff --git a/config/doctrine/User.orm.yml b/config/doctrine/User.orm.yml
index 2cff851a..774ef902 100644
--- a/config/doctrine/User.orm.yml
+++ b/config/doctrine/User.orm.yml
@@ -51,6 +51,15 @@ App\Entity\User:
mailCryptPublicKey:
type: text
nullable: true
+ totpSecret:
+ type: string
+ nullable: true
+ totpConfirmed:
+ type: boolean
+ options:
+ default: 0
+ totpBackupCodes:
+ type: array
manyToOne:
domain:
targetEntity: Domain
diff --git a/config/packages/scheb_2fa.yaml b/config/packages/scheb_2fa.yaml
new file mode 100644
index 00000000..801d46f8
--- /dev/null
+++ b/config/packages/scheb_2fa.yaml
@@ -0,0 +1,17 @@
+# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/5.x/configuration.html
+scheb_two_factor:
+ # Google Authenticator config
+ totp:
+ enabled: true
+ server_name: "%env(APP_DOMAIN)%"
+ issuer: "%env(PROJECT_NAME)%"
+ template: Security/2fa_form.html.twig
+ parameters:
+ image: "%env(PROJECT_LOGO_URL)%"
+ # Backup codes config
+ backup_codes:
+ enabled: true
+
+ # The security token classes, which trigger two-factor authentication.
+ security_tokens:
+ - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index 26f677d6..ec1e6792 100755
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -109,6 +109,10 @@ security:
logout:
success_handler: App\Handler\LogoutSuccessHandler
invalidate_session: false
+ two_factor:
+ auth_form_path: 2fa_login
+ check_path: 2fa_login_check
+ enable_csrf: true
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
@@ -120,12 +124,16 @@ security:
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: ^/logout, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/[a-z]{2}/init", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/[a-z]{2}/login", roles: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: "^/[a-z]{2}/logout", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/[a-z]{2}/recovery", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/[a-z]{2}/register", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/[a-z]{2}/$", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
+ - { path: ^/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }
+ - { path: "^/[a-z]{2}/2fa", roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/admin, roles: ROLE_DOMAIN_ADMIN }
- { path: "^/[a-z]{2}/voucher", roles: ROLE_USER, allow_if: "!is_granted('ROLE_SUSPICIOUS')"}
- { path: "^/[a-z]{2}/alias", roles: ROLE_USER, allow_if: "!is_granted('ROLE_SPAM')"}
diff --git a/config/routes.yaml b/config/routes.yaml
index c5f772d7..a2261937 100644
--- a/config/routes.yaml
+++ b/config/routes.yaml
@@ -108,6 +108,38 @@ user_recovery_token_ack:
requirements:
_locale: '%locales%'
+# TwofactorController
+
+user_twofactor:
+ path: /{_locale}/user/twofactor
+ controller: App\Controller\TwofactorController::twofactorAction
+ requirements:
+ _locale: '%locales%'
+
+user_twofactor_confirm:
+ path: /{_locale}/user/twofactor_confirm
+ controller: App\Controller\TwofactorController::twofactorConfirmAction
+ requirements:
+ _locale: '%locales%'
+
+user_twofactor_backup_ack:
+ path: /{_locale}/user/twofactor_backup_codes
+ controller: App\Controller\TwofactorController::twofactorBackupAckAction
+ requirements:
+ _locale: '%locales%'
+
+user_twofactor_disable:
+ path: /{_locale}/user/twofactor_disable
+ controller: App\Controller\TwofactorController::twofactorDisableAction
+ requirements:
+ _locale: '%locales%'
+
+user_twofactor_qrcode:
+ path: /{_locale}/user/twofactor/qrcode
+ controller: App\Controller\TwofactorController::displayTotpQrCode
+ requirements:
+ _locale: '%locales%'
+
## InitController
init:
path: /{_locale}/init
diff --git a/config/routes/scheb_2fa.yaml b/config/routes/scheb_2fa.yaml
new file mode 100644
index 00000000..53acd388
--- /dev/null
+++ b/config/routes/scheb_2fa.yaml
@@ -0,0 +1,19 @@
+2fa_login:
+ path: /{_locale}/2fa
+ defaults:
+ _controller: "scheb_two_factor.form_controller::form"
+ requirements:
+ _locale: '%locales%'
+
+2fa_login_check:
+ path: /{_locale}/2fa_check
+ requirements:
+ _locale: '%locales%'
+
+2fa_login_fallback:
+ path: /2fa
+ defaults:
+ _controller: "scheb_two_factor.form_controller::form"
+
+2fa_login_check_fallback:
+ path: /2fa_check
diff --git a/config/validator/validation.yaml b/config/validator/validation.yaml
index f4e6d328..63e66089 100644
--- a/config/validator/validation.yaml
+++ b/config/validator/validation.yaml
@@ -94,3 +94,21 @@ App\Form\Model\RecoveryResetPassword:
- App\Validator\Constraints\PasswordPolicy: ~
- NotCompromisedPassword:
skipOnError: true
+
+App\Form\Model\Twofactor:
+ properties:
+ password:
+ - Symfony\Component\Security\Core\Validator\Constraints\UserPassword:
+ message: form.wrong-password
+
+App\Form\Model\TwofactorConfirm:
+ properties:
+ totpSecret:
+ - NotNull: ~
+ - App\Validator\Constraints\TotpSecret: ~
+
+App\Form\Model\TwofactorBackupAck:
+ properties:
+ ack:
+ - IsTrue:
+ message: form.twofactor-backup-ack-missing
diff --git a/default_translations/de/messages.de.yml b/default_translations/de/messages.de.yml
index 2b27a20b..1a045c61 100644
--- a/default_translations/de/messages.de.yml
+++ b/default_translations/de/messages.de.yml
@@ -3,8 +3,9 @@ start:
intro: Hier kannst du dein E-Mail-Konto erstellen und verwalten.
registration-header: Registrierung
registration-text: >
- Du hast noch kein E-Mail-Konto bei %domain%? Kein Problem! Lass dir von einer
- Freund*in einen Einladungscode schicken. Damit kannst du sofort ein Konto erstellen.
+ Du hast noch kein E-Mail-Konto bei %domain%? Kein Problem!
+ Lass dir von einer Freund:in einen Einladungscode schicken. Damit kannst
+ du sofort ein Konto erstellen.
registration-button: Konto anlegen
account-settings: Konto verwalten
account-settings-desc: Passwörter und mehr
@@ -13,7 +14,7 @@ start:
aliases-delete: Alias-Adresse löschen
aliases-desc: Verschleiere deine Identität
vouchers: Einladungscodes
- vouchers-desc: Lade Freund*innen ein
+ vouchers-desc: Lade Freund:innen ein
webmail: Webmail
webmail-desc: Lies und schreib E-Mails
openpgp-delete: OpenPGP-Schlüssel löschen
@@ -25,13 +26,14 @@ index:
title: Verwalte dein E-Mail-Konto
voucher-headline: Deine Einladungscodes
voucher-limit: Du hast keine Einladungscodes mehr verfügbar.
- voucher-disable: "Du bekommst am %date% Uhr drei Einladungscodes gutgeschrieben.\
- \ Dies dient der Vermeidung von Massenregistrierungen und Spam.\n"
+ voucher-disable: >
+ Du bekommst am %date% Uhr drei Einladungscodes gutgeschrieben.
+ Dies dient der Vermeidung von Massenregistrierungen und Spam.
voucher-explanation: >
- Nutze deine Einladungscodes um Freund*innen hierher einzuladen. Standardmäßig
- hast du drei Einladungen zur Verfügung. Wenn du mehr brauchst, wende dich an die
- Admins. Wir speichern Einladungsdaten für einen begrenzten Zeitraum um Missbrauch
- zu verhindern.
+ Nutze deine Einladungscodes um Freund:innen hierher einzuladen.
+ Standardmäßig hast du drei Einladungen zur Verfügung. Wenn du mehr
+ brauchst, wende dich an die Admins. Wir speichern Einladungsdaten für
+ einen begrenzten Zeitraum um Missbrauch zu verhindern.
alias-headline: Deine %alias_type% Alias-Adressen
alias-limit: Du hast das Limit von %alias_limit% erlaubten %alias_type% Alias-Adressen
erreicht.
@@ -42,7 +44,7 @@ index:
delete-description: >
Du kannst dein Konto bei %project_name% löschen. Dadurch werden deine E-Mails
unwiderruflich gelöscht. Eine spätere Wiederherstellung ist nicht möglich.
-
Um dein Konto zu löschen, wirst du aufgefordert dein Passwort einzugeben.
+
Um dein Konto zu löschen, wirst du aufgefordert dein Passwort einzugeben.
delete-button: Konto löschen
recovery-token-button: Wiederherstellungscode verwalten
logged_in_as: Du bist angemeldet als %user%.
@@ -73,6 +75,8 @@ form:
oclock-by: Uhr von
actual-password: Aktuelles Passwort
new-custom-alias: Neue Alias-Adresse
+ plain-password: Neues Passwort
+ plain-password_confirmation: Neues Passwort bestätigen
change-password: Passwort ändern
delete-account: Konto löschen
delete-password: Passwort
@@ -85,10 +89,8 @@ form:
recovery-start: Wiederherstellen
registration-recovery-token-ack: Ich habe den Code an einem sicheren Ort abgespeichert
registration-recovery-token-next-button: Weiter
+ domain: Domain
add: Hinzufügen
- domain: Domäne
- plain-password_confirmation: Neues Passwort bestätigen
- plain-password: Neues Passwort
openpgp-key-file: "Deinen Schlüssel als Datei hochladen:"
openpgp-key-file-mimetype: Die hochgeladende Datei enthält keine OpenPGP-Schlüssel.
openpgp-key-upload-or: oder
@@ -98,6 +100,39 @@ form:
Format hoch.
openpgp-key-submit: OpenPGP-Schlüssel veröffentlichen
openpgp-delete: OpenPGP-Schlüssel löschen
+ verify: Bestätigen
+ twofactor: Zwei-Faktor-Authentifizierung
+ twofactor-short: 2FA
+ twofactor-login: Zwei-Faktor-Authentifizierung
+ twofactor-login-placeholder: 6-stelliger Code
+ twofactor-login-auth-code: Authentifizierungs-Code
+ twofactor-backup-code-ack: Ich habe die Backup-Codes an einem sicheren Ort abgelegt.
+ twofactor-login-desc: >
+ Öffne deine Zwei-Faktor (TOTP) App für den Code.
+ twofactor-login-cancel: Login abbrechen
+ twofactor-enable: Zwei-Faktor-Authentifizierung aktivieren
+ twofactor-disable: Zwei-Faktor-Authentifizierung deaktivieren
+
+account:
+ twofactor:
+ headline: Zwei-Faktor-Authentifizierung
+ unset: Zwei-Faktor-Authentifizierung ist noch nicht aktiv.
+ unset-extra: Du brauchst eine TOTP App auf deinem Smartphone um Zwei-Faktor-Authentifizierung zu aktivieren.
+ set: Zwei-Faktor-Authentifizierung ist aktiv.
+ button: Zwei-Faktor-Authentifizierung konfigurieren
+ lead: Zwei-Faktor-Authentifizierung bringt zusätzliche Sicherheit für deinen Account.
+ desc-login: >
+ Mit Zwei-Faktor-Authentifizierung musst du beim Login ein zweites Passwort eingeben,
+ welches du von deiner Zwei-Faktor-App bekommst. Dieses Extra-Passwort ist nur zum
+ Login unter %app_url% nötig, nicht für andere Dienste wie etwa deine Mails.
+ desc-lost: >
+ Falls du Zugang zu deiner Zwei-Faktor-App verlierst, musst du dich mit Hilfe eines der
+ Zwei-Faktor Backup Codes anmelden.
+ enable-lead: >
+ Scanne das Bild mit deiner Zwei-Faktor-App.
+ backup-ack-lead: >
+ Backup-Codes für die Zwei-Faktor-Authentifizierung. Kopiere und speichere sie an einem sicheren Ort.
+
recovery:
header: Konto wiederherstellen
lead: Passwort zurücksetzen mit Hilfe des Wiederherstellungscodes
@@ -123,7 +158,7 @@ registration:
information: Hinweis
information-intro: >
Du möchtest ein E-Mail-Konto bei %project_name% registrieren.
- Eine Registrierung ist nur möglich, wenn dich eine Freund*in zu unserem Dienst
+ Eine Registrierung ist nur möglich, wenn dich eine Freund:in zu unserem Dienst
einlädt, dir also einen Einladungscode übergibt.
information-password-policy: >
Wichtig: Du benötigst ein Passwort, das aus mindestens 12 Zeichen
@@ -194,10 +229,9 @@ flashes:
alias-deletion-successful: Deine Alias-Adresse wurde gelöscht.
recovery-token-invalid: E-Mail-Adresse oder Wiederherstellungscode sind falsch!
recovery-token-ack: Du hast einen Wiederherstellungscode erzeugt.
- recovery-reauthenticate: Etwas ist schiefgelaufen. Du musst dich erneut authentifizieren!
+ revocery-reauthenticate: Etwas ist schief gelaufen. Bitte erneut authentifizieren!
recovery-password-changed: Dein Passwort wurde geändert.
recovery-next-login: Du kannst dich nun einloggen.
- revocery-reauthenticate: Etwas ist schief gelaufen. Bitte erneut authentifizieren!
openpgp-deletion-successful: Dein OpenPGP-Schlüssel wurde gelöscht.
openpgp-key-upload-successful: Dein OpenPGP-Schlüssel wurde hochgeladen.
openpgp-key-upload-error-no-openpgp: Die hochgadene Datei enthält keine OpenPGP-Schlüssel.
@@ -222,7 +256,7 @@ random: zufälligen
mail:
welcome-subject: Willkommen bei %domain%
welcome-body: |
- Liebe Nutzer*in,
+ Liebe Nutzer:in,
willkommen an Bord. Im Web findest du Anleitungen zur Einrichtung deines
E-Mail-Kontos mit verschiedenen Client-Programmen.
@@ -232,7 +266,7 @@ mail:
E-Mail-Kontos kommen.
In einer Woche erhälst du drei Einladungscodes, die du unter
- %app_url%/voucher abrufen kannst. Diese kannst du an Freund*innen
+ %app_url%/voucher abrufen kannst. Diese kannst du an Freund:innen
weiterleiten und sie so zu %project_name% einladen.
Viel Spaß mit deinem E-Mail-Konto, wünscht dir
@@ -281,23 +315,23 @@ Alias: Alias-Adresse
Voucher: Einladungscode
Reserved Name: Reservierter Name
copy-to-clipboard: In Zwischenablage kopieren
+init.title: Erstellen der Erstkonfiguration
+init_domain:
+ lead: Vielen Dank fürs installieren von Userli. Derzeit sind keine Domänen oder
+ Konten vorhanden.
+ text: Bitte wähle Deine primäre Domäne aus, die Userli behandeln soll.
init_user:
+ lead: Primäre Domäne wurde erstellt.
text: E-Mail-Standards erfordern, dass das Konto postmaster@%domain% vorhanden ist.
Wähle ein Kennwort für dieses erste Administratorkonto aus und melde dich anschließend
an.
- lead: Primäre Domäne wurde erstellt.
-init_domain:
- text: Bitte wähle Deine primäre Domäne aus, die Userli behandeln soll.
- lead: Vielen Dank fürs installieren von Userli. Derzeit sind keine Domänen oder
- Konten vorhanden.
-init.title: Erstellen der Erstkonfiguration
openpgp:
key-info: Veröffentlichter OpenPGP-Schlüssel
delete-button: OpenPGP-Schlüssel löschen
information: Information
information-details: Hier kannst du deinen OpenPGP-Schlüssel hochladen, damit er
in unserem Web Key
- Directory veröffentlicht wird. Damit hilfst du Freund*innen, deinen Schlüssel
+ Directory veröffentlicht wird. Damit hilfst du Freund:innen, deinen Schlüssel
automatisch und sicher zu finden.
keyid-label: "Schlüssel-ID:"
fingerprint-label: "Fingerprint:"
diff --git a/default_translations/en/messages.en.yml b/default_translations/en/messages.en.yml
index 82ea0a8a..262ee38d 100644
--- a/default_translations/en/messages.en.yml
+++ b/default_translations/en/messages.en.yml
@@ -45,8 +45,7 @@ index:
You can delete your %project_name% e-mail account.
This will delete all your e-mails.
A recovery at a later date is not possible.
-
- To delete your account you need to submit your actual password.
+
To delete your account you need to submit your actual password.
delete-button: Delete account
recovery-token-button: Manage recovery token
logged_in_as: You are logged in as %user%.
@@ -100,6 +99,40 @@ form:
openpgp-key-select-one: Please upload key either as file or as ASCII text.
openpgp-key-submit: Publish OpenPGP key
openpgp-delete: Delete OpenPGP key
+ verify: Verify
+ twofactor: Two-factor authentication
+ twofactor-short: 2FA
+ twofactor-login: Two-factor authentication
+ twofactor-login-placeholder: 6-digit code
+ twofactor-login-auth-code: Authentication code
+ twofactor-backup-code-ack: I stored the backup codes at a secure place
+ twofactor-login-desc: >
+ Open your two-factor authenticator (TOTP) app to view your authentication code.
+ twofactor-login-cancel: Cancel login
+ twofactor-enable: Enable two-factor authentication
+ twofactor-disable: Disable two-factor authentication
+
+account:
+ twofactor:
+ headline: Two-factor authentication
+ unset: You don't have two-factor authentication enabled yet.
+ unset-extra: You need a TOTP app on your mobile device to enable two-factor authentication.
+ set: Two-factor authentication is enabled.
+ button: Configure two-factor authentication
+ lead: Two-factor authentication adds an additional layer of security to your account.
+ desc-login: >
+ With two-factor authentication, you have to provide a second password at login, that
+ you get from your two-factor authenticator app. This extra password is only required
+ when logging into %app_url%. It's not required for accessing other services like
+ your mails.
+ desc-lost: >
+ If you ever loose access to your two-factor authenticator app, you will have to authenticate
+ using one of the two-factor backup codes.
+ enable-lead: >
+ Scan the image below with your two-factor app.
+ backup-ack-lead: >
+ Backup codes for two-factor authentication. Copy and store them at a secure place.
+
recovery:
header: Recover your account
lead: Reset the password using your recovery token
@@ -128,8 +161,7 @@ registration:
We only allow registrations if a friend invites you with an invite code to our
services.
information-password-policy: >
- Important:
- You need a password with at least 12 characters.
+ Important: You need a password with at least 12 characters.
Passwords will be tested against a database
of known bad passwords.
label-email: Your preferred e-mail address
@@ -139,9 +171,8 @@ welcome:
lead: You have set up an account with us successfully. What are the next steps?
next-button: To the overview
text: >
-
In the web you can find guides for settting up your e-mail account with several - client programs.
+In the web you can find guides for settting up your e-mail + account with several client programs.
In one week you will receive three invite codes, which you can collect at %app_url%. You can pass these on to friends and invite them to register with %project_name%.
diff --git a/default_translations/en/validators.en.yml b/default_translations/en/validators.en.yml index 3e76f71d..5f87d8fc 100644 --- a/default_translations/en/validators.en.yml +++ b/default_translations/en/validators.en.yml @@ -9,6 +9,8 @@ form: missing-domain: "The domain is not under management in this system." registration-recovery-token-noack: "This field needs to be checked." invalid-token: "This token has an invalid format." + twofactor-secret-invalid: "The verification code is not valid." + twofactor-backup-ack-missing: "This needs to be checked." registration: voucher-invalid: "The invite code is invalid." diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index b74985c5..71d79611 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -10,6 +10,7 @@ use Behat\MinkExtension\Context\MinkContext; use Behat\Symfony2Extension\Context\KernelDictionary; use Doctrine\ORM\Tools\SchemaTool; +use OTPHP\TOTP; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; @@ -130,6 +131,16 @@ public function theFollowingUserExists(TableNode $table) case 'mailCryptPublicKey': $user->setMailCryptPublicKey($value); break; + case 'totpConfirmed': + $user->setTotpConfirmed($value); + break; + case 'totpSecret': + $user->setTotpSecret($value); + break; + case 'totp_backup_codes': + $user->generateBackupCodes(); + $this->setPlaceholder('totp_backup_codes', $user->getBackupCodes()); + break; } } @@ -416,6 +427,18 @@ public function iShouldSeeEmptyConsoleOutput() } } + /** + * @Then I enter TOTP backup code + */ + public function iEnterTotpBackupCode() + { + $totpBackupCodes = $this->getPlaceholder('totp_backup_codes'); + if (!$totpBackupCodes) { + throw new \Exception('No TOTP backup codes cached'); + } + $this->fillField('_auth_code', $totpBackupCodes[0]); + } + /** * @Then /^File "([^"]*)" should exist$/ */ diff --git a/features/login.feature b/features/login.feature index 2c059203..9eed9b72 100644 --- a/features/login.feature +++ b/features/login.feature @@ -119,3 +119,47 @@ Feature: Login Then I should be on "/en/" And the response status code should be 200 And I should see text matching "E-mail access has been turned off" + + @login-2fa + Scenario: Login fails with invalid TOTP code if two-factor auth is enabled + When the following User exists: + | email | password | roles | totpConfirmed | totpSecret | + | twofactor@example.org | asdasd | ROLE_USER | 1 | secret | + And I am on "/login" + And I fill in the following: + | username | twofactor@example.org | + | password | asdasd | + And I press "Sign in" + + Then I should be on "/en/2fa" + And I should see text matching "Authentication code" + + And I fill in "_auth_code" with "invalid-token" + And I press "Verify" + + Then I should be on "/en/2fa" + And I should see text matching "The verification code is not valid." + + And I follow "Cancel login" + Then I should be on "/en/" + And the response status code should be 200 + + @login-2fa + Scenario: Login works with two-factor backup code if two-factor auth is enabled + When the following User exists: + | email | password | roles | totpConfirmed | totpSecret | totp_backup_codes | + | twofactor@example.org | asdasd | ROLE_USER | 1 | secret | true | + And I am on "/login" + And I fill in the following: + | username | twofactor@example.org | + | password | asdasd | + And I press "Sign in" + + Then I should be on "/en/2fa" + And I should see text matching "Authentication code" + + And I enter TOTP backup code + And I press "Verify" + + Then I should be on "/en/" + And the response status code should be 200 diff --git a/features/user.feature b/features/user.feature index 4ef13ac5..932b8c2e 100644 --- a/features/user.feature +++ b/features/user.feature @@ -155,3 +155,32 @@ Feature: User And I press "Create new recovery token" Then I should see text matching "The following recovery token got created for you" + + @twofactor-auth + Scenario: Enable two-factor authentication #1 and enter wrong password + When I am authenticated as "user@example.org" + And I am on "/user/twofactor" + And I fill in the following: + | twofactor_password | wrong-password | + And I press "Enable two-factor authentication" + + Then I should be on "/en/user/twofactor" + And I should see text matching "Wrong password" + + @twofactor-auth + Scenario: Enable two-factor authentication + When I am authenticated as "user@example.org" + And I am on "/user/twofactor" + And I fill in the following: + | twofactor_password | asdasd | + And I press "Enable two-factor authentication" + + Then I should be on "/en/user/twofactor" + And I should see text matching "Scan the image below with your two-factor app." + + And I fill in the following: + | twofactor_confirm_totpSecret | invalid-secret | + And I press "Verify" + + Then I should be on "/en/user/twofactor_confirm" + And I should see text matching "The verification code is not valid." diff --git a/src/Admin/UserAdmin.php b/src/Admin/UserAdmin.php index 3299d2be..da0e9fcd 100644 --- a/src/Admin/UserAdmin.php +++ b/src/Admin/UserAdmin.php @@ -85,10 +85,23 @@ protected function configureFormFields(FormMapper $form): void 'help' => (null !== $userId && $user->hasMailCryptSecretBox()) ? 'Disabled because user has a MailCrypt key pair defined' : null, ]) + ->add('totp_confirmed', CheckboxType::class, [ + 'label' => 'form.twofactor', + 'required' => false, + 'data' => (null !== $userId) ? $user->isTotpAuthenticationEnabled() : false, + 'disabled' => null === $userId || !$user->isTotpAuthenticationEnabled(), + 'help' => 'Can only be enabled by user', + ]) + ->add('recovery_secret_box', CheckboxType::class, [ + 'label' => 'Recovery Token', + 'data' => (null !== $userId) ? $user->hasRecoverySecretBox() : false, + 'disabled' => true, + 'help' => 'Can only be configured by user', + ]) ->add('roles', ChoiceType::class, [ 'choices' => [Roles::getAll()], 'multiple' => true, - 'expanded' => true, + 'expanded' => false, 'label' => 'form.roles', ]) ->add('quota', null, [ @@ -182,6 +195,9 @@ protected function configureListFields(ListMapper $list): void ->addIdentifier('email') ->add('creationTime') ->add('updatedTime') + ->add('isTotpAuthenticationEnabled', 'boolean', [ + 'label' => 'form.twofactor-short', + ]) ->add('recoverySecretBox', 'boolean', [ 'label' => 'Recovery Token', ]) @@ -236,6 +252,12 @@ public function preUpdate($object): void } else { $object->updateUpdatedTime(); } + + if (false === $object->getTotpConfirmed()) { + $object->setTotpSecret(null); + $object->setTotpConfirmed(false); + $object->clearBackupCodes(); + } } /** diff --git a/src/Command/MuninAccountCommand.php b/src/Command/MuninAccountCommand.php index 595df697..7f00da05 100644 --- a/src/Command/MuninAccountCommand.php +++ b/src/Command/MuninAccountCommand.php @@ -71,6 +71,9 @@ protected function execute(InputInterface $input, OutputInterface $output): void $output->writeln('mail_crypt_keys.label Active accounts with mailbox encryption'); $output->writeln('mail_crypt_keys.type GAUGE'); $output->writeln('mail_crypt_keys.min 0'); + $output->writeln('twofactor.label Active accounts with two-factor authentication'); + $output->writeln('twofactor.type GAUGE'); + $output->writeln('twofactor.min 0'); $output->writeln('openpgp_keys.label OpenPGP keys'); $output->writeln('openpgp_keys.type GAUGE'); $output->writeln('openpgp_keys.min 0'); @@ -82,6 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): void $output->writeln(sprintf('deleted.value %d', $this->userRepository->countDeletedUsers())); $output->writeln(sprintf('recovery_tokens.value %d', $this->userRepository->countUsersWithRecoveryToken())); $output->writeln(sprintf('mail_crypt_keys.value %d', $this->userRepository->countUsersWithMailCrypt())); + $output->writeln(sprintf('twofactor.value %d', $this->userRepository->countUsersWithTwofactor())); $output->writeln(sprintf('openpgp_keys.value %d', $this->openPgpKeyRepository->countKeys())); } } diff --git a/src/Command/UsersResetCommand.php b/src/Command/UsersResetCommand.php index c909af79..d3a459ae 100644 --- a/src/Command/UsersResetCommand.php +++ b/src/Command/UsersResetCommand.php @@ -129,10 +129,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->write(sprintf("{{ "account.twofactor.lead"|trans }}
+{% if not twofactor_enabled %} ++ + {{ "account.twofactor.unset"|trans }} +
+{% endif %} +{{ "account.twofactor.button"|trans }}
diff --git a/templates/Twofactor/twofactor_notes.html.twig b/templates/Twofactor/twofactor_notes.html.twig new file mode 100644 index 00000000..587492ad --- /dev/null +++ b/templates/Twofactor/twofactor_notes.html.twig @@ -0,0 +1,9 @@ ++
{{ "account.twofactor.lead"|trans }}
++ {% for key, value in twofactor_backup_codes %} + {{ value }} + {% endfor %} ++