Skip to content

Add token refresh coordinator - #4087

Merged
bbirman merged 5 commits into
forcedotcom:devfrom
bbirman:refreshCoordinator
Jul 10, 2026
Merged

Add token refresh coordinator#4087
bbirman merged 5 commits into
forcedotcom:devfrom
bbirman:refreshCoordinator

Conversation

@bbirman

@bbirman bbirman commented Jun 26, 2026

Copy link
Copy Markdown
Member
  • Adds a layer to coordinate token refreshes so that only a single refresh is requested at a time across SFRestAPI, SFIdentityCoordinator and SFUserAccountManager
  • Consumers calling refresh directly should go through SFUserAccountManager
  • Deprecates SFOAuthSessionRefresher so that it will be internal only in the future

@github-actions

Copy link
Copy Markdown
1 Warning
⚠️ Big PR, try to keep changes smaller if you can.

Generated by 🚫 Danger

@github-actions

Copy link
Copy Markdown
1 Warning
⚠️ Static Analysis found an issue with one or more files you modified. Please fix the issue(s).

Clang Static Analysis Issues

File Type Category Description Line Col
SFRestAPI Nullability Memory error nil assigned to a pointer which is expected to have non-null value 95 5
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1601 15
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1616 15
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 2nd parameter 2257 13

Generated by 🚫 Danger

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.95028% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.47%. Comparing base (29439f0) to head (d234322).
⚠️ Report is 52 commits behind head on dev.

Files with missing lines Patch % Lines
...KCore/Classes/OAuth/SFSDKTokenRefreshCoordinator.m 88.42% 11 Missing ⚠️
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 75.00% 3 Missing ⚠️
...forceSDKCore/Classes/RestAPI/WebSocketClient.swift 83.33% 2 Missing ⚠️
...KCore/SalesforceSDKCore/Classes/Util/SFSDKOAuth2.m 66.66% 2 Missing ⚠️
...ceSDKCore/Classes/Identity/SFIdentityCoordinator.m 0.00% 1 Missing ⚠️
...Core/SalesforceSDKCore/Classes/RestAPI/SFRestAPI.m 97.36% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4087      +/-   ##
==========================================
- Coverage   70.79%   68.47%   -2.33%     
==========================================
  Files         246      247       +1     
  Lines       21541    21614      +73     
==========================================
- Hits        15250    14800     -450     
- Misses       6291     6814     +523     
Components Coverage Δ
Analytics 70.78% <ø> (ø)
Common 70.79% <ø> (-0.19%) ⬇️
Core 62.17% <88.95%> (-3.46%) ⬇️
SmartStore 73.60% <ø> (ø)
MobileSync 88.56% <ø> (-0.39%) ⬇️
Files with missing lines Coverage Δ
...DKCore/Classes/Extensions/UserAccountManager.swift 28.57% <100.00%> (+22.11%) ⬆️
...rceSDKCore/Classes/OAuth/SFOAuthSessionRefresher.m 91.54% <100.00%> (+2.50%) ⬆️
...ceSDKCore/Classes/Identity/SFIdentityCoordinator.m 47.72% <0.00%> (-24.13%) ⬇️
...Core/SalesforceSDKCore/Classes/RestAPI/SFRestAPI.m 89.45% <97.36%> (-0.19%) ⬇️
...forceSDKCore/Classes/RestAPI/WebSocketClient.swift 87.34% <83.33%> (ø)
...KCore/SalesforceSDKCore/Classes/Util/SFSDKOAuth2.m 76.17% <66.66%> (-2.15%) ⬇️
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 54.62% <75.00%> (-7.54%) ⬇️
...KCore/Classes/OAuth/SFSDKTokenRefreshCoordinator.m 88.42% <88.42%> (ø)

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
TestsPassedSkippedFailed ❌️
AuthFlowTester UI Test Results all1 ran1 ❌
TestResult
AuthFlowTester UI Test Results all
AuthFlowTesterUITests.xctest
LegacyLoginTests.testCAOpaque_DefaultScopes_WebServerFlow()❌ failure

@github-actions

Copy link
Copy Markdown
TestsPassed ☑️SkippedFailed ❌️
SalesforceSDKCore iOS ^26 Test Results694 ran693 ✅1 ❌
TestResult
SalesforceSDKCore iOS ^26 Test Results
SFSDKAuthUtilTests.testSingleRefreshWithRevokedAccessToken()❌ failure

