2727package com.salesforce.androidsdk.auth
2828
2929import 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
3432import com.google.android.play.core.integrity.IntegrityManagerFactory.createStandard
33+ import com.google.android.play.core.integrity.StandardIntegrityManager
3534import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
3635import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenProvider
3736import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
3837import com.salesforce.androidsdk.rest.AppAttestationChallengeApiClient
3938import 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
4141import kotlinx.coroutines.runBlocking
4242import kotlinx.coroutines.tasks.await
4343import kotlinx.serialization.Serializable
@@ -47,11 +47,11 @@ import java.security.MessageDigest
4747import 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 */
6262class 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+
0 commit comments