Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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 @@ -32,7 +32,8 @@ interface AuthRequestManager {
fun getAuthRequestByIdFlow(requestId: String): Flow<AuthRequestUpdatesResult>

/**
* Get all auth request and emits updates over time.
* Get all auth requests and emits updates over time, including when a passwordless push for
* the active user indicates the list changed.
*/
fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ 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.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.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.isActive
import java.time.Clock
import java.time.Instant
Expand All @@ -38,7 +43,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,23 +52,31 @@ 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

override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
while (currentCoroutineContext().isActive) {
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = merge(
// Reads immediately, then on the polling interval.
flow {
while (currentCoroutineContext().isActive) {
emit(Unit)
delay(timeMillis = PASSWORDLESS_APPROVER_INTERVAL_MILLIS)
}
},
pushManager
.passwordlessRequestFlow
.filter { it.userId == activeUserId }
.map { },
)
.map {
when (val result = getAuthRequests()) {
is AuthRequestsResult.Error -> {
emit(AuthRequestsUpdatesResult.Error(error = result.error))
}

is AuthRequestsResult.Error -> AuthRequestsUpdatesResult.Error(error = result.error)
is AuthRequestsResult.Success -> {
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
AuthRequestsUpdatesResult.Update(authRequests = result.authRequests)
}
}
delay(timeMillis = PASSWORDLESS_APPROVER_INTERVAL_MILLIS)
}
}

@Suppress("LongMethod")
override fun createAuthRequestWithUpdates(
Expand Down Expand Up @@ -91,18 +104,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 +189,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,23 +257,12 @@ 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) },
Expand All @@ -288,18 +276,11 @@ class AuthRequestManagerImpl(
.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 +294,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 +330,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 +358,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 +416,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,23 @@
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)

/**
* Filters out [AuthRequest]s that match one of the following criteria:
* * The request has been approved.
* * The request has been declined (indicated by it not being approved & having a responseDate).
* * The request has expired (it is at least 5 minutes old).
*/
fun List<AuthRequest>.filterRespondedAndExpired(clock: Clock): List<AuthRequest> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘

filter { it.isActionable(clock = 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