Skip to content

Add downgradeFromDPoP for in-place DPoP→Bearer migration - #4142

Merged
sfdctaka merged 2 commits into
forcedotcom:devfrom
sfdctaka:feature/dpop-downgrade-api
Aug 25, 2026
Merged

Add downgradeFromDPoP for in-place DPoP→Bearer migration#4142
sfdctaka merged 2 commits into
forcedotcom:devfrom
sfdctaka:feature/dpop-downgrade-api

Conversation

@sfdctaka

Copy link
Copy Markdown
Contributor

What

Adds SFUserAccountManager.downgradeFromDPoP(_:success:failure:), the inverse of the existing upgradeToDPoP. It rolls a DPoP-bound session back to unbound Bearer in place — same connected app, redirect URI, and scopes — independent of the global usesDPoP flag. It is a no-op (success callback, unchanged account) when the session is already Bearer.

Why

Completes the in-place DPoP migration pair so apps can move a user off DPoP without a full logout/login, mirroring upgradeToDPoP.

Changes

  • SFUserAccountManager — new downgradeFromDPoP: wrapper delegating to migrateRefreshToken with useDPoP:@NO; no-op guard when already Bearer. On success it deletes the obsolete DPoP key pair + nonce-cache entry keyed to the pre-migration credentials, and unregisters the DP user-agent feature marker for the now-Bearer completion.
  • SFOAuthCoordinator (/authorize binding) — decides whether to append dpop_jkt by precedence: explicit per-call override → per-user credentials.tokenType → global flag. Previously an interactive re-login of a downgraded account fell through to the global flag (default on in 14.0) and silently re-bound the session to DPoP. This is an OAuth2 /authorize behavior change and needs maintainer review.
  • AuthFlowTester — "Downgrade from DPoP" action (enabled only for a DPoP-bound session) + downgrade UI test.
  • Tests — coordinator precedence unit tests (17/17 pass) + downgrade delegation / no-op-guard unit tests.

Review notes

  • Touches the OAuth2 credential path, a public API, and the /authorize binding decision — requesting maintainer (Wolf) review before merge; kept as draft.
  • Device re-run of the downgrade UI test's post-downgrade revoke→refresh leg is the remaining verification.

@sfdctaka
sfdctaka requested a review from wmathurin August 24, 2026 21:49
@sfdctaka
sfdctaka marked this pull request as ready for review August 24, 2026 21:50
@github-actions

github-actions Bot commented Aug 24, 2026

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
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1742 15
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1757 15
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 1st parameter 2433 21
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 2nd parameter 2657 13

Generated by 🚫 Danger

Add SFUserAccountManager.downgradeFromDPoP(_:success:failure:), the inverse
of upgradeToDPoP: it rolls a DPoP-bound session back to Bearer in place using
the same connected app, redirect URI, and scopes, independent of the global
useDPoP flag. It is a no-op (success callback, unchanged account) when the
session is already Bearer.

On success it deletes the obsolete DPoP key pair and nonce-cache entry keyed
to the pre-migration credentials, and unregisters the DP user-agent feature
marker for the now-Bearer completion.

Make the downgrade durable across an interactive re-auth: SFOAuthCoordinator
now decides whether to append dpop_jkt to /authorize by precedence — an
explicit per-call override first, then the per-user credential token type,
and only then the global useDPoP flag. Previously an interactive re-login of
a downgraded account fell through to the global flag (default on in 14.0) and
silently re-bound the session to DPoP.

AuthFlowTester: add a "Downgrade from DPoP" action (enabled only for a
DPoP-bound session) and a downgrade UI test. Add unit tests for the
coordinator precedence and for downgrade delegation and the no-op guard.
@sfdctaka
sfdctaka force-pushed the feature/dpop-downgrade-api branch from fa40832 to 4ada124 Compare August 24, 2026 21:53
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
TestsPassed ☑️SkippedFailed ❌️
SalesforceSDKCore iOS ^18 Test Results1009 ran1008 ✅1 ❌
TestResult
SalesforceSDKCore iOS ^18 Test Results
testLogin❌ failure

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.89189% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.80%. Comparing base (1c2dd30) to head (dba4fe2).
⚠️ Report is 1 commits behind head on dev.

Files with missing lines Patch % Lines
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 88.46% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4142      +/-   ##
==========================================
+ Coverage   71.77%   71.80%   +0.03%     
==========================================
  Files         254      254              
  Lines       22804    22837      +33     
==========================================
+ Hits        16367    16398      +31     
- Misses       6437     6439       +2     
Components Coverage Δ
Analytics 70.78% <ø> (ø)
Common 70.97% <ø> (ø)
Core 67.62% <92.10%> (+0.05%) ⬆️
SmartStore 73.45% <ø> (ø)
MobileSync 88.84% <ø> (ø)
Files with missing lines Coverage Δ
...Core/Classes/OAuth/DPoP/DPoPRequestDecorator.swift 95.52% <100.00%> (+0.20%) ⬆️
...lesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m 65.57% <100.00%> (+0.64%) ⬆️
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 67.65% <88.46%> (+0.68%) ⬆️

... and 3 files with indirect coverage changes

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

…truth