Comment thread libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuth2.m
Comment thread libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFSDKAuthUtilTests.swift Outdated
Comment thread libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFSDKAuthUtilTests.swift Outdated
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
TestsPassed ✅SkippedFailed
SalesforceSDKCore iOS ^18 Test Results694 ran694 ✅
TestResult
No test annotations available

@bbirman

bbirman commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Updated based on feedback and after some more deliberating, moved the completion blocks back to the main thread

@bbirman
bbirman requested a review from wmathurin July 8, 2026 06:47

@sfdctaka sfdctaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@bbirman
bbirman merged commit 6e09678 into forcedotcom:dev Jul 10, 2026
21 of 23 checks passed
@bbirman
bbirman deleted the refreshCoordinator branch July 10, 2026 17:02
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 16, 2026
…baseline

Two test-only resolutions plus a baseline decision, all triaged against the
pre-migration oracle (unmigrated ObjC at merge-base 6ed0ab4).

- SFUserAccountPhotoTests.testPhotoWithoutCompletionBlock: the migration changed
  the assertion from the ObjC original's XCTAssertNotNil(user.photo) to
  XCTAssertTrue over a reference-equality poll (user.photo == testPhoto). The
  `photo` getter re-decodes from disk into a NEW UIImage when _photo is nil
  (byte-faithful to ObjC SFUserAccount.m:170-185), so ref-equality never
  converges and the poll times out. Restored the ObjC assertion semantics
  (wait for the async setPhoto to settle, then assert final state). Both photo
  tests pass. Test-only; production unchanged.

