From bacb4323de9c97889dd77d785aae9de1c3824b51 Mon Sep 17 00:00:00 2001
From: Joas Schilling
Date: Fri, 10 Oct 2025 12:48:56 +0200
Subject: [PATCH] fix: add app config to control onetime case
Signed-off-by: Joas Schilling
---
apps/settings/lib/ConfigLexicon.php | 5 ++-
.../lib/Controller/AuthSettingsController.php | 44 +++++++------------
.../AccountMenu/AccountMenuProfileEntry.vue | 2 +-
.../AccountMenu/AccountQRLoginDialog.vue | 11 +++--
4 files changed, 30 insertions(+), 32 deletions(-)
diff --git a/apps/settings/lib/ConfigLexicon.php b/apps/settings/lib/ConfigLexicon.php
index 754f2a4ccad..415ec2f487b 100644
--- a/apps/settings/lib/ConfigLexicon.php
+++ b/apps/settings/lib/ConfigLexicon.php
@@ -20,6 +20,7 @@ use OCP\Config\ValueType;
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
*/
class ConfigLexicon implements ILexicon {
+ public const LOGIN_QRCODE_ONETIME = 'qrcode_onetime';
public const USER_SETTINGS_EMAIL = 'email';
public const USER_LIST_SHOW_STORAGE_PATH = 'user_list_show_storage_path';
public const USER_LIST_SHOW_USER_BACKEND = 'user_list_show_user_backend';
@@ -33,7 +34,9 @@ class ConfigLexicon implements ILexicon {
}
public function getAppConfigs(): array {
- return [];
+ return [
+ new Entry(key: self::LOGIN_QRCODE_ONETIME, type: ValueType::BOOL, defaultRaw: false, definition: 'Use onetime QR codes for app passwords', note: 'Limits compatibility for mobile apps to versions released in 2026 or later'),
+ ];
}
public function getUserConfigs(): array {
diff --git a/apps/settings/lib/Controller/AuthSettingsController.php b/apps/settings/lib/Controller/AuthSettingsController.php
index 223537e1956..4873592d8a6 100644
--- a/apps/settings/lib/Controller/AuthSettingsController.php
+++ b/apps/settings/lib/Controller/AuthSettingsController.php
@@ -14,12 +14,14 @@ use OC\Authentication\Token\INamedToken;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\RemoteWipe;
use OCA\Settings\Activity\Provider;
+use OCA\Settings\ConfigLexicon;
use OCP\Activity\IManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\Authentication\Exceptions\ExpiredTokenException;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Exceptions\WipeTokenException;
@@ -32,49 +34,31 @@ use OCP\Session\Exceptions\SessionNotAvailableException;
use Psr\Log\LoggerInterface;
class AuthSettingsController extends Controller {
- /** @var IProvider */
- private $tokenProvider;
- /** @var RemoteWipe */
- private $remoteWipe;
-
- /**
- * @param string $appName
- * @param IRequest $request
- * @param IProvider $tokenProvider
- * @param ISession $session
- * @param ISecureRandom $random
- * @param string|null $userId
- * @param IUserSession $userSession
- * @param IManager $activityManager
- * @param RemoteWipe $remoteWipe
- * @param LoggerInterface $logger
- */
public function __construct(
string $appName,
IRequest $request,
- IProvider $tokenProvider,
+ private IProvider $tokenProvider,
private ISession $session,
private ISecureRandom $random,
private ?string $userId,
private IUserSession $userSession,
private IManager $activityManager,
- RemoteWipe $remoteWipe,
+ private IAppConfig $appConfig,
+ private RemoteWipe $remoteWipe,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
- $this->tokenProvider = $tokenProvider;
- $this->remoteWipe = $remoteWipe;
}
/**
* @NoSubAdminRequired
*
- * @param bool $oneTime If set to true, the returned token can only be used to get the actual app password a single time
+ * @param bool $qrcodeLogin If set to true, the returned token could be (depending on server settings) a onetime password, that can only be used to get the actual app password a single time
*/
#[NoAdminRequired]
#[PasswordConfirmationRequired]
- public function create(string $name = '', bool $oneTime = false): JSONResponse {
+ public function create(string $name = '', bool $qrcodeLogin = false): JSONResponse {
if ($this->checkAppToken()) {
return $this->getServiceNotAvailableResponse();
}
@@ -100,10 +84,16 @@ class AuthSettingsController extends Controller {
return $this->getServiceNotAvailableResponse();
}
- if ($oneTime) {
- $name = 'One time login';
- $type = IToken::ONETIME_TOKEN;
- $scope = [];
+ if ($qrcodeLogin) {
+ if ($this->appConfig->getAppValueBool(ConfigLexicon::LOGIN_QRCODE_ONETIME)) {
+ $name = 'One time login';
+ $type = IToken::ONETIME_TOKEN;
+ $scope = [];
+ } else {
+ $name = 'QR Code login';
+ $type = IToken::PERMANENT_TOKEN;
+ $scope = null;
+ }
} elseif ($name === '') {
// No name is only allowed for one time logins
return $this->getServiceNotAvailableResponse();
diff --git a/core/src/components/AccountMenu/AccountMenuProfileEntry.vue b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue
index 4e6eb6a78a1..daf4dee17e3 100644
--- a/core/src/components/AccountMenu/AccountMenuProfileEntry.vue
+++ b/core/src/components/AccountMenu/AccountMenuProfileEntry.vue
@@ -113,7 +113,7 @@ export default defineComponent({
async handleQrCodeClick() {
await confirmPassword()
- const { data } = await axios.post(generateUrl('/settings/personal/authtokens'), { name: '', oneTime: true })
+ const { data } = await axios.post(generateUrl('/settings/personal/authtokens'), { qrcodeLogin: true })
await spawnDialog(AccountQrLoginDialog, { data })
},
diff --git a/core/src/components/AccountMenu/AccountQRLoginDialog.vue b/core/src/components/AccountMenu/AccountQRLoginDialog.vue
index 02ecf282254..eb10e42aa8b 100644
--- a/core/src/components/AccountMenu/AccountQRLoginDialog.vue
+++ b/core/src/components/AccountMenu/AccountQRLoginDialog.vue
@@ -41,13 +41,16 @@ const buttons = [{
callback: () => undefined,
}]
+const isOneTimeToken = (props.data?.deviceToken?.type ?? 1) === 3
+
const qrUrl = computed(() => {
const user = props.data?.loginName ?? ''
const password = props.data?.token ?? ''
+ const path = isOneTimeToken ? 'onetime-login' : 'login'
const server = getBaseUrl()
// TODO return different result for error handling (to not provide invalid URL)
- return `nc://onetime-login/user:${user}&password:${password}&server:${server}`
+ return `nc://${path}/user:${user}&password:${password}&server:${server}`
})
const expirationTimestamp = (props.data?.deviceToken?.lastActivity ? props.data.deviceToken.lastActivity * 1_000 : Date.now()) + 120_000
@@ -77,8 +80,10 @@ function onClosing(result: unknown) {
{{ t('core', 'Use {productName} mobile client you want to connect to scan the code', { productName }) }}
-
- {{ t('core', 'Code will expire {timeCountdown} or after use', { timeCountdown }) }}
+
+
+ {{ t('core', 'Code will expire {timeCountdown} or after use', { timeCountdown }) }}
+