Skip to content

Default DPoP on for new logins in MSDK 14 - #2996

Open
sfdctaka wants to merge 4 commits into
forcedotcom:devfrom
sfdctaka:dpop-default-on
Open

Default DPoP on for new logins in MSDK 14#2996
sfdctaka wants to merge 4 commits into
forcedotcom:devfrom
sfdctaka:dpop-default-on

Conversation

@sfdctaka

Copy link
Copy Markdown
Contributor

What

Flips SalesforceSDKManager.useDPoP from false to true. DPoP (RFC 9449) is now validated by the server, so new apps and new logins request DPoP-bound tokens by default.

The change governs new logins only — the per-credential attachment gate keeps existing Bearer credentials Bearer and existing DPoP credentials bound.

Changes

  • SalesforceSDKManager.kt — default flipped to true.
  • AuthFlowTest.cleanup() — resets useDPoP to the new true default between tests, so non-overriding UI classes gain DPoP coverage for free.
  • LegacyLoginTests — added as the dedicated Bearer (non-DPoP) class using the CA opaque config, forcing DPoP off via a @Before and per-call override.
  • DPoPLoginTests — class doc updated for the new reset value; added the missing SalesforceSDKManager import so androidTest compiles.

Rebase note

This is a small, self-contained flip that overlaps the DPoP upgrade/migration work in #2995 (both touch DPoP defaults and the AuthFlowTester test classes). Whichever of the two merges first, the other will need a rebase. No preference on order.

Testing

Tested and passing.

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

Are there updates needed in native/NativeSampleApps/AuthFlowTester/README.md ?
I noticed a divergence between iOS and Android for LegacyLoginTests (I guess some of the iOS legacy tests are in the CAScopeSelectionLoginTests). Should we move tests around to make them more similar ??

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

The flag flip itself is correct and well-scoped: I traced every read of useDPoP and it only feeds new-login paths (the login-options UI seed and the /authorize dpop_jkt gate in addDpopJktIfNeeded). Resource-time attachment goes through the per-credential DPoPKeyManager.shouldAttachDPoP(tokenType) gate that landed in PR 2981, which fast-exits Bearer credentials — so flipping the default cannot retroactively bind existing Bearer sessions. The KDoc update saying so is accurate and worth having. Two test issues need fixing before this merges.

1. Test not updated for the new default (blocking).
LoginViewModelTest.selectedServer_Changes_GenerateCorrectAuthorizationUrl (in the SalesforceSDKTest instrumented target, run under the unit-tests-pr check) fails. I reproduced it locally on an api36 emulator:

expected: …YQV9KHv_teaXsor5cP6Q
actual:   …YQV9KHv_teaXsor5cP6Q&dpop_jkt=WdHHE-FZZCXootlaTGazbEjPNJkU6KbwRd1Kg-2cxhY

The test's helper generateExpectedAuthorizationUrl builds the expected /authorize URL with an empty extra-params map, so it never contains dpop_jkt. The test's first assertion passes because the default server is login.salesforce.com (a pool server, which addDpopJktIfNeeded excludes); the second assertion fails once selectedServer switches to shouldMatchNothing.salesforce.com — a my-domain (non-pool) server — because with the default now true, addDpopJktIfNeeded appends &dpop_jkt=… to the actual loginUrl. This test file isn't touched by the PR. Please update the expected-URL assertion/helper to account for dpop_jkt on my-domain servers under the new default.

2. The "free DPoP coverage" for CA-opaque default-path tests isn't actually wired (blocking).
BootConfigLoginTests.testCAOpaque_DefaultScopes_WebServerFlow fails with "Expected no 'DP' flag for non-DPoP session." The reason: a default-path CA_OPAQUE login has needsLoginOptions == false, so the login-options screen is skipped and disableDPoP() never runs — the login binds DPoP under the new global default and the UA gets the DP marker — but loginAndValidate(useDPoP: Boolean = false) still defaults false, so validateUser(isDpop = false) asserts no DP marker. The cleanup() reset was flipped to useDPoP = true but the loginAndValidate param default was not, so they now contradict each other.