Address review: the token-type comparisons were inconsistent — the /authorize
binding gate in SFOAuthCoordinator compared case-insensitively against
SFSDKDPoPRequestDecorator.dpopTokenType, while the migration guards in
SFUserAccountManager inlined a case-sensitive isEqualToString:@"DPoP". On a
server casing variant the two paths could disagree on whether a session is
DPoP-bound.

Add SFSDKDPoPRequestDecorator.isDPoPTokenType: — one case-insensitive predicate
(RFC 6749 §5.1 / RFC 9449 §6.1) keyed off the single dpopTokenType constant,
nil/empty-safe — and route every DPoP-vs-Bearer decision through it: the
coordinator gate, the decorator's own proof-attach gate, the upgrade/downgrade
no-op guards, the migration success-callback cleanup, the DP feature-marker
register/unregister, and the photo-request nonce-harvest guard.

Predicate is public (not internal) so it stays visible to the Objective-C
callers across framework, SPM, and CocoaPods builds. Adds a unit test covering
casing variants and the nil/empty/Bearer cases.
@sfdctaka
sfdctaka merged commit 9dde5ab into forcedotcom:dev Aug 25, 2026
22 of 24 checks passed
@sfdctaka
sfdctaka deleted the feature/dpop-downgrade-api branch August 25, 2026 00:26
@github-actions

Copy link
Copy Markdown
TestsPassed ☑️Skipped ⚠️Failed ❌️
AuthFlowTester UI Test Results all110 ran69 ✅5 ⚠️36 ❌
TestResult
AuthFlowTester UI Test Results all
AuthFlowTesterUITests.xctest
DPoPLoginTests.test_givenDPoPHybrid_whenLogin_thenTokenTypeIsDPoPAndRefreshWorks()❌ failure
DPoPLoginTests.test_givenDPoPUserWithSubsetScopes_whenMigrateToAllScopes_thenDPoPBindingPreserved()❌ failure
DPoPLoginTests.test_givenDPoPSession_whenDowngradeFromDPoP_thenBearerUnbound()❌ failure
DPoPLoginTests.test_givenDPoPUser_whenMigrateToDPoPRtr_thenRefreshTokenRotationEnabled()❌ failure
ForceAdvancedAuthTests.testForceAdvancedAuth_Disabled_StandardServer_UsesInAppWebView()❌ failure
BeaconLoginTests.testBeaconOpaque_DefaultScopes()❌ failure
BeaconLoginTests.testBeaconJwt_SubsetScopes()❌ failure
BeaconLoginTests.testBeaconJwt_AllScopes()❌ failure
BeaconLoginTests.testBeaconOpaque_AllScopes()❌ failure
BeaconLoginTests.testBeaconJwt_DefaultScopes()❌ failure
BeaconLoginTests.testBeaconOpaque_SubsetScopes()❌ failure
MultiUserLoginTests.testBeaconAndNonBeacon_MultiUser()❌ failure
MultiUserLoginTests.testFlagDiversity_BeaconNonHybridJwtVsHybridOpaque()❌ failure
MultiUserLoginTests.testAdvancedAuthUser_HasBWFlag_RegularAuthUser_DoesNot()❌ failure
WelcomeLoginTests.testWelcomeDiscovery_AdvancedAuthLoginHost()❌ failure
WelcomeLoginTests.testWelcomeDiscovery_AdvancedAuthLoginHost_DynamicConfig()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconOpaque_SubsetScopes()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconOpaque_AllScopes()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconOpaque_DefaultScopes()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconJwt_DefaultScopes()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconJwt_SubsetScopes()❌ failure
AdvancedAuthBeaconLoginTests.testBeaconJwt_AllScopes()❌ failure
LoginWithRestartTests.testBeaconOpaque_DefaultScopes_WithRestart()❌ failure
LoginWithRestartTests.testBeaconJwt_DefaultScopes_DynamicConfiguration_WithRestart()❌ failure
LoginWithRestartTests.testAdvancedAuth_WithRestart()❌ failure
LoginWithRestartTests.testBeaconJwt_SubsetScopes_DynamicConfiguration_WithRestart()❌ failure
RefreshTokenMigrationWithRestartTests.testMigrateCAToBeacon_WithRestart()❌ failure
RefreshTokenMigrationWithRestartTests.testMigrateBeaconScopeAddition_WithRestart()❌ failure
RefreshTokenMigrationWithRestartTests.testMigrateMultipleUsers_WithRestart()❌ failure
RefreshTokenMigrationTests.testMigrateBeacon_AddMoreScopes()❌ failure
RefreshTokenMigrationTests.testFlagDiversity_MigratedBeaconJwtVsNonHybridOpaque()❌ failure
RefreshTokenMigrationTests.testMigrateBeaconToCA()❌ failure
RefreshTokenMigrationTests.testMigrateBeaconOpaqueToJWTAndBack()❌ failure
RefreshTokenMigrationTests.testMigrateCAToBeacon()❌ failure
RefreshTokenMigrationTests.testMigrateCAUserAgentToBeaconWebServer()❌ failure
RefreshTokenMigrationTests.testMigrateCAToBeaconAndBack()❌ failure

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.

2 participants