PM-13803 Check to see if an existing admin request is pending before … (#4271)

This commit is contained in:
Dave Severns 2024-11-11 10:53:11 -05:00 committed by GitHub
parent 1bb85d0fa0
commit 6dd783051f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 219 additions and 42 deletions

View file

@ -7,13 +7,21 @@ import kotlinx.serialization.Serializable
* Container for the user's API tokens.
*
* @property requestId The ID of the pending Auth Request.
* @property requestPrivateKey The private of the pending Auth Request.
* @property requestPrivateKey The private key of the pending Auth Request.
* @property requestAccessCode The access code of the pending Auth Request.
* @property requestFingerprint The fingerprint of the pending Auth Request.
*/
@Serializable
data class PendingAuthRequestJson(
@SerialName("Id")
@SerialName("id")
val requestId: String,
@SerialName("PrivateKey")
@SerialName("privateKey")
val requestPrivateKey: String,
@SerialName("accessCode")
val requestAccessCode: String,
@SerialName("fingerprint")
val requestFingerprint: String,
)

View file

@ -17,6 +17,7 @@ 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.toAuthRequestTypeJson
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import kotlinx.coroutines.currentCoroutineContext
@ -65,7 +66,7 @@ class AuthRequestManagerImpl(
email: String,
authRequestType: AuthRequestType,
): Flow<CreateAuthRequestResult> = flow {
val initialResult = createNewAuthRequest(
val initialResult = createNewAuthRequestIfNecessary(
email = email,
authRequestType = authRequestType.toAuthRequestTypeJson(),
)
@ -74,7 +75,6 @@ class AuthRequestManagerImpl(
emit(CreateAuthRequestResult.Error)
return@flow
}
val authRequestResponse = initialResult.authRequestResponse
var authRequest = initialResult.authRequest
emit(CreateAuthRequestResult.Update(authRequest))
@ -84,7 +84,7 @@ class AuthRequestManagerImpl(
newAuthRequestService
.getAuthRequestUpdate(
requestId = authRequest.id,
accessCode = authRequestResponse.accessCode,
accessCode = initialResult.accessCode,
isSso = authRequestType.isSso,
)
.map { request ->
@ -112,7 +112,8 @@ class AuthRequestManagerImpl(
emit(
CreateAuthRequestResult.Success(
authRequest = updateAuthRequest,
authRequestResponse = authRequestResponse,
privateKey = initialResult.privateKey,
accessCode = initialResult.accessCode,
),
)
}
@ -354,6 +355,52 @@ class AuthRequestManagerImpl(
)
}
/**
* Creates a new auth request for the given email and returns a [NewAuthRequestData].
* If the auth request type is [AuthRequestTypeJson.ADMIN_APPROVAL], check for a
* pending auth request and return it if it exists we should return that request.
*/
private suspend fun createNewAuthRequestIfNecessary(
email: String,
authRequestType: AuthRequestTypeJson,
): Result<NewAuthRequestData> {
return if (authRequestType == AuthRequestTypeJson.ADMIN_APPROVAL) {
authDiskSource
.getPendingAuthRequest(requireNotNull(activeUserId))
?.let { pendingAuthRequest ->
authRequestsService
.getAuthRequest(pendingAuthRequest.requestId)
.map {
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,
fingerprint = pendingAuthRequest.requestFingerprint,
),
privateKey = pendingAuthRequest.requestPrivateKey,
accessCode = pendingAuthRequest.requestAccessCode,
)
.asSuccess()
}
.getOrNull()
}
?: createNewAuthRequest(email = email, authRequestType = authRequestType)
} else {
createNewAuthRequest(
email = email,
authRequestType = authRequestType,
)
}
}
/**
* Attempts to create a new auth request for the given email and returns a [NewAuthRequestData]
* with the [AuthRequest] and [AuthRequestResponse].
@ -381,6 +428,8 @@ class AuthRequestManagerImpl(
pendingAuthRequest = PendingAuthRequestJson(
requestId = it.id,
requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
),
)
}
@ -400,7 +449,13 @@ class AuthRequestManagerImpl(
fingerprint = authRequestResponse.fingerprint,
)
}
.map { NewAuthRequestData(it, authRequestResponse) }
.map {
NewAuthRequestData(
authRequest = it,
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
)
}
}
private suspend fun getFingerprintPhrase(
@ -420,5 +475,6 @@ class AuthRequestManagerImpl(
*/
private data class NewAuthRequestData(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
val privateKey: String,
val accessCode: String,
)

View file

@ -1,7 +1,5 @@
package com.x8bit.bitwarden.data.auth.manager.model
import com.bitwarden.core.AuthRequestResponse
/**
* Models result of creating a new login approval request.
*/
@ -18,7 +16,8 @@ sealed class CreateAuthRequestResult {
*/
data class Success(
val authRequest: AuthRequest,
val authRequestResponse: AuthRequestResponse,
val privateKey: String,
val accessCode: String,
) : CreateAuthRequestResult()
/**

View file

@ -116,11 +116,11 @@ class LoginWithDeviceViewModel @Inject constructor(
),
dialogState = null,
loginData = LoginWithDeviceState.LoginData(
accessCode = result.authRequestResponse.accessCode,
accessCode = result.accessCode,
requestId = result.authRequest.id,
masterPasswordHash = result.authRequest.masterPasswordHash,
asymmetricalKey = requireNotNull(result.authRequest.key),
privateKey = result.authRequestResponse.privateKey,
privateKey = result.privateKey,
captchaToken = null,
),
)

View file

@ -244,6 +244,8 @@ class AuthDiskSourceTest {
val pendingAuthRequestJson = PendingAuthRequestJson(
requestId = "12345",
requestPrivateKey = "67890",
requestFingerprint = "fingerprint",
requestAccessCode = "accessCode",
)
authDiskSource.storePendingAuthRequest(
userId = userId,
@ -594,15 +596,22 @@ class AuthDiskSourceTest {
pendingAdminAuthRequestKey,
"""
{
"Id": "12345",
"PrivateKey": "67890"
"id": "12345",
"privateKey": "67890",
"fingerprint": "fingerprint",
"accessCode": "accessCode"
}
""",
)
}
val actual = authDiskSource.getPendingAuthRequest(userId = mockUserId)
assertEquals(
PendingAuthRequestJson(requestId = "12345", requestPrivateKey = "67890"),
PendingAuthRequestJson(
requestId = "12345",
requestPrivateKey = "67890",
requestFingerprint = "fingerprint",
requestAccessCode = "accessCode",
),
actual,
)
}
@ -615,6 +624,8 @@ class AuthDiskSourceTest {
val pendingAdminAuthRequest = PendingAuthRequestJson(
requestId = "12345",
requestPrivateKey = "67890",
requestFingerprint = "fingerprint",
requestAccessCode = "accessCode",
)
authDiskSource.storePendingAuthRequest(
userId = mockUserId,
@ -628,8 +639,10 @@ class AuthDiskSourceTest {
json.parseToJsonElement(
"""
{
"Id": "12345",
"PrivateKey": "67890"
"id": "12345",
"privateKey": "67890",
"fingerprint": "fingerprint",
"accessCode": "accessCode"
}
"""
.trimIndent(),

View file

@ -162,7 +162,8 @@ class AuthRequestManagerTest {
assertEquals(
CreateAuthRequestResult.Success(
authRequest = authRequest.copy(requestApproved = true),
authRequestResponse = authRequestResponse,
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
),
awaitItem(),
)
@ -170,6 +171,87 @@ class AuthRequestManagerTest {
}
}
@Suppress("MaxLineLength")
@Test
fun `createAuthRequestWithUpdates with a pending admin request Success and getAuthRequestUpdate with approval should emit Success`() =
runTest {
val email = "email@email.com"
val authRequestResponse = AUTH_REQUEST_RESPONSE
val authRequestResponseJson = AuthRequestsResponseJson.AuthRequest(
id = "1",
publicKey = PUBLIC_KEY,
platform = "Android",
ipAddress = "192.168.0.1",
key = "public",
masterPasswordHash = "verySecureHash",
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
responseDate = null,
requestApproved = false,
originUrl = "www.bitwarden.com",
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE
fakeAuthDiskSource.storePendingAuthRequest(
userId = USER_ID,
pendingAuthRequest = PendingAuthRequestJson(
requestId = authRequestResponseJson.id,
requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
),
)
val updatedAuthRequestResponseJson = authRequestResponseJson.copy(
requestApproved = true,
)
val authRequest = AuthRequest(
id = authRequestResponseJson.id,
publicKey = authRequestResponseJson.publicKey,
platform = authRequestResponseJson.platform,
ipAddress = authRequestResponseJson.ipAddress,
key = authRequestResponseJson.key,
masterPasswordHash = authRequestResponseJson.masterPasswordHash,
creationDate = authRequestResponseJson.creationDate,
responseDate = authRequestResponseJson.responseDate,
requestApproved = authRequestResponseJson.requestApproved ?: false,
originUrl = authRequestResponseJson.originUrl,
fingerprint = authRequestResponse.fingerprint,
)
coEvery {
authRequestsService.getAuthRequest(
requestId = authRequest.id,
)
} returns authRequestResponseJson.asSuccess()
coEvery {
newAuthRequestService.getAuthRequestUpdate(
requestId = authRequest.id,
accessCode = authRequestResponse.accessCode,
isSso = true,
)
} returnsMany listOf(
authRequestResponseJson.asSuccess(),
updatedAuthRequestResponseJson.asSuccess(),
)
repository
.createAuthRequestWithUpdates(
email = email,
authRequestType = AuthRequestType.SSO_ADMIN_APPROVAL,
)
.test {
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
assertEquals(CreateAuthRequestResult.Update(authRequest), awaitItem())
assertEquals(
CreateAuthRequestResult.Success(
authRequest = authRequest.copy(requestApproved = true),
privateKey = authRequestResponse.privateKey,
accessCode = authRequestResponse.accessCode,
),
awaitItem(),
)
awaitComplete()
}
coVerify(exactly = 0) { authSdkSource.getNewAuthRequest(any()) }
}
@Suppress("MaxLineLength")
@Test
fun `createAuthRequestWithUpdates with createNewAuthRequest Success and getAuthRequestUpdate with response date and no approval should emit Declined`() =
@ -305,6 +387,8 @@ class AuthRequestManagerTest {
pendingAuthRequest = PendingAuthRequestJson(
requestId = authRequestResponseJson.id,
requestPrivateKey = authRequestResponse.privateKey,
requestAccessCode = authRequestResponse.accessCode,
requestFingerprint = authRequestResponse.fingerprint,
),
)
assertEquals(CreateAuthRequestResult.Expired, awaitItem())

View file

@ -3566,6 +3566,8 @@ class AuthRepositoryTest {
val pendingAuthRequest = PendingAuthRequestJson(
requestId = "requestId",
requestPrivateKey = "requestPrivateKey",
requestFingerprint = "fingerprint",
requestAccessCode = "accessCode",
)
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
key = null,

View file

@ -2,7 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.AuthRequestResponse
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest
import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestType
@ -166,7 +165,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
CreateAuthRequestResult.Success(
authRequest = AUTH_REQUEST,
privateKey = AUTH_REQUEST_PRIVATE_KEY,
accessCode = AUTH_REQUEST_ACCESS_CODE,
),
)
assertEquals(
DEFAULT_STATE.copy(
@ -196,9 +199,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_RESPONSE.accessCode,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
)
@ -227,7 +230,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(initialState, awaitItem())
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
CreateAuthRequestResult.Success(
authRequest = AUTH_REQUEST,
privateKey = AUTH_REQUEST_PRIVATE_KEY,
accessCode = AUTH_REQUEST_ACCESS_CODE,
),
)
assertEquals(
initialState.copy(
@ -279,7 +286,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
viewModel.eventFlow.test {
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
CreateAuthRequestResult.Success(
authRequest = AUTH_REQUEST,
privateKey = AUTH_REQUEST_PRIVATE_KEY,
accessCode = AUTH_REQUEST_ACCESS_CODE,
),
)
assertEquals(
LoginWithDeviceEvent.NavigateToTwoFactorLogin(emailAddress = EMAIL),
@ -291,9 +302,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_RESPONSE.accessCode,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
)
@ -319,7 +330,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
CreateAuthRequestResult.Success(
authRequest = AUTH_REQUEST,
privateKey = AUTH_REQUEST_PRIVATE_KEY,
accessCode = AUTH_REQUEST_ACCESS_CODE,
),
)
assertEquals(
DEFAULT_STATE.copy(
@ -353,9 +368,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_RESPONSE.accessCode,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
)
@ -382,7 +397,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableCreateAuthRequestWithUpdatesFlow.tryEmit(
CreateAuthRequestResult.Success(AUTH_REQUEST, AUTH_REQUEST_RESPONSE),
CreateAuthRequestResult.Success(
authRequest = AUTH_REQUEST,
privateKey = AUTH_REQUEST_PRIVATE_KEY,
accessCode = AUTH_REQUEST_ACCESS_CODE,
),
)
assertEquals(
DEFAULT_STATE.copy(
@ -416,9 +435,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_RESPONSE.accessCode,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = null,
)
@ -474,9 +493,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
authRepository.login(
email = EMAIL,
requestId = AUTH_REQUEST.id,
accessCode = AUTH_REQUEST_RESPONSE.accessCode,
accessCode = AUTH_REQUEST_ACCESS_CODE,
asymmetricalKey = requireNotNull(AUTH_REQUEST.key),
requestPrivateKey = AUTH_REQUEST_RESPONSE.privateKey,
requestPrivateKey = AUTH_REQUEST_PRIVATE_KEY,
masterPasswordHash = AUTH_REQUEST.masterPasswordHash,
captchaToken = captchaToken,
)
@ -603,12 +622,8 @@ private val AUTH_REQUEST = AuthRequest(
fingerprint = FINGERPRINT,
)
private val AUTH_REQUEST_RESPONSE = AuthRequestResponse(
privateKey = "private_key",
publicKey = "public_key",
accessCode = "accessCode",
fingerprint = "fingerprint",
)
private const val AUTH_REQUEST_ACCESS_CODE = "accessCode"
private const val AUTH_REQUEST_PRIVATE_KEY = "private_key"
private val DEFAULT_LOGIN_DATA = LoginWithDeviceState.LoginData(
accessCode = "accessCode",