Update the CreateAuthRequests API to poll for updates (#884)

This commit is contained in:
David Perez
2024-01-30 22:04:46 -06:00
committed by Álison Fernandes
parent 1794223d02
commit d6c2969332
5 changed files with 417 additions and 0 deletions

View File

@@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
@@ -209,6 +210,11 @@ interface AuthRepository : AuthenticatorProvider {
*/
suspend fun createAuthRequest(email: String): AuthRequestResult
/**
* Creates a new authentication request and then continues to emit updates over time.
*/
fun createAuthRequestWithUpdates(email: String): Flow<CreateAuthRequestResult>
/**
* Get an auth request by its [fingerprint].
*/

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.auth.repository
import android.os.SystemClock
import com.bitwarden.core.AuthRequestResponse
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
@@ -34,6 +35,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
import com.x8bit.bitwarden.data.auth.repository.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
@@ -74,6 +76,8 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -81,18 +85,25 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.isActive
import java.time.Clock
import javax.inject.Singleton
private const val PASSWORDLESS_NOTIFICATION_TIMEOUT_MILLIS: Long = 15L * 60L * 1_000L
private const val PASSWORDLESS_NOTIFICATION_RETRY_INTERVAL_MILLIS: Long = 4L * 1_000L
/**
* Default implementation of [AuthRepository].
*/
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@Singleton
class AuthRepositoryImpl(
private val clock: Clock,
private val accountsService: AccountsService,
private val authRequestsService: AuthRequestsService,
private val devicesService: DevicesService,
@@ -789,6 +800,82 @@ class AuthRepositoryImpl(
onSuccess = { AuthRequestResult.Success(it) },
)
@Suppress("LongMethod")
override fun createAuthRequestWithUpdates(
email: String,
): Flow<CreateAuthRequestResult> = flow {
val initialResult = createNewAuthRequest(email)
.getOrNull()
?: run {
emit(CreateAuthRequestResult.Error)
return@flow
}
val authRequestResponse = initialResult.authRequestResponse
var authRequest = initialResult.authRequest
emit(CreateAuthRequestResult.Update(authRequest))
var isComplete = false
while (currentCoroutineContext().isActive && !isComplete) {
delay(timeMillis = PASSWORDLESS_NOTIFICATION_RETRY_INTERVAL_MILLIS)
newAuthRequestService
.getAuthRequestUpdate(
requestId = authRequest.id,
accessCode = authRequestResponse.accessCode,
)
.map { request ->
AuthRequest(
id = request.id,
publicKey = request.publicKey,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
responseDate = request.responseDate,
requestApproved = request.requestApproved ?: false,
originUrl = request.originUrl,
fingerprint = authRequest.fingerprint,
)
}
.fold(
onFailure = { emit(CreateAuthRequestResult.Error) },
onSuccess = { updateAuthRequest ->
when {
updateAuthRequest.requestApproved -> {
isComplete = true
emit(
CreateAuthRequestResult.Success(
authRequest = updateAuthRequest,
authRequestResponse = authRequestResponse,
),
)
}
!updateAuthRequest.requestApproved &&
updateAuthRequest.responseDate != null -> {
isComplete = true
emit(CreateAuthRequestResult.Declined)
}
updateAuthRequest
.creationDate
.toInstant()
.plusMillis(PASSWORDLESS_NOTIFICATION_TIMEOUT_MILLIS)
.isBefore(clock.instant()) -> {
isComplete = true
emit(CreateAuthRequestResult.Expired)
}
else -> {
authRequest = updateAuthRequest
emit(CreateAuthRequestResult.Update(authRequest))
}
}
},
)
}
}
override suspend fun getAuthRequest(
fingerprint: String,
): AuthRequestResult =
@@ -1021,6 +1108,42 @@ class AuthRepositoryImpl(
)
}
/**
* Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
* with the [AuthRequest] and [AuthRequestResponse].
*/
private suspend fun createNewAuthRequest(
email: String,
): Result<NewAuthRequestData> =
authSdkSource
.getNewAuthRequest(email)
.flatMap { authRequestResponse ->
newAuthRequestService
.createAuthRequest(
email = email,
publicKey = authRequestResponse.publicKey,
deviceId = authDiskSource.uniqueAppId,
accessCode = authRequestResponse.accessCode,
fingerprint = authRequestResponse.fingerprint,
)
.map { request ->
AuthRequest(
id = request.id,
publicKey = request.publicKey,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
responseDate = request.responseDate,
requestApproved = request.requestApproved ?: false,
originUrl = request.originUrl,
fingerprint = authRequestResponse.fingerprint,
)
}
.map { NewAuthRequestData(it, authRequestResponse) }
}
/**
* Get the remembered two-factor token associated with the user's email, if applicable.
*/
@@ -1069,3 +1192,11 @@ class AuthRepositoryImpl(
?.copy(accounts = accounts)
}
}
/**
* Wrapper class for the [AuthRequest] and [AuthRequestResponse] data.
*/
private data class NewAuthRequestData(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
)

View File

@@ -22,6 +22,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton
/**
@@ -35,6 +36,7 @@ object AuthRepositoryModule {
@Singleton
@Suppress("LongParameterList")
fun providesAuthRepository(
clock: Clock,
accountsService: AccountsService,
authRequestsService: AuthRequestsService,
devicesService: DevicesService,
@@ -52,6 +54,7 @@ object AuthRepositoryModule {
userLogoutManager: UserLogoutManager,
pushManager: PushManager,
): AuthRepository = AuthRepositoryImpl(
clock = clock,
accountsService = accountsService,
authRequestsService = authRequestsService,
devicesService = devicesService,

View File

@@ -0,0 +1,38 @@
package com.x8bit.bitwarden.data.auth.repository.model
import com.bitwarden.core.AuthRequestResponse
/**
* Models result of creating a new login approval request.
*/
sealed class CreateAuthRequestResult {
/**
* Models the data returned when receiving an update for an auth request.
*/
data class Update(
val authRequest: AuthRequest,
) : CreateAuthRequestResult()
/**
* Models the data returned when a auth request has been approved.
*/
data class Success(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
) : CreateAuthRequestResult()
/**
* There was a generic error getting the user's auth requests.
*/
data object Error : CreateAuthRequestResult()
/**
* The auth request has been declined.
*/
data object Declined : CreateAuthRequestResult()
/**
* The auth request has expired.
*/
data object Expired : CreateAuthRequestResult()
}