Skip to content

@W-21933885: [MSDK Android] App Attestation Implementation#2868

Open
JohnsonEricAtSalesforce wants to merge 97 commits intoforcedotcom:devfrom
JohnsonEricAtSalesforce:feature/w-21933885_msdk-android-app-attestation-implementation
Open

@W-21933885: [MSDK Android] App Attestation Implementation#2868
JohnsonEricAtSalesforce wants to merge 97 commits intoforcedotcom:devfrom
JohnsonEricAtSalesforce:feature/w-21933885_msdk-android-app-attestation-implementation

Conversation

@JohnsonEricAtSalesforce
Copy link
Copy Markdown
Contributor

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce commented Apr 15, 2026

🧱 Under Construction: Please Hold Reviews Pending Significant Changes And Tests 🚧

This adds Mobile SDK Android support for:

  1. The new "Challenge" API offered by the Salesforce App Attestation External Client App (ECA) Plug-In
    1.1. The "Challenge" API creates challenges for use as the "hash" when creating Google Play App Integrity API "tokens"
  2. Google Play App Integrity API
    2.1. Tokens can be created using the "Challenge" and then passed to authorization and token refresh requests for validation by the App Attestation ECA Plug-In

The app's entry point is calling a new "updateAppAttestationClient" method on the SalesforceSDKManager with the relevant Google Cloud Project ID. Once that's provided, the manager and the new AppAttestationClient manage the interaction with the Challenge API and Google Play Integrity API. The AppAttestationClient provides "Attestation" values to the existing authorization and token refresh logic when applicable.

Most of the new code follows our existing patterns. I'll include a source code walk-though in the differences as well. None of this is set in stone and we can incorporate feedback here or in follow-up work items.

I've tested this by creating a new test app from the REST Explorer sample. That app is configured for deployment to a Google Play Developer account that is configured for App Integrity with a matching Google Cloud project. I've been able to deploy the app to my internal release track. When installed from there, I'm able to successfully authenticate to the internal MSDK App Attestation Test org I've been given access to.

Test coverage is nearing 100% which is great given authentication's critical priority and high availability threshold.

The Google Play App Integrity API has public documentation here https://developer.android.com/google/play/integrity/overview

There's no user interface component for this change in either mobile or web for screenshots or a demo, alas!

Here's an example of how the client app could (re-)initialize app attestation each time the selected login server changes.

        // Prepare for Salesforce App Attestation.
        getInstance().loginServerManager.selectedServer.observeForever { selectedLoginServer ->
            selectedLoginServer.url.toUri().host.let { selectedLoginServerHost ->
                if (selectedLoginServerHost == "msdkappattestationtestorg.test1.my.pc-rnd.salesforce.com") {
                    Pair<String?, Long?>(selectedLoginServerHost, 762473690072L)
                } else {
                    Pair<String?, Long?>(selectedLoginServerHost, null)
                }
            }.let { appAttestationConfiguration ->
                getInstance().updateAppAttestationClient(appAttestationConfiguration.first ?: "_Ignored_Value_", appAttestationConfiguration.second)
            }
        }

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 15, 2026

22 Warnings
⚠️ libs/SalesforceSDK/build.gradle.kts#L20 - A newer version of com.squareup.okhttp3:okhttp than 4.12.0 is available: 5.3.2
⚠️ libs/SalesforceSDK/build.gradle.kts#L21 - A newer version of com.google.firebase:firebase-messaging than 25.0.0 is available: 25.0.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L22 - A newer version of androidx.core:core than 1.16.0 is available: 1.18.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L32 - A newer version of androidx.core:core-ktx than 1.16.0 is available: 1.18.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L33 - A newer version of androidx.activity:activity-ktx than 1.10.1 is available: 1.13.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L34 - A newer version of androidx.activity:activity-compose than 1.10.1 is available: 1.13.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L35 - A newer version of androidx.lifecycle:lifecycle-viewmodel-ktx than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L36 - A newer version of androidx.lifecycle:lifecycle-viewmodel-compose than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L37 - A newer version of androidx.lifecycle:lifecycle-viewmodel-savedstate than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L38 - A newer version of androidx.lifecycle:lifecycle-service than 2.8.7 is available: 2.10.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L39 - A newer version of org.jetbrains.kotlinx:kotlinx-serialization-json than 1.6.3 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L40 - A newer version of androidx.window:window than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L41 - A newer version of androidx.window:window-core than 1.4.0 is available: 1.5.1
⚠️ libs/SalesforceSDK/build.gradle.kts#L42 - A newer version of androidx.compose.material3:material3-android than 1.3.2 is available: 1.4.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L43 - A newer version of androidx.compose:compose-bom than 2025.07.00 is available: 2026.04.01
⚠️ libs/SalesforceSDK/build.gradle.kts#L44 - A newer version of androidx.compose.foundation:foundation-android than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L45 - A newer version of androidx.compose.runtime:runtime-livedata than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L46 - A newer version of androidx.compose.ui:ui-tooling-preview-android than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L47 - A newer version of androidx.compose.material:material than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L49 - A newer version of androidx.compose.ui:ui-tooling than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/build.gradle.kts#L50 - A newer version of androidx.compose.ui:ui-test-manifest than 1.8.2 is available: 1.11.0
⚠️ libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/NativeLoginManager.kt#L249 - This method should only be accessed from tests or within private scope

