Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -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