Follow-up from nextcloud/user_oidc#1452 and nextcloud/user_oidc#1486, at the maintainer's suggestion there ("Your setupFS issue is worth filing separately").
What happens
OC_User::loginWithApache() calls OC_Util::setupFS($uid) and \OC::$server->getUserFolder($uid) unconditionally, on every request where the guard self::getUser() !== $uid is true:
// lib/private/legacy/OC_User.php
if (self::getUser() !== $uid) {
...
$userSession->createSessionToken($request, $uid, $uid, $password);
$userSession->createRememberMeToken($userSession->getUser());
...
OC_Util::setupFS($uid);
...
\OC::$server->getUserFolder($uid);
}
OC_Util::setupFS() goes straight to SetupManager::setupForUser():
// lib/private/legacy/OC_Util.php
$setupManager->setupForUser($userObject);
setupForUser()'s only guard is isSetupComplete(), backed by $this->setupUsersComplete — an in-memory array scoped to the current SetupManager instance, i.e. one request:
// lib/private/Files/SetupManager.php
public function isSetupComplete(IUser $user): bool {
return in_array($user->getUID(), $this->setupUsersComplete, true);
}
So there is no cross-request reuse at all on this path. Contrast with the other full-setup trigger, SetupManager::setupForPath() (reached lazily via Root::getUserFolder()'s LazyUserFolder), which checks a 5-minute distributed cache (fullSetupRequired(), fs_mount_cache_duration) before doing a full setup. loginWithApache()'s call bypasses that cache entirely — it always does the full enumeration, every time the guard is open.
createSessionToken() also unconditionally generates a fresh RSA-2048 keypair (PublicKeyTokenProvider::newToken() → openssl_pkey_new()), with no reuse of any existing token for the same uid.
Who hits this
Any IApacheBackend implementation authenticates through this path — most commonly bearer-token backends (user_oidc's Backend::getCurrentUserId()) where a client sends a token without a session cookie on every request. Each such request re-triggers the full block, because there is nothing that lets the guard recognize "this exact uid was fully set up 200ms ago in a different request."
Other login paths already avoid this. Session::completeLogin()'s isToken branch hard-codes $firstTimeLogin = false, and prepareUserLogin() only calls setupFS()/getUserFolder()/copySkeleton() when $firstTimeLogin is true:
// lib/private/User/Session.php
protected function prepareUserLogin($firstTimeLogin, $refreshCsrfToken = true) {
...
if ($firstTimeLogin) {
OC_Util::setupFS($user);
...
}
}
loginWithApache() has no equivalent distinction between "this user's very first login ever" and "this user was already fully set up a moment ago in another request."
Measured impact
Captured via the built-in profiler app, on a real deployment, isolated (non-concurrent) bearer requests through this path:
fs:setup:user:full: 98.70 ms and 148.61 ms in two separate captures, dominated by mount-provider enumeration (Collectives_Mount_MountProvider, Files_Sharing_MountProvider, GroupFolders_Mount_MountProvider — none relevant to the request being served in either case).
- The token-creation queries themselves (
SELECT/DELETE×2/SELECT/INSERT/UPDATE against oc_authtoken) totaled under 0.02 ms combined — negligible; the keygen and mount setup are the actual cost, not the DB writes.
- Under concurrent load (10 simultaneous bearer requests, same uid), this cost compounds — one capture showed 148 ms of internal work but a multi-second gap between profiler-internal timing and actual TTFB, consistent with several requests hitting this unconditional path at once and contending for DB/worker resources. Not confirmed as directly caused by this specific code path rather than general load, but consistent with it.
Suggested direction
Something that lets loginWithApache() distinguish "already fully set up very recently" from "first sighting," without reintroducing the correctness bug in #1452 (i.e. without setting the session user before the guard runs). Two shapes that seem plausible, not fully evaluated:
- Have
OC_Util::setupFS() / the login-time call check the same distributed cache SetupManager::setupForPath() already uses (fs_mount_cache_duration), instead of only the per-request isSetupComplete() flag.
- Or, narrower: skip
createSessionToken()'s keygen when a valid, non-expired token already exists for the uid from a very recent request, reusing it instead of minting a new one every time.
Happy to help test against a real deployment if useful — happy path (bearer, no cookie, repeated same-uid requests) is easy to reproduce.
Follow-up from nextcloud/user_oidc#1452 and nextcloud/user_oidc#1486, at the maintainer's suggestion there ("Your setupFS issue is worth filing separately").
What happens
OC_User::loginWithApache()callsOC_Util::setupFS($uid)and\OC::$server->getUserFolder($uid)unconditionally, on every request where the guardself::getUser() !== $uidis true:OC_Util::setupFS()goes straight toSetupManager::setupForUser():setupForUser()'s only guard isisSetupComplete(), backed by$this->setupUsersComplete— an in-memory array scoped to the currentSetupManagerinstance, i.e. one request:So there is no cross-request reuse at all on this path. Contrast with the other full-setup trigger,
SetupManager::setupForPath()(reached lazily viaRoot::getUserFolder()'sLazyUserFolder), which checks a 5-minute distributed cache (fullSetupRequired(),fs_mount_cache_duration) before doing a full setup.loginWithApache()'s call bypasses that cache entirely — it always does the full enumeration, every time the guard is open.createSessionToken()also unconditionally generates a fresh RSA-2048 keypair (PublicKeyTokenProvider::newToken()→openssl_pkey_new()), with no reuse of any existing token for the same uid.Who hits this
Any
IApacheBackendimplementation authenticates through this path — most commonly bearer-token backends (user_oidc'sBackend::getCurrentUserId()) where a client sends a token without a session cookie on every request. Each such request re-triggers the full block, because there is nothing that lets the guard recognize "this exact uid was fully set up 200ms ago in a different request."Other login paths already avoid this.
Session::completeLogin()'sisTokenbranch hard-codes$firstTimeLogin = false, andprepareUserLogin()only callssetupFS()/getUserFolder()/copySkeleton()when$firstTimeLoginis true:loginWithApache()has no equivalent distinction between "this user's very first login ever" and "this user was already fully set up a moment ago in another request."Measured impact
Captured via the built-in profiler app, on a real deployment, isolated (non-concurrent) bearer requests through this path:
fs:setup:user:full: 98.70 ms and 148.61 ms in two separate captures, dominated by mount-provider enumeration (Collectives_Mount_MountProvider,Files_Sharing_MountProvider,GroupFolders_Mount_MountProvider— none relevant to the request being served in either case).SELECT/DELETE×2/SELECT/INSERT/UPDATEagainstoc_authtoken) totaled under 0.02 ms combined — negligible; the keygen and mount setup are the actual cost, not the DB writes.Suggested direction
Something that lets
loginWithApache()distinguish "already fully set up very recently" from "first sighting," without reintroducing the correctness bug in #1452 (i.e. without setting the session user before the guard runs). Two shapes that seem plausible, not fully evaluated:OC_Util::setupFS()/ the login-time call check the same distributed cacheSetupManager::setupForPath()already uses (fs_mount_cache_duration), instead of only the per-requestisSetupComplete()flag.createSessionToken()'s keygen when a valid, non-expired token already exists for the uid from a very recent request, reusing it instead of minting a new one every time.Happy to help test against a real deployment if useful — happy path (bearer, no cookie, repeated same-uid requests) is easy to reproduce.