-
Notifications
You must be signed in to change notification settings - Fork 393
@W-22355537: [MSDK Android] App Attestation Public API #2880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c52eefa
b3489e5
2842a8a
a56c6ec
cdf837d
6d061f5
0daa91d
506196a
78b1ccb
cfd9ac0
febface
d7fb7d5
4f9d02c
ddb33e3
1e7fbef
8ade6e6
76eede2
c9a2e8a
8fbe46b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,7 @@ import androidx.compose.runtime.Composable | |
| import androidx.core.content.ContextCompat.RECEIVER_EXPORTED | ||
| import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED | ||
| import androidx.core.content.ContextCompat.registerReceiver | ||
| import androidx.core.net.toUri | ||
| import androidx.lifecycle.DefaultLifecycleObserver | ||
| import androidx.lifecycle.LifecycleOwner | ||
| import androidx.lifecycle.ProcessLifecycleOwner | ||
|
|
@@ -97,6 +98,7 @@ import com.salesforce.androidsdk.auth.NativeLoginManager | |
| import com.salesforce.androidsdk.auth.OAuth2.LogoutReason | ||
| import com.salesforce.androidsdk.auth.OAuth2.LogoutReason.UNKNOWN | ||
| import com.salesforce.androidsdk.auth.OAuth2.revokeRefreshToken | ||
| import com.salesforce.androidsdk.auth.RemoteAccessConsumerKeyProvider | ||
| import com.salesforce.androidsdk.auth.idp.SPConfig | ||
| import com.salesforce.androidsdk.auth.idp.interfaces.IDPManager | ||
| import com.salesforce.androidsdk.auth.idp.interfaces.SPManager | ||
|
|
@@ -176,13 +178,17 @@ import com.salesforce.androidsdk.security.interfaces.ScreenLockManager as Screen | |
| * @param context The Android context | ||
| * @param mainActivity Activity that should be launched after the login flow | ||
| * @param loginActivity Login activity | ||
| * @param googleCloudProjectId The Google Cloud Project ID to use with | ||
| * Google Play Integrity API and Salesforce App Attestation or null to | ||
| * disable both features | ||
| */ | ||
| open class SalesforceSDKManager protected constructor( | ||
| @JvmField | ||
| protected val context: Context, | ||
| mainActivity: Class<out Activity>, | ||
| private val loginActivity: Class<out Activity>? = null, | ||
| internal val nativeLoginActivity: Class<out Activity>? = null, | ||
| googleCloudProjectId: Long? = null, | ||
| ) : DefaultLifecycleObserver { | ||
|
|
||
| constructor( | ||
|
|
@@ -229,50 +235,40 @@ open class SalesforceSDKManager protected constructor( | |
|
|
||
| /** | ||
| * The client side implementation of the Salesforce App Attestation External | ||
| * Client App (ECA) Plugin or null when app attestation is disabled. | ||
| * Client App (ECA) Plugin or null when Salesforce 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 | ||
| @VisibleForTesting | ||
| internal set | ||
|
|
||
| /** Lock object for synchronized access to the app Attestation Client */ | ||
| private val appAttestationClientLock = Any() | ||
| val appAttestationClient: AppAttestationClient? by lazy { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wmathurin - The |
||
| googleCloudProjectId?.let { createAppAttestationClient(it) } | ||
| } | ||
|
|
||
| /** | ||
| * 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. | ||
| * Creates the Salesforce App Attestation ECA Plugin Client for the selected | ||
| * Google Cloud Project ID. When using Salesforce 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( | ||
| apiHostName: String, | ||
| fun createAppAttestationClient( | ||
| 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() | ||
| ) | ||
| } | ||
| } | ||
| ) = googleCloudProjectId?.let { appAttestationGoogleCloudProjectId -> | ||
| AppAttestationClient( | ||
| context = appContext, | ||
| deviceId = deviceId, | ||
| googleCloudProjectId = appAttestationGoogleCloudProjectId, | ||
| remoteAccessConsumerKeyProvider = RemoteAccessConsumerKeyProvider { loginServer -> | ||
| resolveOAuthConfigForLoginServer(loginServer).consumerKey | ||
| }, | ||
| restClient = clientManager.peekUnauthenticatedRestClient() | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -294,6 +290,32 @@ open class SalesforceSDKManager protected constructor( | |
|
|
||
| internal var debugOverrideAppConfig: OAuthConfig? = null | ||
|
|
||
| /** | ||
| * Resolves the OAuth configuration for the specified login server. | ||
| * | ||
| * Resolution order: | ||
| * 1. Debug override configuration (when [isDebugBuild] is true and override | ||
| * is set) | ||
| * 2. Dynamic app configuration for the login host via | ||
| * [appConfigForLoginHost] | ||
| * 3. Static boot configuration from bootconfig.xml | ||
| * | ||
| * This allows apps to use different OAuth configs per server while | ||
| * supporting debug overrides for development/testing. | ||
| * | ||
| * @param loginServer The login server URL | ||
| * @return The OAuth configuration for the specified server | ||
| */ | ||
| internal suspend fun resolveOAuthConfigForLoginServer( | ||
| loginServer: String | ||
| ): OAuthConfig { | ||
| val debugOverride = debugOverrideAppConfig | ||
| return when { | ||
| isDebugBuild && debugOverride != null -> debugOverride | ||
| else -> appConfigForLoginHost(loginServer) ?: OAuthConfig(getBootConfig(appContext)) | ||
| } | ||
| } | ||
|
|
||
| /** The class for the account switcher activity */ | ||
| var accountSwitcherActivityClass = AccountSwitcherActivity::class.java | ||
|
|
||
|
|
@@ -1528,7 +1550,7 @@ open class SalesforceSDKManager protected constructor( | |
| } | ||
|
|
||
| /** Indicates if this is a debug build */ | ||
| internal val isDebugBuild | ||
| internal open val isDebugBuild | ||
| get() = DEBUG | ||
|
|
||
|
|
||
|
|
@@ -1717,19 +1739,24 @@ open class SalesforceSDKManager protected constructor( | |
| * @param context The Android context | ||
| * @param mainActivity The app's main activity class | ||
| * @param loginActivity The app login activity class | ||
| * @param googleCloudProjectId The Google Cloud Project ID to use with | ||
| * Google Play Integrity API and Salesforce App Attestation or null to | ||
| * disable both features | ||
| */ | ||
| private fun init( | ||
| context: Context, | ||
| mainActivity: Class<out Activity>, | ||
| loginActivity: Class<out Activity>? = null, | ||
| nativeLoginActivity: Class<out Activity>? = null, | ||
| googleCloudProjectId: Long? = null, | ||
| ) { | ||
| if (INSTANCE == null) { | ||
| INSTANCE = SalesforceSDKManager( | ||
| context, | ||
| mainActivity, | ||
| loginActivity, | ||
| nativeLoginActivity, | ||
| googleCloudProjectId, | ||
| ) | ||
| } | ||
| initInternal(context) | ||
|
|
@@ -1958,6 +1985,9 @@ open class SalesforceSDKManager protected constructor( | |
| shareBrowserSessionEnabled = false | ||
| ) | ||
|
|
||
| // Disable Salesforce App Attestation for login servers that are not My Domain servers. | ||
| appAttestationClient?.apiHostName = null | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @wmathurin - I went without the app-provided callback to determine the Challenge API Host. Here, we seem to know that we're not using a My Domain and that means App Attestation will be disabled. Is that correct?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I'm still hoping we can move to attesting on code exchange only - but that will only fly if user agent flow can be blocked for a given ECA which is not possible today --- stay tuned). |
||
|
|
||
| return@withTimeoutOrNull | ||
| } | ||
|
|
||
|
|
@@ -1966,6 +1996,9 @@ open class SalesforceSDKManager protected constructor( | |
| browserLoginEnabled = authConfig?.isBrowserLoginEnabled ?: false, | ||
| shareBrowserSessionEnabled = authConfig?.isShareBrowserSessionEnabled ?: false | ||
| ) | ||
|
|
||
| // Consider enabling Salesforce App Attestation for login servers that are My Domain servers. | ||
| appAttestationClient?.apiHostName = loginServer.toUri().host | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the comment a few lines before, here we seem to know we are using a My Domain host and enable App Attestation. How's that look? |
||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,7 +115,7 @@ internal class IDPAuthCodeHelper @VisibleForTesting internal constructor( | |
|
|
||
| // Add Salesforce Mobile App Attestation parameter to authorization URL if applicable. | ||
| val additionalParams = appAttestationClient?.run { | ||
| val challenge = fetchMobileAppAttestationChallenge() | ||
| val challenge = fetchMobileAppAttestationChallenge() ?: return@run null | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's convenient I'd previously separated the challenge and attestation fetch at the call site, so here it's just a second guard when the Challenge Host is not set. |
||
| val attestation = createAppAttestation(challenge) ?: return@run null | ||
| mapOf(ATTESTATION to attestation) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wmathurin, here's the relocated
googleCloudProjectIdin theSalesforceSDKManagerconstructor that subclasses use. I noticed that ourRestExplorerAppsample subclasses, so this gives that app a convenient way to pass this value.Are there other constructor use cases we'd need to expose? I've yet to do a complete inventory of all our samples and templates to see what they are up to and wanted to get your feedback early.