Skip to content
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

Abstract Link Attestation Check #10103

Merged
merged 4 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading