From 731f2a6c29dfddcbea2be0ecfacf6a328c99849f Mon Sep 17 00:00:00 2001 From: Punya Jain <4511945+punya10@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:25:57 -0400 Subject: [PATCH] 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 --- backend/iclouddrive/api/session.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/backend/iclouddrive/api/session.go b/backend/iclouddrive/api/session.go index fa3d2d9f0..3fd964cc1 100644 --- a/backend/iclouddrive/api/session.go +++ b/backend/iclouddrive/api/session.go @@ -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