Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
cedf4cf
@W-21146662: [Android] App attestation integration testing (Initial P…
JohnsonEricAtSalesforce Mar 12, 2026
3e10970
@W-21933885: [MSDK Android] App Attestation Implementation (Adapt To …
JohnsonEricAtSalesforce Apr 9, 2026
e44b562
@W-21933885: [MSDK Android] App Attestation Implementation (Extract T…
JohnsonEricAtSalesforce Apr 10, 2026
8b0a839
@W-21933885: [MSDK Android] App Attestation Implementation (Replace A…
JohnsonEricAtSalesforce Apr 15, 2026
affb4bb
@W-21933885: [MSDK Android] App Attestation Implementation (Improve C…
JohnsonEricAtSalesforce Apr 16, 2026
c87260f
@W-21933885: [MSDK Android] App Attestation Implementation (Temporari…
JohnsonEricAtSalesforce Apr 16, 2026
ae5db34
@W-21933885: [MSDK Android] App Attestation Implementation (Improve T…
JohnsonEricAtSalesforce Apr 16, 2026
a0d5581
@W-21933885: [MSDK Android] App Attestation Implementation (Extract C…
JohnsonEricAtSalesforce Apr 16, 2026
75977f3
@W-21933885: [MSDK Android] App Attestation Implementation (Ignore Fl…
JohnsonEricAtSalesforce Apr 16, 2026
767ed2f
@W-21933885: [MSDK Android] App Attestation Implementation (In-Line R…
JohnsonEricAtSalesforce Apr 16, 2026
579501f
@W-21933885: [MSDK Android] App Attestation Implementation (Return Nu…
JohnsonEricAtSalesforce Apr 16, 2026
e0b448b
@W-21933885: [MSDK Android] App Attestation Implementation (Resolve T…
JohnsonEricAtSalesforce Apr 16, 2026
70b8e5f
@W-21933885: [MSDK Android] App Attestation Implementation (Implement…
JohnsonEricAtSalesforce Apr 16, 2026
483e224
@W-21933885: [MSDK Android] App Attestation Implementation (Ignore Fl…
JohnsonEricAtSalesforce Apr 16, 2026
f47c71f
@W-21933885: [MSDK Android] App Attestation Implementation (Increase …
JohnsonEricAtSalesforce Apr 17, 2026
f6d70d2
@W-21933885: [MSDK Android] App Attestation Implementation (Increase …
JohnsonEricAtSalesforce Apr 17, 2026
8f553a8
@W-21933885: [MSDK Android] App Attestation Implementation (Increase …
JohnsonEricAtSalesforce Apr 17, 2026
166ba99
@W-21933885: [MSDK Android] App Attestation Implementation (First Att…
JohnsonEricAtSalesforce Apr 17, 2026
4de71df
@W-21933885: [MSDK Android] App Attestation Implementation (Finalize …
JohnsonEricAtSalesforce Apr 17, 2026
95b5660
@W-21933885: [MSDK Android] App Attestation Implementation (Improve C…
JohnsonEricAtSalesforce Apr 17, 2026
27f36f0
@W-21933885: [MSDK Android] App Attestation Implementation (Test Cove…
JohnsonEricAtSalesforce Apr 17, 2026
4071cad
@W-21933885: [MSDK Android] App Attestation Implementation (Update Te…
JohnsonEricAtSalesforce Apr 17, 2026
0e20e5e
@W-21933885: [MSDK Android] App Attestation Implementation (Light Cle…
JohnsonEricAtSalesforce Apr 17, 2026
d1846c6
@W-21933885: [MSDK Android] App Attestation Implementation (Correct S…
JohnsonEricAtSalesforce Apr 17, 2026
900c398
@W-21933885: [MSDK Android] App Attestation Implementation (Replace T…
JohnsonEricAtSalesforce Apr 17, 2026
4376d76
@W-21933885: [MSDK Android] App Attestation Implementation (New OAuth…
JohnsonEricAtSalesforce Apr 20, 2026
b41a61e
@W-21933885: [MSDK Android] App Attestation Implementation (Temporary…
JohnsonEricAtSalesforce Apr 20, 2026
851109a
@W-21933885: [MSDK Android] App Attestation Implementation (Light Sel…
JohnsonEricAtSalesforce Apr 20, 2026
cbf9769
@W-21933885: [MSDK Android] App Attestation Implementation (Address U…
JohnsonEricAtSalesforce Apr 20, 2026
4f4c7b3
@W-21933885: [MSDK Android] App Attestation Implementation (Correct s…
JohnsonEricAtSalesforce Apr 20, 2026
b9d6ff8
@W-21933885: [MSDK Android] App Attestation Implementation (Correct C…
JohnsonEricAtSalesforce Apr 20, 2026
5e3aa28
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 20, 2026
104d4e1
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 20, 2026
454fd4d
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 20, 2026
e98b26f
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 20, 2026
def5844
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 20, 2026
de82b68
@W-21933885: [MSDK Android] App Attestation Implementation (Resolve T…
JohnsonEricAtSalesforce Apr 21, 2026
9be799f
@W-21933885: [MSDK Android] App Attestation Implementation (Resolve T…
JohnsonEricAtSalesforce Apr 21, 2026
a836e6b
@W-21933885: [MSDK Android] App Attestation Implementation (Light Aut…
JohnsonEricAtSalesforce Apr 22, 2026
220991b
@W-21933885: [MSDK Android] App Attestation Implementation (Updated T…
JohnsonEricAtSalesforce Apr 22, 2026
2ff8936
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 22, 2026
6a4e7f9
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 22, 2026
f068c6f
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 22, 2026
00a5726
@W-21933885: [MSDK Android] App Attestation Implementation (Automated…
JohnsonEricAtSalesforce Apr 22, 2026
1d45ac1
@W-21933885: [MSDK Android] App Attestation Implementation (Revert Li…
JohnsonEricAtSalesforce Apr 22, 2026
8ab6a22
@W-21933885: [MSDK Android] App Attestation Implementation (Restore P…
JohnsonEricAtSalesforce Apr 23, 2026
5d904ae
@W-21933885: [MSDK Android] App Attestation Implementation (Ignore sc…
JohnsonEricAtSalesforce Apr 23, 2026
c13575c
@W-21933885: [MSDK Android] App Attestation Implementation (Limit Int…
JohnsonEricAtSalesforce Apr 23, 2026
2f3ba20
@W-21933885: [MSDK Android] App Attestation Implementation (Add Code …
JohnsonEricAtSalesforce Apr 23, 2026
be0cfe0
@W-21933885: [MSDK Android] App Attestation Implementation (Remove UR…
JohnsonEricAtSalesforce Apr 27, 2026
98f35ed
@W-21933885: [MSDK Android] App Attestation Implementation (Safe-Defa…
JohnsonEricAtSalesforce Apr 27, 2026
177347f
@W-21933885: [MSDK Android] App Attestation Implementation (Revert in…
JohnsonEricAtSalesforce Apr 27, 2026
10cb0d7
@W-21933885: [MSDK Android] App Attestation Implementation (Remove Un…
JohnsonEricAtSalesforce Apr 27, 2026
83c8436
@W-21933885: [MSDK Android] App Attestation Implementation (Reduce Co…
JohnsonEricAtSalesforce Apr 27, 2026
fe493f9
@W-21933885: [MSDK Android] App Attestation Implementation (Revert At…
JohnsonEricAtSalesforce Apr 27, 2026
a8352aa
@W-21933885: [MSDK Android] App Attestation Implementation (Restore T…
JohnsonEricAtSalesforce Apr 27, 2026
43fc1f8
@W-21933885: [MSDK Android] App Attestation Implementation (Update Lo…
JohnsonEricAtSalesforce Apr 28, 2026
0ec8bfe
@W-21933885: [MSDK Android] App Attestation Implementation (Remove Th…
JohnsonEricAtSalesforce Apr 28, 2026
179663c
@W-21933885: [MSDK Android] App Attestation Implementation (Update Co…
JohnsonEricAtSalesforce Apr 30, 2026
bfb78b0
@W-21933885: [MSDK Android] App Attestation Implementation (Update Co…
JohnsonEricAtSalesforce Apr 30, 2026
9d5294c
@W-21933885: [MSDK Android] App Attestation Implementation (Simplify …
JohnsonEricAtSalesforce Apr 30, 2026
9c5da0e
@W-21933885: [MSDK Android] App Attestation Implementation (Refactor …
JohnsonEricAtSalesforce Apr 30, 2026
8387168
@W-21933885: [MSDK Android] App Attestation Implementation (TEMPORARY…
JohnsonEricAtSalesforce Apr 30, 2026
cc5f7db
@W-21933885: [MSDK Android] App Attestation Implementation (Peer Revi…
JohnsonEricAtSalesforce May 5, 2026
1a0e94c
Revert "@W-21933885: [MSDK Android] App Attestation Implementation (T…
JohnsonEricAtSalesforce May 5, 2026
480c38a
@W-21933885: [MSDK Android] App Attestation Implementation (Revert Co…
JohnsonEricAtSalesforce May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/SalesforceSDK/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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

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


implementation("com.google.android.play:integrity:1.6.0")
implementation("com.google.accompanist:accompanist-drawablepainter:0.37.3")
implementation("com.google.android.material:material:1.13.0") // remove this when all xml is gone
implementation("androidx.appcompat:appcompat:1.7.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import com.salesforce.androidsdk.app.Features.FEATURE_BROWSER_LOGIN
import com.salesforce.androidsdk.app.Features.FEATURE_NATIVE_LOGIN
import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.DARK
import com.salesforce.androidsdk.app.SalesforceSDKManager.Theme.SYSTEM_DEFAULT
import com.salesforce.androidsdk.auth.AppAttestationClient
import com.salesforce.androidsdk.auth.AuthenticatorService.KEY_INSTANCE_URL
import com.salesforce.androidsdk.auth.HttpAccess
import com.salesforce.androidsdk.auth.HttpAccess.DEFAULT
Expand Down Expand Up @@ -226,6 +227,54 @@ open class SalesforceSDKManager protected constructor(
*/
val loginActivityClass: Class<out Activity> = nativeLoginActivity ?: webViewLoginActivityClass

/**
* The client side implementation of the Salesforce App Attestation External
* Client App (ECA) Plugin or null when app attestation is disabled.
*
* This property is not intended for public use outside of Salesforce Mobile
* SDK
*
* TODO: Make this Kotlin-internal once it is no longer referenced by Java. ECJ20260420
*/
@Volatile
var appAttestationClient: AppAttestationClient? = null
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A crux change is creating a new object to encapsulate all the things for the new Salesforce "Challenge" API, the Integrity Token Provider, the Token and providing that in the "Attestation" format the auth and token refresh endpoints now expect. That's here.

Our tools had some great suggestion around making this property thread safe, so I added @volatile, the private setter and a dedicated lock object based on tool feedback.

@VisibleForTesting
internal set

/** Lock object for synchronized access to the app Attestation Client */
private val appAttestationClientLock = Any()

/**
* Updates the Salesforce App Attestation ECA Plugin Client for the selected
* login server and matching Google Cloud Project ID. When using App
* Attestation, this value must match the linked Google Cloud Project ID
* for the app in Google Play Console's Play Integrity API and provided to
* the Salesforce App Attestation External Client App Plugin.
*
* @param apiHostName The Salesforce App Attestation External Client App
* (ECA) Plugin Challenge API Host Name. This usually matches the selected
* login server
* @param googleCloudProjectId The Google Cloud Project ID or null to
* disable Salesforce App Attestation
*/
fun updateAppAttestationClient(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If one reads the description of this pull request, this is the entry point for an app to actually enable App Attestation.

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.

How is it supposed to work? Is the app supposed to register the googleCloudProjectId before first login? Why is it apiHostName specific?
If it needs to be dynamic and decided at runtime, should we use the approach we did for dynamic consumer key where the app can register a block/lambda to provide a googleCloudProjectId during a login flow once the login server is known?

Copy link
Copy Markdown
Contributor Author

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce May 5, 2026

Choose a reason for hiding this comment

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

Based on our conversation last Friday, I created follow-up W-22355537 to cover this. That will separate the Google Cloud Project Id into a new and optional Salesforce SDK Manager initialization parameter. When set, that will allow the manager to immediately begin preparing the Google Play Integrity API Token Provider warm up. The Salesforce Challenge API Hostname will be resolved by a new callback function which the app registers as a separate Salesforce SDK Manager property. That callback will be called with the resolved My Domain host after the well-known authentication configuration is fetched. Only when both are present will the attestation parameter be generated.

apiHostName: String,
googleCloudProjectId: Long? = null
) {
synchronized(appAttestationClientLock) {
appAttestationClient = googleCloudProjectId?.let { appAttestationGoogleCloudProjectId ->
AppAttestationClient(
context = appContext,
apiHostName = apiHostName,
deviceId = deviceId,
googleCloudProjectId = appAttestationGoogleCloudProjectId,
remoteAccessConsumerKey = getBootConfig(appContext).remoteAccessConsumerKey,
restClient = clientManager.peekUnauthenticatedRestClient()
)
}
}
}

/**
* ViewModel Factory the SDK will use in LoginActivity and composable functions. Setting this will allow for
* visual customization without overriding LoginActivity.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* Copyright (c) 2026-present, salesforce.com, inc.
* All rights reserved.
* Redistribution and use of this software in source and binary forms, with or
* without modification, are permitted provided that the following conditions
* are met:
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of salesforce.com, inc. nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission of salesforce.com, inc.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package com.salesforce.androidsdk.auth

import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.android.play.core.integrity.IntegrityManagerFactory.createStandard
import com.google.android.play.core.integrity.IntegrityServiceException
import com.google.android.play.core.integrity.StandardIntegrityManager
import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
import com.google.android.play.core.integrity.model.StandardIntegrityErrorCode.INTEGRITY_TOKEN_PROVIDER_INVALID
import com.salesforce.androidsdk.rest.AppAttestationChallengeApiClient
import com.salesforce.androidsdk.rest.RestClient
import com.salesforce.androidsdk.util.SalesforceSDKLogger.w
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.nio.charset.StandardCharsets.UTF_8
import java.security.MessageDigest
import java.util.Base64

/**
* App attestation features supporting the Salesforce App Attestation External
* Client App (ECA) Plugin, the Salesforce Challenge API, Google Play Integrity
* API and integration of app attestation with Salesforce Authentication.
*
* This method is not intended for public use outside of Salesforce Mobile SDK.
*
* TODO: Make this class internal once Java support is removed. ECJ20260421
*
* @param apiHostName The Salesforce App Attestation Challenge API host
* @param deviceId The device id, usually provided by the Salesforce SDK Manager
* @param googleCloudProjectId The Google Cloud Project ID used with Google Play
* Integrity API
* @param integrityManager The Google Play App Integrity API Integrity Manager.
* This parameter is intended for testing purposes only. Defaults to a new
* instance
* @param remoteAccessConsumerKey The Salesforce Connected App (CA) or External
* Client App (ECA)remote access consumer key, usually provided by the boot
* config
* @param restClient The REST client, usually provided by the Salesforce SDK
* Manager's unauthenticated REST client
*/
class AppAttestationClient(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This object is the heart 'n soul of App Attestation.

context: Context,
@property:VisibleForTesting
internal val apiHostName: String,
@property:VisibleForTesting
internal val deviceId: String,
@property:VisibleForTesting
internal val googleCloudProjectId: Long,
@property:VisibleForTesting
internal val integrityManager: StandardIntegrityManager = createStandard(context),
@property:VisibleForTesting
internal val remoteAccessConsumerKey: String,
@property:VisibleForTesting
internal val restClient: RestClient,
) {


/** The Google Play Integrity API Token Provider */
@VisibleForTesting
internal var integrityTokenProvider: StandardIntegrityTokenProvider? = null

init {
prepareIntegrityTokenProvider()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the "warm up" for Google Play Integrity API, as we often call it in internal discussion and docs.

}

/**
* (Re-)prepares the Google Play Integrity Token Provider. Calling this
* prior to requesting the Integrity Token via
* [createAppAttestation] reduces the latency of the request.
*/
@VisibleForTesting
internal fun prepareIntegrityTokenProvider() = integrityManager.prepareIntegrityToken(
PrepareIntegrityTokenRequest.builder()
.setCloudProjectNumber(googleCloudProjectId)
.build()
).addOnSuccessListener(
::onPrepareIntegrityTokenProviderSuccess
).addOnFailureListener(
::onPrepareIntegrityTokenProviderFailure
)

/**
* A success callback used by [prepareIntegrityTokenProvider].
* @param tokenProvider The Google Play API Integrity Token Provider
*/
@VisibleForTesting
internal fun onPrepareIntegrityTokenProviderSuccess(tokenProvider: StandardIntegrityTokenProvider) {
integrityTokenProvider = tokenProvider
}

/**
* A failure callback for [prepareIntegrityTokenProvider].
* @param exception The exception provided by Google Play Integrity API
*/
@VisibleForTesting
internal fun onPrepareIntegrityTokenProviderFailure(exception: Exception) {
w(javaClass.name, "Failed to prepare Google Play Integrity Token Provider: '${exception.message}'. App Attestation will be disabled.")
}

/**
* Creates a Salesforce App Attestation External Client App (ECA) Plugin
* "attestation". First a Salesforce Mobile App Attestation "Challenge" is
* requested for the device id. Then, a Google Play Integrity API Token is
* fetched using the "Challenge" as the Request Hash. The resulting token is
* encoded into a value usable as the "attestation" parameter in the
* Salesforce OAuth authorization request.
*
* This method is not intended for public use outside of Salesforce Mobile
* SDK.
*
* TODO: Make this Kotlin-internal once it is no longer referenced by Java. ECJ20260420
*
* @param appAttestationChallenge The Salesforce Mobile App Attestation
* External Client App (ECA) Plug-In "Challenge" to use
* @param integrityTokenProvider The Google Play App Integrity API Integrity
* Token Provider. This parameter is intended for testing purposes only
* @param canRetryOnInvalidTokenProvider When true (the default), a single
* inline retry with a freshly prepared Integrity Token Provider is allowed
* if the request fails with [INTEGRITY_TOKEN_PROVIDER_INVALID]. The
* recursive retry call sets this false to guarantee at most one retry
* and prevent unbounded recursion on the caller thread
* @return The "attestation" value usable in Salesforce OAuth authorization
* and token refresh requests or null if the value cannot be created
*/
suspend fun createAppAttestation(
appAttestationChallenge: String,
integrityTokenProvider: StandardIntegrityTokenProvider? = this.integrityTokenProvider,
canRetryOnInvalidTokenProvider: Boolean = true,
): String? {
// Guard to ensure the Google Play Integrity API Integrity Provider was asynchronously resolved or do so synchronously now.
val integrityTokenProviderResolved = integrityTokenProvider ?: prepareIntegrityTokenProvider().await()

// Fetch the Challenge from Salesforce Mobile App Attestation.
val salesforceAppAttestationChallengeHashByteArray = MessageDigest.getInstance("SHA-256")
.digest(appAttestationChallenge.toByteArray(UTF_8))
val salesforceAppAttestationChallengeHashHexString = salesforceAppAttestationChallengeHashByteArray.joinToString("") { "%02x".format(it) }

// Request the Google Play Integrity Token.
val integrityTokenResponse = integrityTokenProviderResolved.request(
StandardIntegrityTokenRequest.builder()
.setRequestHash(salesforceAppAttestationChallengeHashHexString)
.build()
)

/*
* Wait for the Google Play Integrity API response and return the
* Base64-encoded Salesforce OAuth authorization attestation parameter
* JSON. This may block the calling thread if the Google Play Integrity
* API introduces latency, though latency is expected to minimal as the
* API will have been prepared earlier in most scenarios.
*/
return runCatching {
integrityTokenResponse.await()

// When the Google Play Integrity API response is received, return the Base64-encoded Salesforce OAuth authorization attestation parameter JSON.
OAuthAuthorizationAttestation(
attestationId = deviceId,
attestationData = Base64.getEncoder().encodeToString(
integrityTokenResponse.getResult().token().encodeToByteArray()
)
).toBase64String()
}.getOrElse { e ->
// If the Google Play Integrity API failed due to the Integrity Token Provider being expired, re-prepare it once for an inline retry.
// The retry call passes canRetryOnInvalidTokenProvider = false to cap retries at one attempt and prevent unbounded recursion on the caller thread if the freshly prepared provider also reports INTEGRITY_TOKEN_PROVIDER_INVALID.
if (canRetryOnInvalidTokenProvider && (e as? IntegrityServiceException)?.errorCode == INTEGRITY_TOKEN_PROVIDER_INVALID) {
createAppAttestation(
appAttestationChallenge = appAttestationChallenge,
integrityTokenProvider = null,
canRetryOnInvalidTokenProvider = false,
)
} else {
null
}
}
}

/**
* A blocking Java-callable wrapper for [createAppAttestation]
*
* This method is not intended for public use outside of Salesforce Mobile
* SDK.
*
* TODO: Remove method when no longer referenced by Java. ECJ20260420
* @param appAttestationChallenge The Salesforce Mobile App Attestation
* External Client App (ECA) Plug-In "Challenge" to use
*/
fun createAppAttestationBlocking(appAttestationChallenge: String) = runBlocking {
createAppAttestation(appAttestationChallenge)
}

/**
* Fetches a new "Challenge" from the Salesforce App Attestation External
* Client App (ECA) Plug-In.
*
* This method is not intended for public use outside of Salesforce Mobile
* SDK.
*
* TODO: Make this Kotlin-internal once it is no longer referenced by Java. ECJ20260420
*
* @return The Salesforce App Attestation ECA Plug-In's "Challenge"
*/
fun fetchMobileAppAttestationChallenge(): String {
// Create the Salesforce App Attestation Challenge API client and fetch a new challenge.
val appAttestationChallengeApiClient = AppAttestationChallengeApiClient(
apiHostName = apiHostName,
restClient = restClient
)
return appAttestationChallengeApiClient.fetchChallenge(
attestationId = deviceId,
remoteConsumerKey = remoteAccessConsumerKey
)
}
}

/**
* A Salesforce OAuth 2.0 authorization "attestation" parameter.
* @param attestationId The attestation id used when creating the Salesforce
* Mobile App Attestation API Challenge. This is intended to be the
* Salesforce Mobile SDK device id
* @param attestationData The token provided by the Google Play Integrity API
*/
@Serializable
internal data class OAuthAuthorizationAttestation(
val attestationId: String,
val attestationData: String,
) {

/**
* Returns a Base64-encoded JSON representation of this object.
*
* Note: Standard Base64 alphabet with padding is used by design. The
* Salesforce App Attestation server-side contract requires the
* standard (not URL-safe) Base64 encoding with padding, and the value
* is consumed as-is without URI percent-encoding at the token endpoint
* (see OAuth2.makeTokenEndpointRequest). This has been verified
* end-to-end; do not switch to Base64.getUrlEncoder() or strip padding.
*/
fun toBase64String(): String? = Base64.getEncoder().encodeToString(Json.encodeToString(serializer(), this).encodeToByteArray())
}

Loading
Loading