Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introducing configurable options #1549

Open
wants to merge 61 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
22657b2
feat: introducing configurable options
ernolf Jul 22, 2024
2311905
refactor: code style fixes applied by php-cs-fixer
ernolf Jul 22, 2024
d508993
refactor: code style fixes applied by npm run lint -- --fix
ernolf Jul 22, 2024
c3cdce8
fix(tests): adjust SettingsControllerTest setup to match constructor …
ernolf Jul 22, 2024
d4ce413
fix(tests): add missing import for IURLGenerator in SettingsControlle…
ernolf Jul 22, 2024
5561f71
fix: adjust createSecret method call to match interface definition
ernolf Jul 22, 2024
da476b0
chore: update generated files after build
ernolf Jul 22, 2024
cf8ec46
refactor: code style fixes applied by php-cs-fixer
ernolf Jul 22, 2024
dba9f49
fix: add missing use statement and update to use correct constants
ernolf Jul 22, 2024
ebe8b00
fix: define getSettings method in ITotp interface
ernolf Jul 28, 2024
01e61f5
fix: INT must be INTEGER
ernolf Jul 28, 2024
3abee28
fix: use global RuntimeException in Totp service
ernolf Jul 28, 2024
0449118
test: fix and update SettingsController tests for redirect response
ernolf Jul 28, 2024
9c7264c
test: Fix issues in SettingsControllerTest
ernolf Jul 28, 2024
fc978fa
test(tests): fix and run PersonalTotpSettings.spec.js
ernolf Jul 28, 2024
d46ca72
feat: add "Advanced Settings" button
ernolf Jul 29, 2024
5bc3293
fix: correct attribute order in PersonalTotpSettings.vue
ernolf Jul 29, 2024
f30add6
fix: add missing trailing comma in PersonalTotpSettings.spec.js
ernolf Jul 29, 2024
2c46cfb
build: update built JS files after recent changes
ernolf Jul 29, 2024
0a6587e
fix: change minlength from 6 to 4 in challenge
ernolf Jul 29, 2024
9d24964
refactor: redesign message in advanced setting UI
ernolf Jul 31, 2024
a0ea70e
feat: provide favicon as image for FreeOTP
ernolf Aug 2, 2024
6a2f1bf
style: remove trailing blank line to pass linter
ernolf Aug 2, 2024
a4a06a2
test: add unit test for getFaviconUrl method
ernolf Aug 2, 2024
e3b4ee9
test: remove failing test for private method
ernolf Aug 2, 2024
0345b82
refactor: more compact warning in advanced setting UI
ernolf Aug 4, 2024
9e06ece
refactor(totp-settings): fetch advanced settings only when expanded
ernolf Aug 4, 2024
ca05be2
feat(configurable options): Redesign and expansion of configurable op…
ernolf Aug 8, 2024
62092b8
test: adapted testfiles for configurable options
ernolf Aug 8, 2024
0c437ef
docs: Documentation of configurable options in README.md
ernolf Aug 8, 2024
9928714
style: apply code style fixes from Composer
ernolf Aug 8, 2024
0524aea
fix: remove accidentally included code block
ernolf Aug 8, 2024
6b1b097
refactor: move private constants to public interface
ernolf Aug 8, 2024
b421035
refactor: remove redundant getDefaultPeriod() method
ernolf Aug 8, 2024
1d780c2
test: adapt test to use static method directly in testCreateSecret
ernolf Aug 8, 2024
bf4f8e7
test: add mock for OC.Notification in PersonalTotpSettings tests
ernolf Aug 8, 2024
24e485a
fix: adjust test for static method invocation
ernolf Aug 8, 2024
bd1990d
style: apply automatic lint fixes
ernolf Aug 8, 2024
4443a0c
refactor: change getAlgorithmById from static to instance to make it …
ernolf Aug 8, 2024
1629028
test: fix QR URL encoding in testCreateSecret
ernolf Aug 8, 2024
6c9ec9d
refactor: comment out sensitive 'secret' parameter in logger
ernolf Aug 8, 2024
9a7fd9d
feat(ui): improve user feedback and interaction
ernolf Aug 9, 2024
4d23809
fix: revert PersonalTotpSettings.spec.js to original state from maste…
ernolf Aug 9, 2024
ac97afb
refactor: remove redundant Advanced Settings from PersonalTotpSetting…
ernolf Aug 9, 2024
91bd32f
fix: update custom event name to kebab-case for Vue.js compliance
ernolf Aug 9, 2024
52466d5
fix(tests): remove unused QR import in SetupConfirmation.spec.js
ernolf Aug 9, 2024
a881f59
test: use pure vue test suite instead of chai properties
ernolf Aug 9, 2024
140169c
style: apply automatic lint fixes
ernolf Aug 9, 2024
e00ba1b
test: remove console logs to comply with linter rules
ernolf Aug 9, 2024
75c29c2
build: update compiled javacript files
ernolf Aug 9, 2024
4f05f3f
fix(migration): rename 'period' column to 'seconds' to avoid MariaDB …
ernolf Aug 14, 2024
c4bf415
chore: merge master into ernolf/configurable_options
ernolf Aug 23, 2024
52b6498
chore: rebuild assets after merging master
ernolf Aug 23, 2024
a61fef5
feat: add cancel button to abort setup procedure from personal settings
ernolf Aug 25, 2024
e07d237
feat: integrate advanced settings option in login setup
ernolf Aug 25, 2024
2aa78e0
refactor: restructure code to satisfy linting rules
ernolf Aug 25, 2024
1f71384
fix: prevent applying invalid custom secret
ernolf Aug 26, 2024
af65887
refactor: restructure code to satisfy linting rules
ernolf Aug 26, 2024
b980023
test: simplify and improve validation test for custom secret
ernolf Aug 26, 2024
200d1f4
refactor: restructure code to satisfy linting rules
ernolf Aug 26, 2024
c388d42
chore: rebuild assets
ernolf Aug 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,65 @@ The app is available through the [app store](https://apps.nextcloud.com/apps/two
![](screenshots/enter_challenge.png)
![](screenshots/settings.png)

## Setting options for security hardening

The secret generated by this application is 32 characters long and includes only the characters A-Z and the numbers 2, 3, 4, 5, 6, and 7 ([RFC 4648 Base32 alphabet](https://datatracker.ietf.org/doc/html/rfc4648#section-6)), meeting the recommended 160-bit depth by [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226#section-4).

Every 30 seconds, a 6-character numeric one-time password (OTP) is generated using the SHA-1 hash algorithm on both - the server and the TOTP authenticator app on your smartphone /-device. So whoever has the secret can get access to the second factor. That's why TOTP apps are (should be) usually very well secured.

### Configurable options by admin

1. **Secret Length:**
||characters|bits|
|-|-|-|
|Default:|32|160
|Minimum:|26|130
|Maximum:|128|640

Adjust the default secret length using the number of characters as value:

```sh
occ config:app:set --value=64 -- twofactor_totp secret_length
```

2. **Hash Algorithm:**
||algorithm|key|
|-|-|-|
|Default:|SHA1|1
|Optional:|SHA256|2
|Optional:|SHA512|3

Adjust the default algorithm using the key as value:

```sh
occ config:app:set --value=3 -- twofactor_totp hash_algorithm
```

3. **Token Length:**

||digits|
|-|-|
|Default:|6
|Maximum:|8

Adjust the default token length of the OTP using the number of digits as value:

```sh
occ config:app:set --value=8 -- twofactor_totp token_length
```

### Configurable options by user

During activation and setup by the user, a QR code is generated with the secret based on the default values as described here. By scanning that QR code with any TOTP app, the secret can be immediately activated with the first matching OTP. An "Advanced Settings" button gives the user access to further setting options that go beyond the default values:

* Own secret
* Token length in a range from 4 to 10
* Validity period of the OTP from 15 to 60 seconds

Not all apps can use all of these setting options or not to this extent, so these values cannot be set by the admin by default, otherwise only a small number of TOTP apps would be compatible.

If an app does not support a setting, then either the generated QR code is not accepted by the app, there may be an error message, or the first OTP is incorrect and the secret cannot be activated. It can be varied until a QR code works.

## Login with external apps
Once you enable OTP with Two Factor Totp, your aplications (for example your Android app or your GNOME app) will need to login using device passwords. To manage it, [know more here](https://docs.nextcloud.com/server/stable/user_manual/en/session_management.html#managing-devices)

Expand All @@ -40,3 +99,4 @@ Once you enable OTP with Two Factor Totp, your aplications (for example your And
* `composer i`
* `npm ci`
* `npm run build` or `npm run dev` [more info](https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/npm.html)

11 changes: 11 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/**
* @author Christoph Wurst <[email protected]>
* @author 2024 [ernolf] Raphael Gradenwitz <[email protected]>
*
* Two-factor TOTP
*
Expand Down Expand Up @@ -32,5 +33,15 @@
'url' => '/settings/enable',
'verb' => 'POST'
],
[
'name' => 'settings#updateSettings',
'url' => '/settings/update',
'verb' => 'POST'
],
[
'name' => 'settings#getDefaults',
'url' => '/settings/defaults',
'verb' => 'GET'
],
]
];
4 changes: 2 additions & 2 deletions js/twofactor_totp-main-login-setup.js

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion js/twofactor_totp-main-login-setup.js.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,42 @@
* Date: 2020-01-18T06:04:33.222Z
*/

/*!
* vuex v3.6.2
* (c) 2021 Evan You
* @license MIT
*/

/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

/**
* @copyright 2019 Christoph Wurst <[email protected]>
*
* @author 2019 Christoph Wurst <[email protected]>
*
* @license GNU AGPL version 3 or any later version
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* @copyright 2019 Christoph Wurst <[email protected]>
*
* @author 2019 Christoph Wurst <[email protected]>
* @author 2024 [ernolf] Raphael Gradenwitz <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
Expand Down
2 changes: 1 addition & 1 deletion js/twofactor_totp-main-login-setup.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions js/twofactor_totp-main-settings.js

Large diffs are not rendered by default.

26 changes: 24 additions & 2 deletions js/twofactor_totp-main-settings.js.LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
*
* @author 2018 Christoph Wurst <[email protected]>
*
* @license GNU AGPL version 3 or any later version
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
Expand All @@ -75,7 +75,29 @@
*
* @author 2019 Christoph Wurst <[email protected]>
*
* @license GNU AGPL version 3 or any later version
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

/**
* @copyright 2019 Christoph Wurst <[email protected]>
*
* @author 2019 Christoph Wurst <[email protected]>
* @author 2024 [ernolf] Raphael Gradenwitz <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
Expand Down
2 changes: 1 addition & 1 deletion js/twofactor_totp-main-settings.js.map

Large diffs are not rendered by default.

121 changes: 115 additions & 6 deletions lib/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/**
* @author Christoph Wurst <[email protected]>
* @author 2024 [ernolf] Raphael Gradenwitz <[email protected]>
*
* Two-factor TOTP
*
Expand All @@ -25,11 +26,14 @@

use InvalidArgumentException;
use OCA\TwoFactorTOTP\Service\ITotp;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Authentication\TwoFactorAuth\ALoginSetupController;
use OCP\Defaults;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use RuntimeException;
use function is_null;

Expand All @@ -44,11 +48,27 @@ class SettingsController extends ALoginSetupController {
/** @var Defaults */
private $defaults;

public function __construct(string $appName, IRequest $request, IUserSession $userSession, ITotp $totp, Defaults $defaults) {
/** @var IURLGenerator */
private $urlGenerator;

/** @var LoggerInterface */
private $logger;

public function __construct(
string $appName,
IRequest $request,
IUserSession $userSession,
ITotp $totp,
Defaults $defaults,
IURLGenerator $urlGenerator,
LoggerInterface $logger
) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->totp = $totp;
$this->defaults = $defaults;
$this->urlGenerator = $urlGenerator;
$this->logger = $logger;
}

/**
Expand All @@ -62,6 +82,9 @@ public function state(): JSONResponse {
}
return new JSONResponse([
'state' => $this->totp->hasSecret($user) ? ITotp::STATE_ENABLED : ITotp::STATE_DISABLED,
'algorithm' => $this->totp->getAlgorithmId($user),
'digits' => $this->totp->getDigits($user),
'period' => $this->totp->getPeriod($user)
]);
}

Expand All @@ -71,28 +94,54 @@ public function state(): JSONResponse {
*
* @param int $state
* @param string|null $code for verification
* @param string|null $secret
* @param int $algorithm
* @param int $digits
* @param int $period
*/
public function enable(int $state, string $code = null): JSONResponse {
public function enable(int $state, string $code = null, string $secret = null, int $algorithm = null, int $digits = null, int $period = ITotp::DEFAULT_PERIOD) {
$this->logger->debug('Enable called', [
'state' => $state,
'code' => $code,
/* sensitive parameter
'secret' => $secret,
*/
'algorithm' => $algorithm,
'digits' => $digits,
'period' => $period
]);

// Use defaults as set by admin if null
$algorithm = $algorithm ?? $this->totp->getDefaultAlgorithm();
$digits = $digits ?? $this->totp->getDefaultDigits();

$this->logger->debug('Enable after default values', [
'algorithm' => $algorithm,
'digits' => $digits,
]);

$user = $this->userSession->getUser();
if (is_null($user)) {
throw new \Exception('user not available');
}

switch ($state) {
case ITotp::STATE_DISABLED:
$this->totp->deleteSecret($user);
return new JSONResponse([
'state' => ITotp::STATE_DISABLED,
]);
case ITotp::STATE_CREATED:
$secret = $this->totp->createSecret($user);

$secret = $secret ?? $this->totp->createSecret($user, null, $algorithm, $digits, $period);
$secretName = $this->getSecretName();
$issuer = $this->getSecretIssuer();
$qrUrl = "otpauth://totp/$secretName?secret=$secret&issuer=$issuer";
$algorithmName = strtoupper($this->totp->getAlgorithmById($algorithm));
$faviconUrl = $this->getFaviconUrl();
$qrUrl = "otpauth://totp/$secretName?secret=$secret&issuer=$issuer&algorithm=$algorithmName&digits=$digits&period=$period&image=$faviconUrl";
return new JSONResponse([
'state' => ITotp::STATE_CREATED,
'secret' => $secret,
'qrUrl' => $qrUrl,
'qrUrl' => $qrUrl
]);
case ITotp::STATE_ENABLED:
if ($code === null) {
Expand All @@ -107,6 +156,55 @@ public function enable(int $state, string $code = null): JSONResponse {
}
}

/**
* Update TOTP settings after TOTP has been enabled.
*
* @NoAdminRequired
* @PasswordConfirmationRequired
*
* @param string|null $secret
* @param int $algorithm
* @param int $digits
* @param int $period
* @return JSONResponse
*/
public function updateSettings(string $secret = null, int $algorithm, int $digits, int $period): JSONResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new \Exception('user not available');
}

$this->totp->updateSettings($user, $secret, $algorithm, $digits, $period);
return new JSONResponse([
'secret' => $secret,
'algorithm' => $algorithm,
'digits' => $digits,
'period' => $period
]);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function getDefaults(): DataResponse {
return new DataResponse([
'defaultAlgorithm' => $this->totp->getDefaultAlgorithm(),
'defaultDigits' => $this->totp->getDefaultDigits(),
'defaultPeriod' => ITotp::DEFAULT_PERIOD
]);
}

public function getSettings(): JSONResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new \Exception('user not available');
}

$settings = $this->totp->getSettings($user);
return new JSONResponse($settings);
}

/**
* The user's cloud id, e.g. "[email protected]/owncloud"
*
Expand All @@ -131,4 +229,15 @@ private function getSecretIssuer(): string {
$productName = $this->defaults->getName();
return rawurlencode($productName);
}

/**
* FaviconUrl for FreeOTP
*
* @return string
*/
private function getFaviconUrl(): string {
$baseUrl = $this->urlGenerator->getBaseUrl();
$subPath = $this->urlGenerator->linkToRoute('theming.Icon.getFavicon', ['app' => 'core']);
return $baseUrl . $subPath;
}
}
Loading
Loading