From 6c3db68fc7de9c58840d60c13ed8df503a826b2b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 22 Sep 2026 16:46:42 +0200 Subject: [PATCH 1/2] fix: verify unauthorized credentials before deleting accounts Recheck stored 401 errors with the server and clear stale unauthorized flags. Deduplicate account checks, guard against cancellation and credential changes, and avoid switching unrelated controllers. Signed-off-by: Marino Faggiana --- iOSClient/Account/NCAccount.swift | 43 ++++++++++++++++++- .../Networking/NCNetworking+ServerError.swift | 9 ++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/iOSClient/Account/NCAccount.swift b/iOSClient/Account/NCAccount.swift index aec8f102dc..c571cead19 100644 --- a/iOSClient/Account/NCAccount.swift +++ b/iOSClient/Account/NCAccount.swift @@ -8,6 +8,7 @@ import NextcloudKit @MainActor class NCAccount: NSObject { + private static var accountsCheckingRemoteUser: Set = [] let database = NCManageDatabase.shared let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let global = NCGlobal.shared @@ -189,10 +190,35 @@ class NCAccount: NSObject { } func checkRemoteUser(account: String, controller: NCMainTabBarController?) async { + guard Self.accountsCheckingRemoteUser.insert(account).inserted else { + return + } + defer { Self.accountsCheckingRemoteUser.remove(account) } + let token = NCPreferences().getPassword(account: account) guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(predicate: NSPredicate(format: "account == %@", account)) else { return } + guard let session = NextcloudKit.shared.nkCommonInstance.nksessions.session(forAccount: account), + session.password == token else { + return + } + + // A stored 401 may belong to an inconsistent or outdated request. Verify with the server. + let result = await NextcloudKit.shared.getUserProfileAsync(account: account, options: NKRequestOptions(checkInterceptor: false)) + guard !Task.isCancelled, + remoteUserCredentialsMatch(session, token: token) else { + return + } + if result.error == .success, result.userProfile?.userId == session.userId { + NCNetworking.shared.removeUnauthorizedAccount(account) + return + } + guard result.responseData?.response?.statusCode == NCGlobal.shared.errorUnauthorized else { + // Network failures and other responses do not confirm invalid credentials. + return + } + let windowScene = SceneManager.shared.getWindowScene(controller: controller) await showErrorBanner(windowScene: windowScene, text: String(format: NSLocalizedString("_account_unauthorized_", comment: ""), account), errorCode: NCGlobal.shared.errorUnauthorized) @@ -206,6 +232,10 @@ class NCAccount: NSObject { } // REMOVE ACCOUNT + guard !Task.isCancelled, + remoteUserCredentialsMatch(session, token: token) else { + return + } await NCAccount().deleteAccount(account, wipe: resultsWipe.wipe) if resultsWipe.wipe { let resultsSetWipe = await NextcloudKit.shared.setRemoteWipeCompletitionAsync(serverUrl: tblAccount.urlBase, token: token, account: tblAccount.account) { task in @@ -219,11 +249,22 @@ class NCAccount: NSObject { nkLog(debug: "Set Remote Wipe Completition error code: \(resultsSetWipe.error.errorCode)") } - if account.count > 0 { + if !account.isEmpty, controller == nil || controller?.account == account { await switchToFirstAvailableAccount(controller: controller) } } + private func remoteUserCredentialsMatch(_ session: NKSession, token: String) -> Bool { + guard let currentSession = NextcloudKit.shared.nkCommonInstance.nksessions.session(forAccount: session.account) else { + return false + } + return currentSession.urlBase == session.urlBase + && currentSession.user == session.user + && currentSession.userId == session.userId + && currentSession.password == session.password + && NCPreferences().getPassword(account: session.account) == token + } + /// Presents the login (or intro) screen if no account remains. func switchToFirstAvailableAccount(controller: NCMainTabBarController?) async { let accounts = await NCManageDatabase.shared.getAccountsAsync() diff --git a/iOSClient/Networking/NCNetworking+ServerError.swift b/iOSClient/Networking/NCNetworking+ServerError.swift index 7b80026456..f5389a2322 100644 --- a/iOSClient/Networking/NCNetworking+ServerError.swift +++ b/iOSClient/Networking/NCNetworking+ServerError.swift @@ -22,6 +22,15 @@ extension NCNetworking { return true } + func removeUnauthorizedAccount(_ account: String) { + guard let groupDefaults = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) else { + return + } + var accounts = groupDefaults.array(forKey: nkComm.groupDefaultsUnauthorized) as? [String] ?? [] + accounts.removeAll { $0 == account } + groupDefaults.set(accounts, forKey: nkComm.groupDefaultsUnauthorized) + } + func removeServerErrorAccount(_ account: String) { guard let groupDefaults = UserDefaults(suiteName: NCBrandOptions.shared.capabilitiesGroup) else { return From 57d93c46914347f98ee0e710def14af94289e8a1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Tue, 22 Sep 2026 16:57:08 +0200 Subject: [PATCH 2/2] fix: synchronize file context before account switch requests Signed-off-by: Marino Faggiana --- iOSClient/Account/NCAccount.swift | 25 +++++++++++++------------ iOSClient/Files/NCFiles.swift | 16 +++++++++------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/iOSClient/Account/NCAccount.swift b/iOSClient/Account/NCAccount.swift index c571cead19..fc5efa7b20 100644 --- a/iOSClient/Account/NCAccount.swift +++ b/iOSClient/Account/NCAccount.swift @@ -90,20 +90,10 @@ class NCAccount: NSObject { func changeAccount(_ account: String, userProfile: NKUserProfile?, controller: NCMainTabBarController?) async { if let tblAccount = await database.setAccountActiveAsync(account) { - // Set account - controller?.account = account // Set User Profile if let userProfile { await database.setAccountUserProfileAsync(account: account, userProfile: userProfile) } - // Networking Certificate - NCNetworking.shared.activeAccountCertificate(account: account) - // Subscribing Push Notification - await NCPushNotification.shared.subscribingNextcloudServerPushNotification(account: tblAccount.account, urlBase: tblAccount.urlBase) - // Start the service - Task(priority: .utility) { - await NCService().startRequestServicesServer(account: account, controller: controller) - } // Capabilities if let capabilities = await self.database.getCapabilities(account: account) { // set theming color @@ -112,13 +102,24 @@ class NCAccount: NSObject { // Networking Process await NCNetworkingProcess.shared.setCurrentAccount(account) + // Update the account and the file context together, before starting network requests. + controller?.account = account + NCNetworking.shared.activeAccountCertificate(account: account) + // Color NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeTheming, userInfo: ["account": account]) // Notification if let controller { - NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeUser, userInfo: ["account": account, "controller": controller]) + NotificationCenter.default.post(name: Notification.Name(self.global.notificationCenterChangeUser), object: nil, userInfo: ["account": account, "controller": controller]) } else { - NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeUser, userInfo: ["account": account]) + NotificationCenter.default.post(name: Notification.Name(self.global.notificationCenterChangeUser), object: nil, userInfo: ["account": account]) + } + + // Subscribing Push Notification + await NCPushNotification.shared.subscribingNextcloudServerPushNotification(account: tblAccount.account, urlBase: tblAccount.urlBase) + // Start the service + Task(priority: .utility) { + await NCService().startRequestServicesServer(account: account, controller: controller) } } } diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index d932b690bd..1a3a3e5172 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -53,8 +53,8 @@ class NCFiles: NCCollectionViewCommon { self.serverUrl = utilityFileSystem.getHomeServer(session: session) self.titleCurrentFolder = getNavigationTitle() - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { notification in - Task { @MainActor in + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: .main) { notification in + MainActor.assumeIsolated { guard let userInfo = notification.userInfo, let account = userInfo["account"] as? String, self.controller?.account == account else { @@ -69,8 +69,8 @@ class NCFiles: NCCollectionViewCommon { self.mainNavigationController?.menuPlusButton.menu = nil self.mainNavigationController?.menuPlusButton.isEnabled = false - self.navigationController?.popToRootViewController(animated: false) self.serverUrl = self.utilityFileSystem.getHomeServer(session: session) + self.navigationController?.popToRootViewController(animated: false) self.isSearchingMode = false self.isEditMode = false self.fileSelect.removeAll() @@ -87,10 +87,12 @@ class NCFiles: NCCollectionViewCommon { self.titleCurrentFolder = self.getNavigationTitle() self.navigationItem.title = self.titleCurrentFolder - await self.mainNavigationController?.menuPlus?.create(session: session) - await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() - await self.reloadDataSource() - await self.getServerData() + Task { @MainActor in + await self.mainNavigationController?.menuPlus?.create(session: session) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() + await self.reloadDataSource() + await self.getServerData() + } } } }