Generated by 🚫 Danger

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 81.64557% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.77%. Comparing base (535d8e4) to head (76c91d8).
⚠️ Report is 2 commits behind head on dev.

Files with missing lines Patch % Lines
...alesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt 0.00% 19 Missing ⚠️
...sforce/androidsdk/analytics/logger/FileLogger.java 50.00% 3 Missing and 1 partial ⚠️
.../androidsdk/analytics/logger/SalesforceLogger.java 50.00% 2 Missing ⚠️
...salesforce/androidsdk/auth/AppAttestationClient.kt 98.43% 0 Missing and 1 partial ⚠️
...m/salesforce/androidsdk/auth/NativeLoginManager.kt 88.88% 0 Missing and 1 partial ⚠️
...SDK/src/com/salesforce/androidsdk/auth/OAuth2.java 94.11% 1 Missing ⚠️
...src/com/salesforce/androidsdk/ui/LoginViewModel.kt 85.71% 0 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (50.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##                dev    #2868      +/-   ##
============================================
- Coverage     64.92%   62.77%   -2.16%     
+ Complexity     2976     2904      -72     
============================================
  Files           223      226       +3     
  Lines         17467    17599     +132     
  Branches       2490     2508      +18     
============================================
- Hits          11341    11048     -293     
- Misses         4922     5346     +424     
- Partials       1204     1205       +1     
Components Coverage Δ
Analytics 48.56% <50.00%> (-0.15%) ⬇️
SalesforceSDK 55.94% <84.24%> (-3.89%) ⬇️
Hybrid 59.05% <ø> (ø)
SmartStore 78.20% <ø> (ø)
MobileSync 81.68% <ø> (ø)
React 51.50% <ø> (ø)
Files with missing lines Coverage Δ
.../salesforce/androidsdk/app/SalesforceSDKManager.kt 59.75% <100.00%> (+0.29%) ⬆️
...ndroidsdk/rest/AppAttestationChallengeApiClient.kt 100.00% <100.00%> (ø)
...oidsdk/rest/AppAttestationChallengeApiException.kt 100.00% <100.00%> (ø)
...salesforce/androidsdk/auth/AppAttestationClient.kt 98.43% <98.43%> (ø)
...m/salesforce/androidsdk/auth/NativeLoginManager.kt 20.77% <88.88%> (+10.28%) ⬆️
...SDK/src/com/salesforce/androidsdk/auth/OAuth2.java 78.86% <94.11%> (+1.59%) ⬆️
...src/com/salesforce/androidsdk/ui/LoginViewModel.kt 86.11% <85.71%> (-2.52%) ⬇️
.../androidsdk/analytics/logger/SalesforceLogger.java 38.85% <50.00%> (-0.11%) ⬇️
...sforce/androidsdk/analytics/logger/FileLogger.java 75.00% <50.00%> (-2.78%) ⬇️
...alesforce/androidsdk/auth/idp/IDPAuthCodeHelper.kt 5.95% <0.00%> (-0.90%) ⬇️

... and 21 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch 2 times, most recently from 145a8a9 to 6990e6f Compare April 16, 2026 15:25
@github-actions
Copy link
Copy Markdown

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

Generated by 🚫 Danger

…ndroid Jetpack Data Store Use, Initialize Unit Tests And Code Coverage)
…ll For Unhandled Exceptions Getting Integrity Token)
… Salesforce App Attestation ECA Plugin Challenge API Error Type)
…Test Code Coverage For AppAttestationClient)
…Test Code Coverage For AppAttestationChallengeApiClient)
…Test Code Coverage For NativeLoginManager.createRequestBody)
…empt At Testing App Attestation In NativeLoginManager.login())
…Current Iteration Of Testing App Attestation In NativeLoginManager.login() While Deferring A Comprehensive Test For That Method)
…rage For SalesforceSDKManager.updateAppAttestationClient)
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch from 2be39b2 to 8129ef1 Compare April 17, 2026 17:32
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch from 8129ef1 to a2a451f Compare April 17, 2026 17:59
…dency Injection To OAuth2 And IDPAuthCodeHelper For Testing)

- Add dependency injection for SalesforceSDKManager to OAuth2.getAuthorizationUrl() and getBrandedLoginPath()
- Create backward-compatible overloads that default to SalesforceSDKManager.getInstance()
- Update IDPAuthCodeHelper.getAuthorizationPathForSP() to accept and pass injected SDKManager
- Fix IDPAuthCodeHelperTest to properly mock the 11-parameter OAuth2.getAuthorizationUrl() method signature
- Add OAuth2.TIMESTAMP_FORMAT access before mockkStatic() to force class initialization
- Update OAuth2Test to test new overloaded methods with SalesforceSDKManager parameter