Worth looking at how the iOS twin (4134) structured this, since iOS doesn't hit this gap: iOS keys the DP-marker assertion off the app config (userAppConfig.isDPoP) rather than the login param, and its login() unconditionally applies the DPoP toggle, so passing useDPoP: false truly disables it. Two ways to fix on Android: (a) flip the loginAndValidate / helper useDPoP defaults to true to match cleanup() (LegacyLoginTests already passes false explicitly, so it stays a Bearer holdout); or (b) have the CA_OPAQUE default-path callers pass useDPoP = true. Option (a) mirrors iOS most closely.

Notes (non-blocking)

  • The remaining ui-tests-pr failures (roughly two dozen distinct tests across unrelated classes) all carry the "Failed to inject touch input" Custom-Tab hand-off signature — the known environment flake, not this PR. Flagging so it's not mistaken for fallout from the flip. It does mean finding 2 is under-reported by the harness, but the one clean CA_OPAQUE default-path failure already surfaces it.
  • LegacyLoginTests as a dedicated Bearer holdout (forcing DPoP off via @Before + explicit per-call useDPoP = false) is a good pattern and matches the iOS twin.

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

@sfdctaka

Copy link
Copy Markdown
Contributor Author

@JohnsonEricAtSalesforce This #2995 PR is the bigger and more important one, and will affect this PR. I will address your feedback that you made here in this PR once 2995 goes in first.

@sfdctaka

Copy link
Copy Markdown
Contributor Author

Fixed in 21c9c17 — thanks for catching both.

Updated native/NativeSampleApps/AuthFlowTester/README.md:

  • cleanup() reset value (DPoPLoginTests section): no longer says it resets DPoP to false. It now states that MSDK 14 defaults DPoP on for new logins (SalesforceSDKManager.useDPoP defaults to true), so cleanup() resets it to true, and that turning it off is the explicit Bearer compatibility path (LegacyLoginTests).
  • Manual-testing Login Options entry: the Use DPoP toggle description no longer says "(default: off)". It now notes the toggle initializes from SalesforceSDKManager.useDPoP and therefore defaults on as of MSDK 14, with turning it off called out as the Bearer path.

Verified the toggle wiring: LoginOptionsActivity seeds useDPoP = MutableLiveData(SalesforceSDKManager.getInstance().useDPoP), and this PR sets that default to true — so "defaults on" is accurate.

Flip SalesforceSDKManager.useDPoP default from false to true. DPoP
(RFC 9449) is now validated by the server, so new apps and new logins
request DPoP-bound tokens by default. The per-credential attachment gate
keeps existing Bearer credentials Bearer and existing DPoP credentials
bound, so the change governs new logins only.

Tests:
- AuthFlowTest.cleanup() resets useDPoP to the new true default between
  tests, so non-overriding UI classes gain DPoP coverage for free.
- Add LegacyLoginTests as the dedicated Bearer (non-DPoP) class using the
  CA opaque config, forcing DPoP off via a @before and per-call override.
- Update DPoPLoginTests class doc for the new reset value and add its
  missing SalesforceSDKManager import so androidTest compiles.
MSDK 14 defaults DPoP on for new logins, and this PR changes the test
cleanup() reset from false to true accordingly. Update the README so it
no longer says cleanup() resets DPoP to false, and fix the manual-testing
Login Options entry: the Use DPoP toggle now defaults on (initialized
from SalesforceSDKManager.useDPoP). Turning it off is the explicit Bearer
compatibility path.
The SDK 14.0 useDPoP default of true appends a dpop_jkt parameter to the
generated authorization URL. The general URL-shape assertions in this
class build their expected URLs without dpop_jkt, so pin the flag off for
the default view model under test and restore it in teardown. The
dedicated dpop_jkt tests use a mock manager with an explicit useDPoP
value and are unaffected.
The UI tests build the OAuth authorization URL from the global useDPoP
flag, read at URL-generation time. On the CA opaque all-defaults path the
login helper skips the Login Options screen, so the DPoP toggle is never
applied there; under the new useDPoP=true default that left those logins
DPoP-bound while their assertions expected Bearer.

