Skip to content

@W-24269744 fix: Bound the iOS DPoP nonce cache - #4180

Open
wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:fix-ios-dpop-nonce-cache
Open

wmathurin wants to merge 2 commits into
forcedotcom:devfrom
wmathurin:fix-ios-dpop-nonce-cache

Conversation

@wmathurin

@wmathurin wmathurin commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Description

Reworks the process-lifetime iOS DPoP nonce cache from a flat composite-key
dictionary into scope-indexed state.

  • Makes exact nonce lookup, latest-for-scope lookup, and scope clearing O(1)
    expected time.
  • Tracks the most recent write explicitly, removing dictionary-order ambiguity.
  • Bounds each credential scope to 32 HTU entries using least-recently-written
    eviction.
  • Compacts stale eviction records so repeated nonce rotation remains bounded.
  • Preserves the existing Swift and Objective-C API, canonical HTU handling,
    thread-safety model, and account lifecycle cleanup.

Spec PR: https://git.soma.salesforce.com/SalesforceMobileSDK/SalesforceMobileSDK-Workspace/pull/112

Resolves

W-24269744

Testing

Tests in the PR

  • Manual Tests
  • Unit Tests
  • Integration Tests

Manual Tests

No manual authentication flow is required for this internal cache data-structure
change.

  • Ran SFSDKDPoPTests on iPhone 16 / iOS 18.6.
  • 55 tests passed, 0 failed, 0 skipped.
  • Added deterministic latest-write, capacity, rewritten-entry eviction, and
    concurrent bounded-capacity coverage.
  • Ran git diff --check and reviewed the diff for public API changes and
    sensitive logging.

@wmathurin wmathurin self-assigned this Sep 22, 2026
@github-actions

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

github-actions Bot commented Sep 22, 2026

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

@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.94%. Comparing base (822df13) to head (56b708f).
⚠️ Report is 6 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4180      +/-   ##
==========================================
+ Coverage   67.81%   67.94%   +0.12%     
==========================================
  Files         254      254              
  Lines       23040    23083      +43     
==========================================
+ Hits        15625    15684      +59     
+ Misses       7415     7399      -16     
Components Coverage Δ
Analytics 71.17% <ø> (+0.39%) ⬆️
Common 71.76% <ø> (+0.55%) ⬆️
Core 61.85% <100.00%> (+0.14%) ⬆️
SmartStore 73.45% <ø> (ø)
MobileSync 88.90% <ø> (+0.05%) ⬆️
Files with missing lines Coverage Δ
...rceSDKCore/Classes/OAuth/DPoP/DPoPNonceCache.swift 95.00% <100.00%> (+3.57%) ⬆️

... and 6 files with indirect coverage changes

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

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

Reviewed at commit 56b708f. This reworks the DPoP nonce cache from a flat
[compositeKey: String] dictionary into per-scope state ([scope: ScopeCache]), replacing the
old linear scan for "latest nonce in this scope" with an explicit latestHTU pointer, and adds
a 32-entry-per-scope bound with least-recently-written eviction.

Correctness of the three headline claims, verified against the code (not just the PR
description):

  • Exact lookup, latest-for-scope, and clear-for-scope are all genuinely O(1) expected time now
    — direct dictionary lookups, no scan. clear(forScope:) in particular improves beyond what
    was asked: it now only touches the target scope's own entry, versus the old code's O(n) walk
    over every key in the whole cache across all scopes.
  • The old "latest" lookup was relying on Swift dictionary iteration order, which is undefined —
    that was the real root cause behind the reported bug, not just the scan cost. The new
    latestHTU field, updated on every write, replaces that with an explicit, correct pointer.
  • The eviction design correctly distinguishes "oldest write" from "oldest currently-live entry":
    evictIfNeeded only evicts a FIFO record if entries[htu]?.sequence still matches that
    record's sequence, so a HTU that's been rewritten since its FIFO record was queued survives —
    this is what makes test_givenRewrittenNonce_whenCapacityExceeded_thenRewriteRefreshesEvictionOrder
    pass and is the subtle part of the LRU-ish design that would be easy to get wrong.

Trace against a nonce rotation exceeding 32 entries while a retry is in flight: every real
caller (DPoPRequestDecorator.decorate, invoked from the OAuth coordinator, the token-endpoint
helper, and SFRestAPI's resource-call retry path) reads the cache fresh immediately before
building each proof rather than holding a nonce value across a round-trip, and the entry just
written can never be the one evicted in the same call (eviction only pops from the FIFO's
oldest end, and the just-written entry always has the newest sequence number). Worst case for a
resource-server nonce evicted between attempts is a harmless fallback to the token-endpoint
nonce or one extra use_dpop_nonce round-trip — not a wrong or missing proof.

Trace against the "eviction bookkeeping itself stays bounded" claim: confirmed by
arithmetic for the pathological case (one HTU rewritten indefinitely, which never trips the
main 32-entry eviction): compactWriteOrderIfNeeded's liveRecordCount > 64 guard caps how far
the writeOrder FIFO can grow between compactions, and compaction rebuilds it down to
entries.count. The self-referential concern in the PR description (an eviction mechanism that
itself leaks) doesn't materialize.

Test coverage: the four new tests each map to one of the properties above (deterministic
latest-write, capacity eviction, rewrite-refreshes-eviction-order, concurrent bounded capacity)
and the assertions in each match what the test name claims — no mismatches or vacuous
assertions found. The concurrent test's per-iteration _ = latest(forScope:) call after each
write is worth noting as deliberate synchronization (GCD guarantees a .sync enqueued after an
.async on the same queue drains everything ahead of it) rather than incidental — that's what
makes the post-loop assertion deterministic instead of racy.

Public API: unchanged — every @objc selector on DPoPNonceCache keeps its name and
signature. The renamed helper (cacheKey -> scopeKey) is private static.

Cross-platform: no Android twin for this exact nonce-cache finding was found in this
review batch, so there's no parity check to perform here.

Approving on the merits above.

This review was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

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

This branch has not been deployed

No deployments
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