Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ interface AuthRequestManager {
*/
fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult>

/**
* Get the [AuthRequest] for each incoming passwordless request for the active user, hydrated
* with the fingerprint required to approve it.
*
* Only requests that can still be acted upon are emitted; those already approved, declined, or
* expired are not. Requests that cannot be retrieved are omitted rather than emitted as errors.
*/
fun getPasswordlessAuthRequestFlow(): Flow<AuthRequest>

/**
* Get an [AuthRequest] by its request ID.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsResult
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
import com.x8bit.bitwarden.data.auth.manager.util.isActionable
import com.x8bit.bitwarden.data.auth.manager.util.isSso
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequest
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.isActive
import timber.log.Timber
import java.time.Clock
import java.time.Instant
import javax.inject.Singleton
Expand All @@ -38,7 +44,7 @@ private const val PASSWORDLESS_APPROVER_INTERVAL_MILLIS: Long = 5L * 60L * 1_000
/**
* Default implementation of [AuthRequestManager].
*/
@Suppress("TooManyFunctions")
@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
class AuthRequestManagerImpl(
private val clock: Clock,
Expand All @@ -47,6 +53,7 @@ class AuthRequestManagerImpl(
private val authDiskSource: AuthDiskSource,
private val authSdkSource: AuthSdkSource,
private val vaultSdkSource: VaultSdkSource,
private val pushManager: PushManager,
) : AuthRequestManager {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId

Expand Down Expand Up @@ -91,18 +98,11 @@ class AuthRequestManagerImpl(
isSso = authRequestType.isSso,
)
.map { request ->
AuthRequest(
id = request.id,
request.toAuthRequest(
fingerprint = authRequest.fingerprint,
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,
isRequestApproved = request.requestApproved ?: false,
)
}
.fold(
Expand Down Expand Up @@ -183,20 +183,13 @@ class AuthRequestManagerImpl(
isRequestApproved = false
responseDate = clock.instant()
}
AuthRequest(
id = request.id,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
originUrl = request.originUrl,
responseDate = responseDate,
requestApproved = isRequestApproved,
// The PublicKey and Fingerprint should be frozen in place to
// ensure no funny-business happens between multiple requests.
publicKey = initialAuthRequest.publicKey,
// The PublicKey and Fingerprint should be frozen in place to ensure no
// funny-business happens between multiple requests.
request.toAuthRequest(
fingerprint = initialAuthRequest.fingerprint,
publicKey = initialAuthRequest.publicKey,
responseDate = responseDate,
isRequestApproved = isRequestApproved,
)
}
}
Expand Down Expand Up @@ -258,48 +251,56 @@ class AuthRequestManagerImpl(
authRequestsService
.getAuthRequest(requestId)
.mapCatching { response ->
getFingerprintPhrase(response.publicKey)
.getOrThrow()
.let { fingerprint ->
AuthRequest(
id = response.id,
publicKey = response.publicKey,
platform = response.platform,
ipAddress = response.ipAddress,
key = response.key,
masterPasswordHash = response.masterPasswordHash,
creationDate = response.creationDate,
responseDate = response.responseDate,
requestApproved = response.requestApproved ?: false,
originUrl = response.originUrl,
fingerprint = fingerprint,
)
}
response.toAuthRequest(
fingerprint = getFingerprintPhrase(response.publicKey).getOrThrow(),
publicKey = response.publicKey,
responseDate = response.responseDate,
isRequestApproved = response.requestApproved ?: false,
)
}
.fold(
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
)
}

override fun getPasswordlessAuthRequestFlow(): Flow<AuthRequest> = pushManager
.passwordlessRequestFlow
// A push for a non-active user would otherwise be hydrated with the active user's token.
.filter { it.userId == activeUserId }
.mapNotNull { data ->
authRequestsService
.getAuthRequest(data.loginRequestId)
.mapCatching { response ->
response.toAuthRequest(
fingerprint = getFingerprintPhrase(response.publicKey).getOrThrow(),
publicKey = response.publicKey,
responseDate = response.responseDate,
isRequestApproved = response.requestApproved ?: false,
)
}
.fold(
onFailure = {
Timber.d(it, "Unable to hydrate the requested auth request.")
null
},
onSuccess = { authRequest ->
authRequest.takeIf { it.isActionable(clock = clock) }
},
)
}

override suspend fun getAuthRequestIfApproved(requestId: String): Result<AuthRequest> =
authRequestsService
.getAuthRequest(requestId)
.flatMap { request ->
if (request.requestApproved == true) {
getFingerprintPhrase(request.publicKey).map { fingerprint ->
AuthRequest(
id = request.id,
request.toAuthRequest(
fingerprint = fingerprint,
publicKey = request.publicKey,
platform = request.platform,
ipAddress = request.ipAddress,
key = request.key,
masterPasswordHash = request.masterPasswordHash,
creationDate = request.creationDate,
responseDate = request.responseDate,
requestApproved = true,
originUrl = request.originUrl,
fingerprint = fingerprint,
isRequestApproved = true,
)
}
} else {
Expand All @@ -313,18 +314,11 @@ class AuthRequestManagerImpl(
.map { response ->
response.authRequests.mapNotNull { request ->
getFingerprintPhrase(request.publicKey).getOrNull()?.let { fingerprint ->
AuthRequest(
id = request.id,
request.toAuthRequest(
fingerprint = fingerprint,
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 = fingerprint,
isRequestApproved = request.requestApproved ?: false,
)
}
}
Expand Down Expand Up @@ -356,18 +350,11 @@ class AuthRequestManagerImpl(
)
}
.map { request ->
AuthRequest(
id = request.id,
request.toAuthRequest(
fingerprint = "",
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 = "",
isRequestApproved = request.requestApproved ?: false,
)
}
.fold(
Expand All @@ -391,20 +378,13 @@ class AuthRequestManagerImpl(
?.let { pendingAuthRequest ->
authRequestsService
.getAuthRequest(pendingAuthRequest.requestId)
.map {
.map { request ->
NewAuthRequestData(
authRequest = AuthRequest(
id = it.id,
publicKey = it.publicKey,
platform = it.platform,
ipAddress = it.ipAddress,
key = it.key,
masterPasswordHash = it.masterPasswordHash,
creationDate = it.creationDate,
responseDate = it.responseDate,
requestApproved = it.requestApproved ?: false,
originUrl = it.originUrl,
authRequest = request.toAuthRequest(
fingerprint = pendingAuthRequest.requestFingerprint,
publicKey = request.publicKey,
responseDate = request.responseDate,
isRequestApproved = request.requestApproved ?: false,
),
privateKey = pendingAuthRequest.requestPrivateKey,
accessCode = pendingAuthRequest.requestAccessCode,
Expand Down Expand Up @@ -456,18 +436,11 @@ class AuthRequestManagerImpl(
}
}
.map { request ->
AuthRequest(
id = request.id,
request.toAuthRequest(
fingerprint = authRequestResponse.fingerprint,
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,
isRequestApproved = request.requestApproved ?: false,
)
}
.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ object AuthManagerModule {
authSdkSource: AuthSdkSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
pushManager: PushManager,
): AuthRequestManager =
AuthRequestManagerImpl(
clock = clock,
Expand All @@ -81,6 +82,7 @@ object AuthManagerModule {
authSdkSource = authSdkSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
pushManager = pushManager,
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.x8bit.bitwarden.data.auth.manager.util

import com.bitwarden.core.util.isOverFiveMinutesOld
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
import java.time.Clock

/**
* Whether this request may still be approved or declined
* and has not expired (it is under 5 minutes old).
*/
fun AuthRequest.isActionable(clock: Clock): Boolean =
!requestApproved &&
responseDate == null &&
!creationDate.isOverFiveMinutesOld(clock)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.auth.manager.util

import com.bitwarden.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
import java.time.Instant

/**
* Converts the given [AuthRequestsResponseJson.AuthRequest] to an [AuthRequest], given the
* [fingerprint] that the response itself does not carry.
*
* The [publicKey], [responseDate], and [isRequestApproved] are supplied by the caller.
*/
fun AuthRequestsResponseJson.AuthRequest.toAuthRequest(
fingerprint: String,
publicKey: String,
responseDate: Instant?,
isRequestApproved: Boolean,
): AuthRequest = AuthRequest(
id = this.id,
publicKey = publicKey,
platform = this.platform,
ipAddress = this.ipAddress,
key = this.key,
masterPasswordHash = this.masterPasswordHash,
creationDate = this.creationDate,
responseDate = responseDate,
requestApproved = isRequestApproved,
originUrl = this.originUrl,
fingerprint = fingerprint,
)
Loading
Loading