iclouddrive: fix 2FA failing with 409 even when the code is valid

Since around mid-2026 Apple's idmsa endpoints `POST
/verify/trusteddevice/securitycode` and `POST /verify/phone/securitycode`
return HTTP 409 (instead of 2xx) even when the submitted code is accepted:
the response body carries `"securityCode": {..., "valid": true}` and the
response headers include a fresh X-Apple-Session-Token, scnt and
X-Apple-Auth-Attributes, which are only issued on successful validation.

rclone treated any 409 as failure and aborted before TrustSession, so
configuring an iclouddrive remote always failed after the 2FA step with:

    validate2FACode failed: HTTP error 409 (409 ) returned body:
    "{... \"securityCode\": {\"code\": \"...\", \"valid\": true} ...}"

Treat a 409 response that carries X-Apple-Session-Token as success: absorb
the session headers and continue to TrustSession. Applies to both the
trusted-device and SMS validation paths.

Fixes #9488
Closes #9534

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Punya Jain
2026-07-29 20:25:57 +01:00
committed by GitHub
co-authored by Claude Opus 5
parent d183f4a43d
commit 731f2a6c29
+24 -2
View File
@@ -194,6 +194,22 @@ func (s *Session) Request(ctx context.Context, opts rest.Opts, request any, resp
return resp, nil
}
// acceptedDespiteConflict returns true when Apple signals a successful code
// validation with HTTP 409. Since ~mid-2026, idmsa returns 409 on the
// securitycode endpoints even when the code is accepted (body carries
// securityCode.valid=true), but it still issues X-Apple-Session-Token on
// success. Treat token issuance as ground truth and absorb the headers so
// TrustSession can proceed.
func (s *Session) acceptedDespiteConflict(resp *http.Response) bool {
if resp == nil || resp.StatusCode != 409 || resp.Header.Get("X-Apple-Session-Token") == "" {
return false
}
s.mu.Lock()
s.extractHeaders(resp)
s.mu.Unlock()
return true
}
// Requires2FA returns true if the session requires 2FA
func (s *Session) Requires2FA() bool {
if s.needs2FA {
@@ -660,7 +676,10 @@ func (s *Session) Validate2FACode(ctx context.Context, code string) error {
NoResponse: true,
}
_, err = s.Request(ctx, opts, nil, nil)
resp, err := s.Request(ctx, opts, nil, nil)
if err != nil && s.acceptedDespiteConflict(resp) {
err = nil
}
if err == nil {
if err := s.TrustSession(ctx); err != nil {
return err
@@ -788,7 +807,10 @@ func (s *Session) ValidateSMSCode(ctx context.Context, code string, phoneID int,
Body: body,
NoResponse: true,
}
_, err = s.Request(ctx, opts, nil, nil)
resp, err := s.Request(ctx, opts, nil, nil)
if err != nil && s.acceptedDespiteConflict(resp) {
err = nil
}
if err == nil {
if err := s.TrustSession(ctx); err != nil {
return err