Skip to content

Commit

Permalink
Abstract Link Attestation Check (#10103)
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe authored Feb 10, 2025
1 parent 129c556 commit 257b869
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 315 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ interface ErrorReporter : FraudDetectionErrorReporter {
LINK_LOG_OUT_FAILURE(
eventName = "link.log_out.failure"
),
LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER(
eventName = "link.native.integrity.preparation_failed"
),
PAYMENT_LAUNCHER_CONFIRMATION_NULL_ARGS(
eventName = "payments.paymentlauncherconfirmation.null_args"
),
Expand Down Expand Up @@ -195,9 +198,6 @@ interface ErrorReporter : FraudDetectionErrorReporter {
LINK_WEB_FAILED_TO_PARSE_RESULT_URI(
partialEventName = "link.web.result.parsing_failed"
),
LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER(
partialEventName = "link.native.integrity.preparation_failed"
),
LINK_NATIVE_FAILED_TO_ATTEST_REQUEST(
partialEventName = "link.native.failed_to_attest_request"
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,16 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.navigation.NavHostController
import com.stripe.android.link.LinkActivity.Companion.getArgs
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.account.LinkAuth
import com.stripe.android.link.account.LinkAuthResult
import com.stripe.android.link.account.linkAccountUpdate
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.attestation.LinkAttestationCheck
import com.stripe.android.link.injection.DaggerNativeLinkComponent
import com.stripe.android.link.injection.NativeLinkComponent
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.link.model.LinkAccount
import com.stripe.android.link.ui.LinkAppBarState
import com.stripe.android.model.EmailSource
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.R
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.attestation.IntegrityRequestManager
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -45,11 +40,8 @@ internal class LinkActivityViewModel @Inject constructor(
confirmationHandlerFactory: ConfirmationHandler.Factory,
private val linkAccountManager: LinkAccountManager,
val eventReporter: EventReporter,
private val integrityRequestManager: IntegrityRequestManager,
private val linkGate: LinkGate,
private val errorReporter: ErrorReporter,
private val linkAuth: LinkAuth,
private val linkConfiguration: LinkConfiguration,
private val linkAttestationCheck: LinkAttestationCheck,
private val startWithVerificationDialog: Boolean
) : ViewModel(), DefaultLifecycleObserver {
val confirmationHandler = confirmationHandlerFactory.create(viewModelScope)
Expand Down Expand Up @@ -192,14 +184,19 @@ internal class LinkActivityViewModel @Inject constructor(
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
viewModelScope.launch {
performAttestationCheck().fold(
onSuccess = {
updateScreenState()
},
onFailure = {
val attestationCheckResult = linkAttestationCheck.invoke()
when (attestationCheckResult) {
is LinkAttestationCheck.Result.AttestationFailed -> {
moveToWeb()
}
)
LinkAttestationCheck.Result.Successful -> {
updateScreenState()
}
is LinkAttestationCheck.Result.AccountError,
is LinkAttestationCheck.Result.Error -> {
// Display error screen here like web
}
}
}
}

Expand All @@ -209,33 +206,6 @@ internal class LinkActivityViewModel @Inject constructor(
}
}

private suspend fun performAttestationCheck(): Result<Unit> {
if (linkGate.useAttestationEndpoints.not()) return Result.success(Unit)
return integrityRequestManager.prepare()
.onFailure { error ->
errorReporter.report(
errorEvent = ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER,
stripeException = LinkEventException(error)
)
}
.mapCatching {
when (val lookupResult = lookupUser()) {
is LinkAuthResult.AttestationFailed -> {
errorReporter.report(
errorEvent = ErrorReporter.UnexpectedErrorEvent.LINK_NATIVE_FAILED_TO_ATTEST_REQUEST,
stripeException = LinkEventException(lookupResult.error)
)
throw lookupResult.error
}
is LinkAuthResult.Error,
is LinkAuthResult.AccountError,
LinkAuthResult.NoLinkAccountFound,
is LinkAuthResult.Success,
null -> Unit
}
}
}

private suspend fun updateScreenState() {
val accountStatus = linkAccountManager.accountStatus.first()
val linkAccount = linkAccountManager.linkAccount.value
Expand Down Expand Up @@ -272,18 +242,6 @@ internal class LinkActivityViewModel @Inject constructor(
navigate(screen, clearStack = true, launchSingleTop = true)
}

private suspend fun lookupUser(): LinkAuthResult? {
val customerEmail = linkAccountManager.linkAccount.value?.email
?: linkConfiguration.customerInfo.email
?: return null

return linkAuth.lookUp(
email = customerEmail,
emailSource = EmailSource.CUSTOMER_OBJECT,
startSession = false
)
}

companion object {
private val showHeaderRoutes = setOf(
LinkScreen.Wallet.route,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.stripe.android.link.attestation

import com.stripe.android.core.exception.StripeException
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.account.LinkAuth
import com.stripe.android.link.account.LinkAuthResult
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.model.EmailSource
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.attestation.IntegrityRequestManager
import javax.inject.Inject

internal class DefaultLinkAttestationCheck @Inject constructor(
private val linkGate: LinkGate,
private val linkAuth: LinkAuth,
private val integrityRequestManager: IntegrityRequestManager,
private val linkAccountManager: LinkAccountManager,
private val linkConfiguration: LinkConfiguration,
private val errorReporter: ErrorReporter
) : LinkAttestationCheck {
override suspend fun invoke(): LinkAttestationCheck.Result {
if (linkGate.useAttestationEndpoints.not()) return LinkAttestationCheck.Result.Successful
val result = integrityRequestManager.prepare()

return result.fold(
onSuccess = {
val email = linkAccountManager.linkAccount.value?.email
?: linkConfiguration.customerInfo.email
if (email == null) return@fold LinkAttestationCheck.Result.Successful
val lookupResult = linkAuth.lookUp(
email = email,
emailSource = EmailSource.CUSTOMER_OBJECT,
startSession = false
)
when (lookupResult) {
is LinkAuthResult.AttestationFailed -> {
LinkAttestationCheck.Result.AttestationFailed(lookupResult.error)
}
is LinkAuthResult.Error -> {
LinkAttestationCheck.Result.Error(lookupResult.error)
}
is LinkAuthResult.AccountError -> {
LinkAttestationCheck.Result.AccountError(lookupResult.error)
}
LinkAuthResult.NoLinkAccountFound,
is LinkAuthResult.Success -> {
LinkAttestationCheck.Result.Successful
}
}
},
onFailure = { error ->
errorReporter.report(
errorEvent = ErrorReporter.ExpectedErrorEvent.LINK_NATIVE_FAILED_TO_PREPARE_INTEGRITY_MANAGER,
stripeException = StripeException.create(error)
)
LinkAttestationCheck.Result.AttestationFailed(error)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.stripe.android.link.attestation

internal interface LinkAttestationCheck {
suspend fun invoke(): Result

sealed interface Result {
data object Successful : Result
data class AttestationFailed(val error: Throwable) : Result
data class AccountError(val error: Throwable) : Result
data class Error(val error: Throwable) : Result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.stripe.android.link.injection

import android.app.Application
import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
internal object ApplicationIdModule {
@Provides
@Named(APPLICATION_ID)
fun provideApplicationId(
application: Application
): String {
return application.packageName
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stripe.android.link.injection

import android.app.Application
import com.stripe.android.BuildConfig
import com.stripe.android.core.Logger
import com.stripe.attestation.IntegrityRequestManager
import com.stripe.attestation.IntegrityStandardRequestManager
import com.stripe.attestation.RealStandardIntegrityManagerFactory
import dagger.Module
import dagger.Provides

@Module
internal object IntegrityRequestManagerModule {
@Provides
fun providesIntegrityStandardRequestManager(
context: Application
): IntegrityRequestManager = IntegrityStandardRequestManager(
cloudProjectNumber = 577365562050, // stripe-payments-sdk-prod
logError = { message, error ->
Logger.getInstance(BuildConfig.DEBUG).error(message, error)
},
factory = RealStandardIntegrityManagerFactory(context)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ package com.stripe.android.link.injection
import com.stripe.android.link.LinkActivityViewModel
import com.stripe.android.link.LinkConfiguration
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.account.LinkAuth
import com.stripe.android.link.gate.LinkGate
import com.stripe.android.link.attestation.LinkAttestationCheck
import com.stripe.android.paymentelement.confirmation.DefaultConfirmationHandler
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.attestation.IntegrityRequestManager
import dagger.Module
import dagger.Provides
import javax.inject.Named
Expand All @@ -22,23 +19,17 @@ internal object LinkViewModelModule {
defaultConfirmationHandlerFactory: DefaultConfirmationHandler.Factory,
linkAccountManager: LinkAccountManager,
eventReporter: EventReporter,
integrityRequestManager: IntegrityRequestManager,
linkGate: LinkGate,
errorReporter: ErrorReporter,
linkAuth: LinkAuth,
linkConfiguration: LinkConfiguration,
linkAttestationCheck: LinkAttestationCheck,
@Named(START_WITH_VERIFICATION_DIALOG) startWithVerificationDialog: Boolean
): LinkActivityViewModel {
return LinkActivityViewModel(
activityRetainedComponent = component,
confirmationHandlerFactory = defaultConfirmationHandlerFactory,
linkAccountManager = linkAccountManager,
eventReporter = eventReporter,
integrityRequestManager = integrityRequestManager,
linkGate = linkGate,
errorReporter = errorReporter,
linkAuth = linkAuth,
linkConfiguration = linkConfiguration,
linkAttestationCheck = linkAttestationCheck,
startWithVerificationDialog = startWithVerificationDialog
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ internal annotation class NativeLinkScope
modules = [
NativeLinkModule::class,
LinkViewModelModule::class,
IntegrityRequestManagerModule::class,
ApplicationIdModule::class,
DefaultConfirmationModule::class,
]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.stripe.android.link.injection

import android.app.Application
import android.content.Context
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.SavedStateHandle
Expand Down Expand Up @@ -29,6 +28,8 @@ import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.account.LinkAuth
import com.stripe.android.link.analytics.DefaultLinkEventsReporter
import com.stripe.android.link.analytics.LinkEventsReporter
import com.stripe.android.link.attestation.DefaultLinkAttestationCheck
import com.stripe.android.link.attestation.LinkAttestationCheck
import com.stripe.android.link.confirmation.DefaultLinkConfirmationHandler
import com.stripe.android.link.confirmation.LinkConfirmationHandler
import com.stripe.android.link.gate.DefaultLinkGate
Expand All @@ -46,9 +47,6 @@ import com.stripe.android.paymentsheet.analytics.DefaultEventReporter
import com.stripe.android.paymentsheet.analytics.EventReporter
import com.stripe.android.repository.ConsumersApiService
import com.stripe.android.repository.ConsumersApiServiceImpl
import com.stripe.attestation.IntegrityRequestManager
import com.stripe.attestation.IntegrityStandardRequestManager
import com.stripe.attestation.RealStandardIntegrityManagerFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -96,6 +94,10 @@ internal interface NativeLinkModule {
@NativeLinkScope
fun bindsLinkAuth(linkGate: DefaultLinkAuth): LinkAuth

@Binds
@NativeLinkScope
fun bindsLinkAttestationCheck(linkAttestationCheck: DefaultLinkAttestationCheck): LinkAttestationCheck

@SuppressWarnings("TooManyFunctions")
companion object {
@Provides
Expand Down Expand Up @@ -191,29 +193,8 @@ internal interface NativeLinkModule {
factory: DefaultLinkConfirmationHandler.Factory
): LinkConfirmationHandler.Factory = factory

@Provides
@NativeLinkScope
fun providesIntegrityStandardRequestManager(
context: Application
): IntegrityRequestManager = IntegrityStandardRequestManager(
cloudProjectNumber = 577365562050, // stripe-payments-sdk-prod
logError = { message, error ->
Logger.getInstance(BuildConfig.DEBUG).error(message, error)
},
factory = RealStandardIntegrityManagerFactory(context)
)

@Provides
@NativeLinkScope
fun provideEventReporterMode(): EventReporter.Mode = EventReporter.Mode.Custom

@Provides
@NativeLinkScope
@Named(APPLICATION_ID)
fun provideApplicationId(
application: Application
): String {
return application.packageName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@ import androidx.test.espresso.intent.Intents.assertNoUnverifiedIntents
import androidx.test.espresso.intent.rule.IntentsRule
import com.google.common.truth.Truth.assertThat
import com.stripe.android.link.account.FakeLinkAccountManager
import com.stripe.android.link.account.FakeLinkAuth
import com.stripe.android.link.account.LinkAccountManager
import com.stripe.android.link.gate.FakeLinkGate
import com.stripe.android.link.attestation.FakeLinkAttestationCheck
import com.stripe.android.link.model.AccountStatus
import com.stripe.android.paymentelement.confirmation.FakeConfirmationHandler
import com.stripe.android.paymentsheet.analytics.FakeEventReporter
import com.stripe.android.testing.CoroutineTestRule
import com.stripe.android.testing.FakeErrorReporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
Expand Down Expand Up @@ -146,10 +144,7 @@ internal class LinkActivityTest {
confirmationHandlerFactory = { FakeConfirmationHandler() },
linkAccountManager = linkAccountManager,
eventReporter = FakeEventReporter(),
integrityRequestManager = FakeIntegrityRequestManager(),
linkGate = FakeLinkGate(),
errorReporter = FakeErrorReporter(),
linkAuth = FakeLinkAuth(),
linkAttestationCheck = FakeLinkAttestationCheck(),
linkConfiguration = TestFactory.LINK_CONFIGURATION,
startWithVerificationDialog = use2faDialog,
)
Expand Down
Loading

0 comments on commit 257b869

Please sign in to comment.