All IDPAuthCodeHelperTest tests now pass (10/10, 100% success rate).
…PAuthCodeHelperTest.idpAuthCodeHelper_getAuthorizationPathForSP_whenAttestationClientReturnsAttestation_includesAttestationInQuery)
…thCodeHelperTest Attestation Tests Missing OAuth2 Mock)

Fixed two tests that were timing out with UncompletedCoroutinesError:
- idpAuthCodeHelper_getAuthorizationPathForSP_whenAttestationClientReturnsAttestation_includesAttestationInQuery
- idpAuthCodeHelper_getAuthorizationPathForSP_whenCreateAppAttestationReturnsNull_excludesAttestationFromQuery

Root cause: Both tests were calling the real OAuth2.getAuthorizationUrl() static
method instead of a mock, causing the test coroutine to hang and timeout after 1 minute.

Resolution: Added stubOAuthAuthorizationUrl() calls in both tests to properly mock
OAuth2.getAuthorizationUrl() before calling getAuthorizationPathForSP(). The first
test now returns a URI with attestation parameter, the second returns a URI without.

This follows the same pattern used in other tests in this file that mock the OAuth2
static method before invoking code that depends on it.
…ging Notes For Test Investigation)

Added commented debugging notes to track test failures and crashes encountered
during attestation test development:

- LoginViewModelTest.kt: Added NullPointerException note for jwtFlow_Changes_loginUrl
  test showing SalesforceKeyGenerator.getSHA256Hash crash with null string

- IDPAuthCodeHelperTest.kt: Added ANR crash note and removed unused Ignore import

- LoginActivityScenarioTest.kt: Added native crash stack trace from Bluetooth
  service (system/gd/stack_manager.cc) affecting testWebviewSettings test

These notes document issues encountered during test execution and will aid in
future debugging and test stabilization efforts.
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch 3 times, most recently from 7d09391 to b9ddfc0 Compare April 25, 2026 17:44
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch from b9ddfc0 to 5f968fa Compare April 25, 2026 18:25
…nt Update To Avoid mockkStatic In IDPAuthCodeHelperTest.kt)
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce force-pushed the feature/w-21933885_msdk-android-app-attestation-implementation branch from 9919599 to d526e06 Compare April 25, 2026 22:46
…e Unit Test Configuration And Code Coverage Report For IDPAuthCodeHelperTest.kt)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 26, 2026

2 Warnings
⚠️ libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/logger/FileLogger.java#L275 - Consider using apply() instead; commit writes its data to persistent storage immediately, whereas apply will handle it in the background
⚠️ libs/SalesforceAnalytics/src/com/salesforce/androidsdk/analytics/logger/FileLogger.java#L296 - Consider using apply() instead; commit writes its data to persistent storage immediately, whereas apply will handle it in the background

Generated by 🚫 Danger

@@ -23,6 +23,7 @@ dependencies {
api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of androidx.browser:browser than 1.8.0 is available: 1.10.0

@@ -23,6 +23,7 @@ dependencies {
api("androidx.browser:browser:1.8.0") // Update requires API 36 compileSdk
api("androidx.work:work-runtime-ktx:2.10.3")
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of androidx.work:work-runtime-ktx than 2.10.3 is available: 2.11.2

@@ -55,6 +56,14 @@ dependencies {
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of androidx.compose.ui:ui-test-junit4 than 1.8.2 is available: 1.11.0

@@ -55,6 +56,14 @@ dependencies {
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
androidTestImplementation("io.mockk:mockk-android:1.14.0") // Update requires Kotlin 2
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of io.mockk:mockk-android than 1.14.0 is available: 1.14.9

androidTestImplementation("io.mockk:mockk-android:1.14.0") // Update requires Kotlin 2

testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.14.0") // Update requires Kotlin 2
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of io.mockk:mockk than 1.14.0 is available: 1.14.9


testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.14.0") // Update requires Kotlin 2
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1") // Update requires Kotlin 2
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of org.jetbrains.kotlinx:kotlinx-coroutines-test than 1.8.1 is available: 1.10.2

testImplementation("androidx.test:core:1.7.0")
testImplementation("androidx.test.ext:junit:1.3.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("org.robolectric:robolectric:4.14.1")
Copy link
Copy Markdown

@github-actions github-actions Bot Apr 26, 2026

Choose a reason for hiding this comment

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

⚠️ A newer version of org.robolectric:robolectric than 4.14.1 is available: 4.16.1

Updated sourceDirectories in both convertCodeCoverage and unitTestCoverageReport
tasks to use the correct source root path. Previously, JaCoCo was looking for
source files at incorrect paths, causing coverage data for files like
IDPAuthCodeHelper.kt to be excluded from XML reports sent to CodeCov.

Changes:
- convertCodeCoverage: Changed from "src/main/java" to "src" and "src/debug"
- unitTestCoverageReport: Changed from "src/com" to "src" and "src/debug"

This ensures JaCoCo can properly map compiled class files to their source files,
allowing CodeCov to receive complete coverage data in CI runs.
assertTrue(viewModel.loginUrl.value!!.contains(newCodeChallenge))
}

@Ignore
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Test is ignored without giving any explanation

import org.junit.Test
import org.junit.runner.RunWith

@Ignore
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Test is ignored without giving any explanation

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