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 }) }} +