From 395b8eb083c9944e20c4ba29b7d176cd6bb2de2c Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 29 Aug 2026 23:50:47 +0200 Subject: [PATCH] Refuse a login whose Apple identity moved under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storing checked that the identity scope was resolved, but not that it was still the one the login was made under. Credentials are filed per identity, and the round trip leaves room for the scope to move — the account obtained here would then be written into somebody else's namespace and synced to their Keychain. Only a move between two named identities counts. Unresolved at either end is left to the storing side, which already refuses to write under a scope it cannot name; rejecting it here as well would turn an ordinary sign-in that began before iCloud settled — or a tokenless scope revalidating on activation, which it does every time — into a failed login. --- Yattee/Localizable.xcstrings | 10 ++++++++++ Yattee/Views/Settings/InstanceLoginView.swift | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 02c91241..9a4d7220 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -153,6 +153,16 @@ } } }, + "accounts.signIn.identityChanged" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Your iCloud account changed while signing in. Try again." + } + } + } + }, "accounts.signIn.identityUnresolved" : { "localizations" : { "en" : { diff --git a/Yattee/Views/Settings/InstanceLoginView.swift b/Yattee/Views/Settings/InstanceLoginView.swift index ad9f5743..4c82ab5a 100644 --- a/Yattee/Views/Settings/InstanceLoginView.swift +++ b/Yattee/Views/Settings/InstanceLoginView.swift @@ -153,11 +153,31 @@ struct InstanceLoginView: View { isLoading = true errorMessage = nil + // Which Apple identity this login is being made under. Credentials are + // filed per identity, and the round trip below leaves room for the + // scope to move — storing afterwards would file the account obtained + // here in somebody else's namespace, and sync it to their Keychain. + let scopeAtStart = appEnvironment.credentialIdentityScope.current + Task { do { let credential = try await performLogin(appEnvironment: appEnvironment) await MainActor.run { + // Only a move between two *named* identities is a mismatch. + // Unresolved at either end is left to `storeLogin`, which + // already refuses to write under a scope it cannot name — + // rejecting here as well would turn an ordinary sign-in + // that began before iCloud settled, or a tokenless scope + // revalidating on activation, into a failed login. + if let scopeAtStart, + let scopeNow = appEnvironment.credentialIdentityScope.current, + scopeNow != scopeAtStart { + errorMessage = String(localized: "accounts.signIn.identityChanged") + isLoading = false + return + } + guard onLoginSuccess(credential, username) else { // The failure is reported by a toast from the storing // side; staying open keeps the form filled in so the