Pin useDPoP off in a base @before (and reset to off, not true, in
cleanup) so every test starts Bearer. DPoP tests re-enable it explicitly
through the Login Options toggle, whose non-CA-opaque configs always open
that screen. This mirrors iOS, where the DPoP toggle is always applied on
the login-options surface rather than inferred from a helper parameter,
and keeps the fast skip path on its existing surface. LegacyLoginTests no
longer needs its own forceBearer hook now that the base class provides the
baseline.
@sfdctaka

Copy link
Copy Markdown
Contributor Author

@JohnsonEricAtSalesforce Both findings addressed now that #2995 is in. Two focused test-only commits on top of the flip:

  • 2eacd2d1a — finding 1 (LoginViewModelTest)
  • d28f7e378 — finding 2 (AuthFlowTester Bearer baseline)

Finding 1 — LoginViewModelTest.selectedServer_Changes_GenerateCorrectAuthorizationUrl.
Fixed by pinning useDPoP = false on the real SalesforceSDKManager in @Before (restored in teardown), so the default view-model under test builds /authorize URLs without dpop_jkt and the existing expected-URL helper stays valid. I went this way rather than teaching generateExpectedAuthorizationUrl to emit dpop_jkt because the flag-pin matches how this class already handles the same kind of global (isBrowserLoginEnabled), and it keeps the my-domain vs. pool-server distinction out of the helper. The dedicated dpop_jkt tests use a mock manager with an explicit value, so they're unaffected. Verified green locally (92/92).

Finding 2 — CA-opaque default-path DP-marker contradiction.
You diagnosed it exactly: skip path → disableDPoP() never runs → login binds DPoP under the new default, while isDpop = false asserts no DP. The root cause is that the /authorize URL reads the global useDPoP at generation time, but the assertion was keyed off the login param — they can disagree on the skip path.

I did try your option (a) (flip the helper defaults to true) first, but it regresses reliability: it forces the CA-opaque default path through the Login Options → Custom-Tab-regeneration surface, which is exactly where the "Failed to inject touch input" hand-off flake lives. It failed 0/3 locally, every time in the Custom Tab login surface rather than the assertion.

So I mirrored what iOS actually does at the mechanism level instead: iOS keeps its helper defaults false and makes the toggle authoritative on the login surface. On Android I established a Bearer baseline — a base-class @Before pins useDPoP = false before the login surface reads it, and cleanup() now resets to false (not true). Every test therefore starts Bearer and the skip path is honest; DPoP tests opt in through the Login Options toggle, whose non-CA-opaque configs always open that screen (so the toggle is always applied there, as on iOS). This keeps the fast CA-opaque skip path on its existing surface — no new Custom Tab churn — while closing the param-vs-reality gap. LegacyLoginTests no longer needs its own forceBearer() hook now that the base class provides the baseline; its explicit per-call useDPoP = false stays as documentation of Bearer intent.

Net effect vs. the "free DPoP coverage" framing: DPoP coverage stays where it's explicit (DPoPLoginTests + the Login Options toggle), the same partition as the iOS twin — rather than implicitly converting the CA-opaque common path.

Verification note. Finding 1 is green. For finding 2, the fix compiles clean and the one CA-opaque run that got past the Custom Tab passed validateUser (the DP-marker assertion — the thing this finding is about) and then flaked at the subsequent validateApiRequest alert render — the same environment flake you flagged as under-reporting this finding. A fully clean local UI green is blocked by that flake, so I'm leaning on ui-tests-pr in CI for the end-to-end pass; happy to iterate if CI surfaces anything beyond the known hand-off signature.

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