Stop asking for a verification code Apple has already spent - #170
Merged
Conversation
FindMy.py's 2FA submit does two things: it sends the code, which passes, and then runs a full Grand Slam re-authentication, which can 503 on its own. By then Apple has consumed the code - so the screen's response, "Two-Factor Authentication failed", an empty box and try again, was the one action guaranteed to fail. It compounds. The retry comes back as InvalidCredentialsError, a different error that reads as a typo, and failed2FAAttemptCount climbs until the screen advises changing the Anisette server. Anisette had no part in it, and changing it forces a re-login against a different machine identity (rule 4): somebody is sent to fix something that was never broken. Reported as #168 and reproduced since. The desktop exporter fixed its half through the same library in #169; this is reasoned the same way rather than invented differently, from the handover note. So: classify before counting, say Apple took the code, wait, then request a *new* one - after the wait, never before, because Apple's codes expire and one fetched first is two minutes stale by the time it is typed. No prompt: there is no question worth asking when re-typing cannot work and waiting is the only option. The wait counts down on screen, because two minutes of a still screen on a phone is indistinguishable from a hang. Two goes, then it says plainly that this is Apple's fault - and warns the password may be refused once, which happened in the one observed recovery and otherwise reads as a second, unrelated problem. The Apple ID and password are not asked for again: the account is still in its second-factor state, so requesting on the chosen method is all that is needed. The waits are 60s then 120s, and ACodeAppleAlreadyTookTest goes red if either drops. They are a measurement, not round numbers - the observed recovery was still being refused about a minute after the 503 and worked about two rounds out - and the test carries the reasoning so lowering them has to be a decision rather than a tidy-up. Checked both ways on a device: two of the three screen tests go red with the classification disabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parawanderer
temporarily deployed
to
Android Build
August 30, 2026 20:42 — with
GitHub Actions
Inactive
parawanderer
temporarily deployed
to
Android Build
August 30, 2026 20:42 — with
GitHub Actions
Inactive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #168. The desktop exporter's half of the same bug was #169; this is deliberately reasoned the same way rather than invented differently, from the handover note that came with it.
What happens
FindMy.py's 2FA submit does two things: it sends the code — that is the check, and it passes — and then runs a full Grand Slam re-authentication, which can
503on its own. By then Apple has consumed the code.So the user's code was accepted and is now spent, and re-typing it cannot work.
Why the old behaviour made it worse
The screen said "Two-Factor Authentication failed", cleared the box and asked for the code again — the one action guaranteed to fail. It compounds:
InvalidCredentialsError, a different error, which reads as a typo.failed2FAAttemptCountclimbs.What it does now
The waits are 60s then 120s, and there is a test that goes red if they drop
They are a measurement, not round numbers. The one observed recovery went: the 503; a full manual round — Apple ID, password, choose delivery, wait, type — which was refused at the password step; then another round that worked. A round is the better part of a minute, so the account was still refusing about a minute after the 503, and what worked was roughly two rounds out.
The honest caveat, carried over: that middle refusal was on the password call rather than the 2FA call, so it does not strictly prove a new code would have been rejected at that instant. It is the only measurement there is and it points one way.
ACodeAppleAlreadyTookTestfails if either number is lowered, with the reasoning attached, so shortening them has to be a decision rather than a tidy-up.Not done: the upstream fix
_gsa_requestfolds every non-OK status intoUnhandledProtocolErrorcarrying only the number, andfindmy/errors.pyhas no transient-failure type — so the classification here matches on the message. A proper type in the fork would let both consumers classify cleanly, but it means re-pinning FindMy.py in all four places (rule 14). Same call as #169: left out deliberately.The net is wide on purpose. Everything it catches happened after the submit returned, so the code is gone in all of them, and the cost of being wrong runs one way: treating a spent code as a typo sends somebody back to type it again and ends in bad Anisette advice, while treating a typo as a spent code costs a wait and a fresh code.
Verified
Branched off
mainand independent of #139.🤖 Generated with Claude Code