Skip to content

Commit e5052ce

Browse files
@W-21933885: [MSDK Android] App Attestation Implementation (Replace Android Jetpack Data Store Use, Initialize Unit Tests And Code Coverage)
1 parent 445cd54 commit e5052ce

7 files changed

Lines changed: 369 additions & 166 deletions

File tree

libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,37 @@ open class SalesforceSDKManager protected constructor(
227227
*/
228228
val loginActivityClass: Class<out Activity> = nativeLoginActivity ?: webViewLoginActivityClass
229229

230+
/**
231+
* The Google Cloud Project ID used to for the client implementation of the
232+
* Salesforce App Attestation External Client App Plugin. When using App
233+
* Attestation, this value must match the linked Google Cloud Project ID
234+
* for the app in Google Play Console's Play Integrity API and provided to
235+
* the Salesforce App Attestation External Client App Plugin.
236+
*
237+
* When null, App Attestation and Google Play Integrity will be ignored by
238+
* the Salesforce Mobile SDK.
239+
*/
240+
var appAttestationGoogleCloudProjectId: Long? = null
241+
set(value) {
242+
field = value
243+
244+
appAttestationClient = field?.let { appAttestationGoogleCloudProjectId ->
245+
AppAttestationClient(
246+
appContext,
247+
deviceId,
248+
appAttestationGoogleCloudProjectId,
249+
getBootConfig(getInstance().appContext).remoteAccessConsumerKey,
250+
clientManager.peekUnauthenticatedRestClient()
251+
)
252+
}
253+
}
254+
255+
/**
256+
* The client side implementation of the Salesforce App Attestation External
257+
* Client App Plugin or null with app attestation is disabled.
258+
*/
259+
var appAttestationClient: AppAttestationClient? = null
260+
230261
/**
231262
* ViewModel Factory the SDK will use in LoginActivity and composable functions. Setting this will allow for
232263
* visual customization without overriding LoginActivity.
@@ -684,18 +715,6 @@ open class SalesforceSDKManager protected constructor(
684715
)
685716
}
686717

687-
/**
688-
* Creates a client for use with the Salesforce App Attestation External
689-
* Client App (ECA) Plugin
690-
* @return The app authentication attestation client
691-
*/
692-
fun createAppAttestationClient() = AppAttestationClient(
693-
appContext,
694-
deviceId,
695-
getBootConfig(getInstance().appContext).remoteAccessConsumerKey,
696-
clientManager.peekUnauthenticatedRestClient()
697-
)
698-
699718
/**
700719
* Optionally enables browser based login instead of web view login.
701720
*

libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AppAttestationClient.kt

Lines changed: 101 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@
2727
package com.salesforce.androidsdk.auth
2828

2929
import android.content.Context
30-
import android.util.Log
31-
import androidx.datastore.core.DataStore
32-
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
33-
import androidx.datastore.dataStore
30+
import androidx.annotation.VisibleForTesting
31+
import com.google.android.gms.tasks.Task
3432
import com.google.android.play.core.integrity.IntegrityManagerFactory.createStandard
33+
import com.google.android.play.core.integrity.StandardIntegrityManager
3534
import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
3635
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
3736
import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
3837
import com.salesforce.androidsdk.rest.AppAttestationChallengeApiClient
3938
import com.salesforce.androidsdk.rest.RestClient
40-
import kotlinx.coroutines.flow.map
39+
import com.salesforce.androidsdk.util.SalesforceSDKLogger.e
40+
import com.salesforce.androidsdk.util.SalesforceSDKLogger.w
4141
import kotlinx.coroutines.runBlocking
4242
import kotlinx.coroutines.tasks.await
4343
import kotlinx.serialization.Serializable
@@ -47,11 +47,11 @@ import java.security.MessageDigest
4747
import java.util.Base64
4848

4949
/**
50-
* Authentication app attestation features supporting the Salesforce App
51-
* Attestation External Client App (ECA) Plugin, the Salesforce Challenge API,
52-
* Google Play Integrity API and integration of app attestation with Salesforce
53-
* Authentication.
54-
* @param context The Android context
50+
* App attestation features supporting the Salesforce App Attestation External
51+
* Client App (ECA) Plugin, the Salesforce Challenge API, Google Play Integrity
52+
* API and integration of app attestation with Salesforce Authentication.
53+
* @param googleCloudProjectId The Google Cloud Project ID used with Google Play
54+
* Integrity API
5555
* @param deviceId The device id, usually provided by the Salesforce SDK Manager
5656
* @param remoteAccessConsumerKey The Salesforce Connected App (CA) or External
5757
* Client App (ECA)remote access consumer key, usually provided by the boot
@@ -60,63 +60,69 @@ import java.util.Base64
6060
* Manager's unauthenticated REST client
6161
*/
6262
class AppAttestationClient(
63-
val context: Context,
63+
context: Context,
6464
val deviceId: String,
65+
val googleCloudProjectId: Long,
6566
val remoteAccessConsumerKey: String,
6667
val restClient: RestClient
6768
) {
6869

69-
/** The data store for authentication attestation data */
70-
val Context.dataStore: DataStore<AuthenticationAttestationData> by dataStore(
71-
serializer = AuthenticationAttestationDataSerializer,
72-
fileName = "${context.filesDir.path}/authentication_attestation_data_store.json",
73-
corruptionHandler = ReplaceFileCorruptionHandler { AuthenticationAttestationData(googleCloudProjectId = null) }
74-
)
70+
/** The Google Play Integrity Manager and Token Provider */
71+
private val integrityManager = createStandard(context)
7572

76-
/** The Google Play Integrity API Token Provider */
77-
private var integrityTokenProvider: StandardIntegrityTokenProvider? = null
78-
79-
/** The flow of authentication attestation data */
80-
fun googleCloudProjectIdFlow() = context.dataStore.data.map { authenticationAttestationData ->
81-
authenticationAttestationData.googleCloudProjectId
82-
}
83-
84-
/** Sets the Google Cloud Project ID */
85-
suspend fun setGoogleCloudProjectId(googleCloudProjectId: Long) {
86-
context.dataStore.updateData { authenticationAttestationData ->
87-
authenticationAttestationData.copy(googleCloudProjectId = googleCloudProjectId)
88-
}
89-
}
9073

91-
/** Prepares authorization app attestation for use */
92-
suspend fun prepare() {
93-
googleCloudProjectIdFlow().collect { googleCloudProjectId ->
94-
onGoogleCloudProjectIdCollected(googleCloudProjectId ?: return@collect)
95-
}
96-
}
74+
/** The Google Play Integrity API Token Provider */
75+
@VisibleForTesting
76+
internal var integrityTokenProvider: StandardIntegrityTokenProvider? = null
9777

9878
/**
99-
* Prepares for authorization and authorization token refresh using app
79+
* Prepares for authorization and authorization token refresh with app
10080
* attestation using the Salesforce Mobile App Attestation Challenge API
10181
* and Google Play Integrity API.
10282
* @param googleCloudProjectId The Google Cloud Project ID
10383
*/
104-
private fun onGoogleCloudProjectIdCollected(googleCloudProjectId: Long) {
84+
init {
85+
prepareIntegrityTokenProvider()
86+
}
10587

106-
// Create the Google Play Integrity Manager and Token Provider.
107-
val integrityManager = createStandard(context)
88+
/**
89+
* (Re-)prepares the Google Play Integrity token provider.
90+
* @param integrityManager The Google Play Integrity API integrity manager.
91+
* This parameter is intended for testing purposes only
92+
*/
93+
@VisibleForTesting
94+
internal fun prepareIntegrityTokenProvider(
95+
integrityManager: StandardIntegrityManager = this.integrityManager
96+
): Task<StandardIntegrityTokenProvider> {
10897

10998
// Prepare the Google Play Integrity token. Calling this prior to requesting the Integrity Token reduces the latency of the request.
110-
integrityManager.prepareIntegrityToken(
99+
return integrityManager.prepareIntegrityToken(
111100
PrepareIntegrityTokenRequest.builder()
112101
.setCloudProjectNumber(googleCloudProjectId)
113102
.build()
114-
).addOnSuccessListener { tokenProvider ->
115-
integrityTokenProvider = tokenProvider
116-
Log.i("AppAttestation", "Prepared Google Play Integrity Token Provider: '${tokenProvider}'.")
117-
}.addOnFailureListener { exception ->
118-
Log.e("AppAttestation", "Failed to prepare Google Play Integrity Token Provider: '${exception.message}'.")
119-
}
103+
).addOnSuccessListener(
104+
::onPrepareIntegrityTokenProviderSuccess
105+
).addOnFailureListener(
106+
::onPrepareIntegrityTokenProviderFailure
107+
)
108+
}
109+
110+
/**
111+
* A success callback used by [prepareIntegrityTokenProvider].
112+
* @param tokenProvider The Google Play API Integrity Token Provider
113+
*/
114+
@VisibleForTesting
115+
internal fun onPrepareIntegrityTokenProviderSuccess(tokenProvider: StandardIntegrityTokenProvider) {
116+
integrityTokenProvider = tokenProvider
117+
}
118+
119+
/**
120+
* A failure callback for [prepareIntegrityTokenProvider].
121+
* @param exception The exception provided by Google Play Integrity API
122+
*/
123+
@VisibleForTesting
124+
internal fun onPrepareIntegrityTokenProviderFailure(exception: Exception) {
125+
w(javaClass.name, "Failed to prepare Google Play Integrity Token Provider: '${exception.message}'. App Attestation will be disabled.")
120126
}
121127

122128
/**
@@ -126,12 +132,19 @@ class AppAttestationClient(
126132
* fetched using the "Challenge" as the Request Hash. The resulting token is
127133
* encoded into a value usable as the "attestation" parameter in the
128134
* Salesforce OAuth authorization request.
135+
* @param integrityManager The Google Play Integrity API integrity manager.
136+
* This parameter is intended for testing purposes only
137+
* @param integrityTokenProvider The Google Play App Integrity API Integrity
138+
* Token Provider. This parameter is intended for testing purposes only
129139
* @return The "attestation" value usable in Salesforce OAuth authorization
130140
* and token refresh requests
131141
*/
132-
public fun createSalesforceOAuthAuthorizationAppAttestation(): String? {
133-
// Guards.
134-
val integrityTokenProvider = integrityTokenProvider ?: return ""
142+
suspend fun createSalesforceOAuthAuthorizationAppAttestation(
143+
integrityManager: StandardIntegrityManager = this.integrityManager,
144+
integrityTokenProvider: StandardIntegrityTokenProvider? = this.integrityTokenProvider,
145+
): String? {
146+
// Guard to ensure the Google Play Integrity API Integrity Provider was asynchronously resolved or do so synchronously now
147+
val integrityTokenProviderResolved = integrityTokenProvider ?: prepareIntegrityTokenProvider(integrityManager).result
135148

136149
// Fetch the Salesforce Mobile App Attestation Challenge.
137150
val salesforceAppAttestationChallenge = fetchSalesforceMobileAppAttestationChallenge()
@@ -140,25 +153,25 @@ class AppAttestationClient(
140153
val salesforceAppAttestationChallengeHashHexString = salesforceAppAttestationChallengeHashByteArray.joinToString("") { "%02x".format(it) }
141154

142155
// Request the Google Play Integrity Token.
143-
val integrityTokenResponse = integrityTokenProvider.request(
156+
val integrityTokenResponse = integrityTokenProviderResolved.request(
144157
StandardIntegrityTokenRequest.builder()
145158
.setRequestHash(salesforceAppAttestationChallengeHashHexString)
146159
.build()
147160
)
148-
val googlePlayIntegrityTask = integrityTokenResponse.addOnSuccessListener { response ->
149-
Log.i("AppAttestation", "Received Google Play Integrity Token: '${response.token()}'.")
150-
151-
}.addOnFailureListener { exception ->
152-
// If the app uses the same token provider for too long, the token provider can expire which results in the INTEGRITY_TOKEN_PROVIDER_INVALID error on the next token request. You should handle this error by requesting a new provider.
153-
Log.e("AppAttestation", "Failed To Receive Google Play Integrity Token: Message: '${exception.message}'.")
154-
155-
// TODO: Handle the error by requesting a new Google Play Integrity Token Provider. ECJ20260311
161+
val googlePlayIntegrityTask = integrityTokenResponse.addOnFailureListener { exception ->
162+
e(javaClass.name, "Failed To Receive Google Play Integrity Token: Message: '${exception.message}'.")
163+
// Synchronously re-prepare the Google Play Integrity Token Provider to provide a result to the caller.
164+
runBlocking { prepareIntegrityTokenProvider().await() /* TODO: Make this retry the request. ECJ20260414 */ }
156165
}
157166

158-
// Wait for the Google Play Integrity API response and return the Base64-encoded Salesforce OAuth authorization attestation parameter JSON.
159-
runBlocking {
160-
googlePlayIntegrityTask.await()
161-
}
167+
/*
168+
* Wait for the Google Play Integrity API response and return the
169+
* Base64-encoded Salesforce OAuth authorization attestation parameter
170+
* JSON. This may block the calling thread if the Google Play Integrity
171+
* API introduces latency, though latency is expected to minimal as the
172+
* API will have been prepared earlier in most scenarios.
173+
*/
174+
googlePlayIntegrityTask.await()
162175
return OAuthAuthorizationAttestation(
163176
attestationId = deviceId,
164177
attestationData = Base64.getEncoder().encodeToString(
@@ -167,6 +180,13 @@ class AppAttestationClient(
167180
).toBase64String()
168181
}
169182

183+
/**
184+
* A blocking Java-callable wrapper for
185+
* [createSalesforceOAuthAuthorizationAppAttestation]
186+
*/
187+
@JvmName("createSalesforceOAuthAuthorizationAppAttestationBlocking")
188+
fun createSalesforceOAuthAuthorizationAppAttestationBlocking() = runBlocking { createSalesforceOAuthAuthorizationAppAttestation() }
189+
170190
/**
171191
* Fetches a new "Challenge" from the Salesforce App Attestation External
172192
* Client App (ECA) Plug-In.
@@ -185,23 +205,24 @@ class AppAttestationClient(
185205

186206
return salesforceAppAttestationChallenge
187207
}
208+
}
209+
210+
/**
211+
* A Salesforce OAuth 2.0 authorization "attestation" parameter.
212+
* @param attestationId The attestation id used when creating the Salesforce
213+
* Mobile App Attestation API Challenge. This is intended to be the
214+
* Salesforce Mobile SDK device id
215+
* @param attestationData The token provided by the Google Play Integrity API
216+
*/
217+
@Serializable
218+
internal data class OAuthAuthorizationAttestation(
219+
val attestationId: String,
220+
val attestationData: String,
221+
) {
188222

189223
/**
190-
* A Salesforce OAuth 2.0 authorization "attestation" parameter.
191-
* @param attestationId The attestation id used when creating the Salesforce
192-
* Mobile App Attestation API Challenge. This is intended to be the
193-
* Salesforce Mobile SDK device id
194-
* @param attestationData The token provided by the Google Play Integrity API
224+
* Returns a Base64-encoded JSON representation of this object
195225
*/
196-
@Serializable
197-
internal data class OAuthAuthorizationAttestation(
198-
val attestationId: String,
199-
val attestationData: String,
200-
) {
201-
202-
/**
203-
* Returns a Base64-encoded JSON representation of this object
204-
*/
205-
fun toBase64String(): String? = Base64.getEncoder().encodeToString(Json.encodeToString(serializer(), this).encodeToByteArray())
206-
}
226+
fun toBase64String(): String? = Base64.getEncoder().encodeToString(Json.encodeToString(serializer(), this).encodeToByteArray())
207227
}
228+

libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/AuthenticationAttestationData.kt

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)