- SalesforceOAuthUnitTests/testCredentialsCoding: already green (resolved by the
  cluster forcedotcom#1 init?(coder:) decode fix, deae6b0); confirmed by run. No change.

- SFSDKAuthUtilTests.testOpenIDToken: baselined as pre-existing/old-refresh-flow,
  NOT a migration regression. A 3-way oracle comparison proved it: the same
  intermittent setUp auth-refresh hang (listener status stuck 'waiting' -> 30s
  timeout, callback never fires) reproduces in the UNMIGRATED ObjC at our
  merge-base 6ed0ab4, while current dev -- which carries upstream's new token
  refresh coordinator (PR forcedotcom#4087 / 8f597c9) ~155 commits ahead -- runs 9/9
  green. So it is an old-refresh-flow defect upstream already fixed, independent
  of the ObjC->Swift migration. Likely refresh-token rotation. Will be
  superseded when the refresh-coordinator work is pulled via the port queue.

Baseline entries added with rationale (anti-laundering: not migration-caused).
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 17, 2026
…0.2h)

Five live-org test classes (RestClientPublisherTests, RestClientTest,
SalesforceRestAPITests, SFSDKAuthUtilTests, MobileSync SyncManagerTestCase)
run TestSetupUtils.synchronousAuthRefresh() in class func setUp(). The
pre-token-refresh-coordinator OAuth flow hangs in the simulator (the refresh
callback never fires, 30s timeout, returnStatus stays 'waiting'), and the old
fatal assert(returnStatus == didLoad) then trapped the test host before any
test ran. xcodebuild restarts, re-traps, exceeds max-restart-count, and aborts
the ENTIRE run -- silently masking every class alphabetically after the first
live-org class. That masking is what hid the 3 migration regressions found in
the 2026-07-17 oracle revalidation.

A fresh, independently-verified-valid refresh token does NOT fix the hang: the
defect is the old refresh flow itself (fixed upstream by the token refresh
coordinator, 997c4e0 / PR forcedotcom#4087 / 8f597c9), not token staleness. So the
only local remedy is to degrade the abort into a clean per-class skip.

Fix (test-harness only):
- TestSetupUtils: record `authRefreshDidSucceed` instead of asserting; log a
  warning when the live refresh doesn't complete.
- The 5 live classes: XCTSkipUnless(authRefreshDidSucceed) in setUpWithError so
  they skip cleanly instead of trapping the host.
- SalesforceRestAPITests.tearDown: guard on authRefreshDidSucceed -- tearDown
  runs even after a skip, and its cleanup() sent a live REST request with no
  session, tripping a separate assert (SFRestAPI.swift:262) and restart-looping.

This is a DELIBERATE divergence from the merge-base oracle. It is intended to
conflict with the eventual token-refresh-coordinator port as a detectable
"revisit me" marker; see the comment block in TestSetupUtils.swift.

Verified: the full SalesforceSDKCore suite now runs to the alphabetical end
(WebViewStateManagerTests executes) with only 2 self-recovering restarts and no
run-ending abort -- 481 passed / 95 skipped / 4 distinct failing / 0 aborts.
Removing the mask surfaced one deterministic failure,
SFNetworkTests.testSessionSharing, which is pending merge-base-oracle triage.

Docs: .claude/test-baseline.md (Live-auth abort hardening section; also
tightened the two SmartStore baseline entries) + tracker row P0.2h.
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 20, 2026
…cedotcom/dev b5d37d8

- Add forcedotcom remote; target b5d37d8 = forcedotcom/dev HEAD, marker bac0171 ancestor.
- Re-seed .claude/upstream-sync-backlog.md with all 49 first-parent units in strict upstream
  order (21 libs-prod / 15 test-proj / 13 non-libs / 9 flaky-stabilize); archive drained-12 pass.
- Re-derive .claude/test-baseline-ids.txt at HEAD 5a9d307: SDKCore 497p/3f, SmartStore 176p/1f;
  drop stale live-gated entries (testRedirect/testOpenIDToken now SKIP) + testGetGlobalStoreNames.
- Record live-org-skip-ledger REVALIDATION 2026-07-19 (deferrals re-checked; forcedotcom#4087 = unit 44).

Analysis only; no product code, no marker move.
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 20, 2026
… 18/49)

Test-only (live-gated class). Bump all 4 publisher-test timeouts (5/5/10/10 -> 60)
and route the shared evaluateResults sink through .receive(on: DispatchQueue.main)
for deterministic main-thread delivery; inline generateRecordName. Preserved our
migration deltas: the live-gate setUpWithError XCTSkipUnless(authRefreshDidSucceed)
and the RestClient.sharedInstance/CompositeRequestBuilder.addRequest API names.

LIVE-gated class: SDKCore TEST BUILD ✓ (0 new warnings) is the gate; tests SKIP at
runtime until the token-refresh coordinator lands (unit 44, forcedotcom#4087). The known
pre-coordinator auth-refresh hang is in class func setUp() (before the instance
skip); oracle-identical, not a regression. Baseline unchanged.
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 20, 2026
… (unit 44)

Semantic re-implementation of forcedotcom/dev 6e09678 (merge of
bbirman/refreshCoordinator) onto the ObjC→Swift migration branch.

WHAT: introduces SFSDKTokenRefreshCoordinator — a process-wide singleton that
coalesces concurrent token-refresh requests per credential (keyed by
credentials.identifier) so at most one refresh is in-flight at a time. This
prevents the double-spend race with single-use (rotating) refresh tokens, where
concurrent refreshes would invalidate each other's tokens. Callbacks are
delivered on the main queue; background-task protection wraps the refresh.

MIGRATION NOTES (no new ObjC; compiled path is Swift):
- NEW SFSDKTokenRefreshCoordinator.swift (upstream added .h/.m ObjC) —
  @objc singleton, wired into pbxproj (framework Sources + Headers-free).
  NEW SFSDKTokenRefreshCoordinatorTests.swift (upstream .m, +602) ported to
  Swift and wired to the test target.
- SFRestAPI.swift / SFIdentityCoordinator.swift / SFUserAccountManager.swift /
  UserAccountManager.swift (async refresh) / SFSDKOAuth2.swift /
  SFSDKTestRequestListener.swift: the compiled Swift twins carry the behavior
  change (route through the coordinator; SFRestAPI: sessionRefreshInProgress +
  pendingRequestsBeingProcessed + oauthSessionRefresher collapse to a single
  refreshCycleActive flag; cleanup now delivers "User logged out" and cancels
  in-flight tasks; OAuth2 completion no longer double-hops main queue;
  TestRequestListener spins the run loop instead of a semaphore to avoid a
  main-thread deadlock now that the coordinator delivers on main).
- WebSocketClient.swift: TokenRefreshCoordinator actor renamed
  WebSocketReconnectCoordinator (it only gates reconnection; token dedup now
  lives in SFSDKTokenRefreshCoordinator).
- Public-API deprecation of SFOAuthSessionRefresher (14.0→15.0): expressed on
  the Swift members consumers call (@available deprecated), with non-deprecated
  internal seams (init(internalCredentials:) / refreshSessionInternal) that the
  coordinator, tests, and mock use so the SDK's own paths stay warning-free —
  mirrors the unit-39 forceAdvancedAuthenticationInternal precedent. Upstream's
  ObjC SFSDK_DEPRECATED lives on tombstoned headers, so it has no compiled home.
- De-referenced .m/.h mirrors (SFRestAPI.m, SFIdentityCoordinator.m,
  SFSDKOAuth2.m, SFSDKTestRequestListener.m, SFOAuthSessionRefresher.m/.h and
  the two test .m) ref-synced to the upstream post-image on top of the migrated
  pre-image (keeping the migration's @import/-Swift.h deltas); tombstone headers
  skipped; SFUserAccountManager.m had nothing to sync (refreshCredentials lives
  in the .swift twin).

ESCALATION (flag for human PR review): OAuth/token-refresh control flow +
public-API deprecation (SFOAuthSessionRefresher). This is the live-auth
unblocker for Phase 2.

Gate: SDKCore/SmartStore/MobileSync build-for-testing all GREEN, 0 new
warnings; 22 targeted tests pass (11 coordinator + 7 data-task-race + 4
refresher). Live-org auth-util end-to-end tests remain XCTSkip-gated (Phase 2).

Upstream: 6e09678 (PR forcedotcom#4087) · marker unit 44 · __U44__
JohnsonEricAtSalesforce added a commit to JohnsonEricAtSalesforce/SalesforceMobileSDK-iOS that referenced this pull request Jul 20, 2026
… (unit 44)

Semantic re-implementation of forcedotcom/dev 6e09678 (merge of
bbirman/refreshCoordinator) onto the ObjC→Swift migration branch.

WHAT: introduces SFSDKTokenRefreshCoordinator — a process-wide singleton that
coalesces concurrent token-refresh requests per credential (keyed by
credentials.identifier) so at most one refresh is in-flight at a time. This
prevents the double-spend race with single-use (rotating) refresh tokens, where
concurrent refreshes would invalidate each other's tokens. Callbacks are
delivered on the main queue; background-task protection wraps the refresh.

MIGRATION NOTES (no new ObjC; compiled path is Swift):
- NEW SFSDKTokenRefreshCoordinator.swift (upstream added .h/.m ObjC) —
  @objc singleton, wired into pbxproj (framework Sources + Headers-free).
  NEW SFSDKTokenRefreshCoordinatorTests.swift (upstream .m, +602) ported to
  Swift and wired to the test target.
- SFRestAPI.swift / SFIdentityCoordinator.swift / SFUserAccountManager.swift /
  UserAccountManager.swift (async refresh) / SFSDKOAuth2.swift /
  SFSDKTestRequestListener.swift: the compiled Swift twins carry the behavior
  change (route through the coordinator; SFRestAPI: sessionRefreshInProgress +
  pendingRequestsBeingProcessed + oauthSessionRefresher collapse to a single
  refreshCycleActive flag; cleanup now delivers "User logged out" and cancels
  in-flight tasks; OAuth2 completion no longer double-hops main queue;
  TestRequestListener spins the run loop instead of a semaphore to avoid a
  main-thread deadlock now that the coordinator delivers on main).
- WebSocketClient.swift: TokenRefreshCoordinator actor renamed
  WebSocketReconnectCoordinator (it only gates reconnection; token dedup now
  lives in SFSDKTokenRefreshCoordinator).
- Public-API deprecation of SFOAuthSessionRefresher (14.0→15.0): expressed on
  the Swift members consumers call (@available deprecated), with non-deprecated
  internal seams (init(internalCredentials:) / refreshSessionInternal) that the
  coordinator, tests, and mock use so the SDK's own paths stay warning-free —
  mirrors the unit-39 forceAdvancedAuthenticationInternal precedent. Upstream's
  ObjC SFSDK_DEPRECATED lives on tombstoned headers, so it has no compiled home.
- De-referenced .m/.h mirrors (SFRestAPI.m, SFIdentityCoordinator.m,
  SFSDKOAuth2.m, SFSDKTestRequestListener.m, SFOAuthSessionRefresher.m/.h and
  the two test .m) ref-synced to the upstream post-image on top of the migrated
  pre-image (keeping the migration's @import/-Swift.h deltas); tombstone headers
  skipped; SFUserAccountManager.m had nothing to sync (refreshCredentials lives
  in the .swift twin).

ESCALATION (flag for human PR review): OAuth/token-refresh control flow +
public-API deprecation (SFOAuthSessionRefresher). This is the live-auth
unblocker for Phase 2.

Gate: SDKCore/SmartStore/MobileSync build-for-testing all GREEN, 0 new
warnings; 22 targeted tests pass (11 coordinator + 7 data-task-race + 4
refresher). Live-org auth-util end-to-end tests remain XCTSkip-gated (Phase 2).

Upstream: 6e09678 (PR forcedotcom#4087) · marker unit 44 · __U44__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants