Refuse a login whose Apple identity moved under it

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.
This commit is contained in:
Arkadiusz Fal
2026-08-29 23:50:47 +02:00
parent 718aac57c6
commit 395b8eb083
2 changed files with 30 additions and 0 deletions

View File

@@ -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" : {

View File

@@ -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