From 081863827399eecd982b5112a9ab3a91d13a0399 Mon Sep 17 00:00:00 2001 From: Caleb Derosier <125901828+caleb-livefront@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:50:09 -0700 Subject: [PATCH] BIT-1291: Initiate Login with Device flow (#791) --- .../datasource/network/api/AuthRequestsApi.kt | 11 ++ .../network/di/AuthNetworkModule.kt | 10 ++ .../network/model/AuthRequestRequestJson.kt | 28 +++++ .../network/model/AuthRequestTypeJson.kt | 19 ++++ .../network/model/AuthRequestsResponseJson.kt | 1 + .../service/AuthRequestsServiceImpl.kt | 1 - .../network/service/NewAuthRequestService.kt | 19 ++++ .../service/NewAuthRequestServiceImpl.kt | 31 ++++++ .../data/auth/repository/AuthRepository.kt | 6 ++ .../auth/repository/AuthRepositoryImpl.kt | 40 ++++++- .../repository/di/AuthRepositoryModule.kt | 3 + .../repository/model/AuthRequestResult.kt | 18 ++++ .../LoginWithDeviceViewModel.kt | 34 ++++++ .../service/NewAuthRequestServiceTest.kt | 73 +++++++++++++ .../auth/repository/AuthRepositoryTest.kt | 102 ++++++++++++++++++ .../LoginWithDeviceViewModelTest.kt | 14 ++- 16 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestRequestJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestTypeJson.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestService.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/AuthRequestResult.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthRequestsApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthRequestsApi.kt index 12d9b028c2..ca4cefdedc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthRequestsApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/AuthRequestsApi.kt @@ -1,13 +1,24 @@ package com.x8bit.bitwarden.data.auth.datasource.network.api +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST /** * Defines raw calls under the /auth-requests API. */ interface AuthRequestsApi { + /** + * Notifies the server of a new authentication request. + */ + @POST("/auth-requests") + suspend fun createAuthRequest( + @Body body: AuthRequestRequestJson, + ): Result + /** * Gets a list of auth requests for this device. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt index 0837ada4c9..bf632d6cd4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/di/AuthNetworkModule.kt @@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedSe import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedServiceImpl import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl +import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService +import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestServiceImpl import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits import dagger.Module import dagger.Provides @@ -74,4 +76,12 @@ object AuthNetworkModule { .build() .create(), ) + + @Provides + @Singleton + fun providesNewAuthRequestService( + retrofits: Retrofits, + ): NewAuthRequestService = NewAuthRequestServiceImpl( + authRequestsApi = retrofits.unauthenticatedApiRetrofit.create(), + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestRequestJson.kt new file mode 100644 index 0000000000..9de85e4262 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestRequestJson.kt @@ -0,0 +1,28 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Request body for creating an auth request. + */ +@Serializable +data class AuthRequestRequestJson( + @SerialName("email") + val email: String, + + @SerialName("publicKey") + val publicKey: String, + + @SerialName("deviceIdentifier") + val deviceId: String, + + @SerialName("accessCode") + val accessCode: String, + + @SerialName("type") + val type: AuthRequestTypeJson, + + @SerialName("fingerprintPhrase") + val fingerprint: String, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestTypeJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestTypeJson.kt new file mode 100644 index 0000000000..228ee33d5a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestTypeJson.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.model + +import androidx.annotation.Keep +import com.x8bit.bitwarden.data.platform.datasource.network.serializer.BaseEnumeratedIntSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents the different types of auth requests. + */ +@Serializable(AuthRequestTypeSerializer::class) +enum class AuthRequestTypeJson { + @SerialName("0") + LOGIN_WITH_DEVICE, +} + +@Keep +private class AuthRequestTypeSerializer : + BaseEnumeratedIntSerializer(AuthRequestTypeJson.entries.toTypedArray()) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestsResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestsResponseJson.kt index 1943a2fbd1..3b195aa46d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestsResponseJson.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/AuthRequestsResponseJson.kt @@ -41,6 +41,7 @@ data class AuthRequestsResponseJson( @SerialName("requestIpAddress") val ipAddress: String, + @SerialName("key") val key: String?, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt index 403da2e971..4eaf0168e5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/AuthRequestsServiceImpl.kt @@ -6,7 +6,6 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsRespon class AuthRequestsServiceImpl( private val authRequestsApi: AuthRequestsApi, ) : AuthRequestsService { - override suspend fun getAuthRequests(): Result = authRequestsApi.getAuthRequests() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestService.kt new file mode 100644 index 0000000000..077ff17348 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestService.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson + +/** + * Provides an API for creating a new authentication request. + */ +interface NewAuthRequestService { + /** + * Informs the server of a new auth request in order to notify approving devices. + */ + suspend fun createAuthRequest( + email: String, + publicKey: String, + deviceId: String, + accessCode: String, + fingerprint: String, + ): Result +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt new file mode 100644 index 0000000000..8ebcd515fb --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceImpl.kt @@ -0,0 +1,31 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestRequestJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestTypeJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson + +/** + * The default implementation of the [NewAuthRequestService]. + */ +class NewAuthRequestServiceImpl( + private val authRequestsApi: AuthRequestsApi, +) : NewAuthRequestService { + override suspend fun createAuthRequest( + email: String, + publicKey: String, + deviceId: String, + accessCode: String, + fingerprint: String, + ): Result = + authRequestsApi.createAuthRequest( + AuthRequestRequestJson( + email = email, + publicKey = publicKey, + deviceId = deviceId, + accessCode = accessCode, + fingerprint = fingerprint, + type = AuthRequestTypeJson.LOGIN_WITH_DEVICE, + ), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 6d7432a4e6..f7f20a0df0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest +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 @@ -157,6 +158,11 @@ interface AuthRepository : AuthenticatorProvider { */ fun setSsoCallbackResult(result: SsoCallbackResult) + /** + * Creates a new authentication request. + */ + suspend fun createAuthRequest(email: String): AuthRequestResult + /** * Get a list of the current user's [AuthRequest]s. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index c7fe115b95..c3274424e1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -21,10 +21,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService +import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest +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 @@ -80,6 +82,7 @@ class AuthRepositoryImpl( private val devicesService: DevicesService, private val haveIBeenPwnedService: HaveIBeenPwnedService, private val identityService: IdentityService, + private val newAuthRequestService: NewAuthRequestService, private val authSdkSource: AuthSdkSource, private val authDiskSource: AuthDiskSource, private val environmentRepository: EnvironmentRepository, @@ -555,6 +558,40 @@ class AuthRepositoryImpl( mutableSsoCallbackResultFlow.tryEmit(result) } + override suspend fun createAuthRequest( + email: String, + ): AuthRequestResult = + authSdkSource + .getNewAuthRequest(email) + .flatMap { authRequest -> + newAuthRequestService.createAuthRequest( + email = email, + publicKey = authRequest.publicKey, + deviceId = authDiskSource.uniqueAppId, + accessCode = authRequest.accessCode, + fingerprint = authRequest.fingerprint, + ) + } + .fold( + onFailure = { AuthRequestResult.Error }, + onSuccess = { request -> + AuthRequestResult.Success( + authRequest = 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, + ), + ) + }, + ) + override suspend fun getAuthRequests(): AuthRequestsResult = authRequestsService.getAuthRequests() .fold( @@ -582,7 +619,8 @@ class AuthRepositoryImpl( override suspend fun getFingerprintPhrase( email: String, ): UserFingerprintResult = - authSdkSource.getNewAuthRequest(email) + authSdkSource + .getNewAuthRequest(email) .flatMap { requestResponse -> authSdkSource .getUserFingerprint( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt index f01d8701ce..51d4d68767 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService +import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -36,6 +37,7 @@ object AuthRepositoryModule { devicesService: DevicesService, identityService: IdentityService, haveIBeenPwnedService: HaveIBeenPwnedService, + newAuthRequestService: NewAuthRequestService, authSdkSource: AuthSdkSource, authDiskSource: AuthDiskSource, dispatchers: DispatcherManager, @@ -48,6 +50,7 @@ object AuthRepositoryModule { authRequestsService = authRequestsService, devicesService = devicesService, identityService = identityService, + newAuthRequestService = newAuthRequestService, authSdkSource = authSdkSource, authDiskSource = authDiskSource, haveIBeenPwnedService = haveIBeenPwnedService, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/AuthRequestResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/AuthRequestResult.kt new file mode 100644 index 0000000000..c1638a504f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/AuthRequestResult.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +/** + * Models result of creating a new login approval request. + */ +sealed class AuthRequestResult { + /** + * Models the data returned when creating an auth request. + */ + data class Success( + val authRequest: AuthRequest, + ) : AuthRequestResult() + + /** + * There was an error getting the user's auth requests. + */ + data object Error : AuthRequestResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt index 4818a51573..1849dd6308 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.Text @@ -32,6 +33,8 @@ class LoginWithDeviceViewModel @Inject constructor( ), ) { init { + sendNewAuthRequest() + viewModelScope.launch { trySendAction( LoginWithDeviceAction.Internal.FingerprintPhraseReceived( @@ -47,6 +50,10 @@ class LoginWithDeviceViewModel @Inject constructor( LoginWithDeviceAction.ResendNotificationClick -> handleResendNotificationClicked() LoginWithDeviceAction.ViewAllLogInOptionsClick -> handleViewAllLogInOptionsClicked() + is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> { + handleNewAuthRequestResultReceived(action) + } + is LoginWithDeviceAction.Internal.FingerprintPhraseReceived -> { handleFingerprintPhraseReceived(action) } @@ -66,6 +73,14 @@ class LoginWithDeviceViewModel @Inject constructor( sendEvent(LoginWithDeviceEvent.NavigateBack) } + private fun handleNewAuthRequestResultReceived( + action: LoginWithDeviceAction.Internal.NewAuthRequestResultReceive, + ) { + if (action.result is AuthRequestResult.Error) { + // TODO BIT-1563 handle error + } + } + private fun handleFingerprintPhraseReceived( action: LoginWithDeviceAction.Internal.FingerprintPhraseReceived, ) { @@ -91,6 +106,18 @@ class LoginWithDeviceViewModel @Inject constructor( } } } + + private fun sendNewAuthRequest() { + viewModelScope.launch { + trySendAction( + LoginWithDeviceAction.Internal.NewAuthRequestResultReceive( + result = authRepository.createAuthRequest( + email = state.emailAddress, + ), + ), + ) + } + } } /** @@ -176,6 +203,13 @@ sealed class LoginWithDeviceAction { * Models actions for internal use by the view model. */ sealed class Internal : LoginWithDeviceAction() { + /** + * A new auth request result was received. + */ + data class NewAuthRequestResultReceive( + val result: AuthRequestResult, + ) : Internal() + /** * A fingerprint phrase for this user has been received. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceTest.kt new file mode 100644 index 0000000000..22b0fa2fdf --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/NewAuthRequestServiceTest.kt @@ -0,0 +1,73 @@ +package com.x8bit.bitwarden.data.auth.datasource.network.service + +import com.x8bit.bitwarden.data.auth.datasource.network.api.AuthRequestsApi +import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson +import com.x8bit.bitwarden.data.platform.base.BaseServiceTest +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import retrofit2.create +import java.time.ZonedDateTime + +class NewAuthRequestServiceTest : BaseServiceTest() { + + private val authRequestsApi: AuthRequestsApi = retrofit.create() + private val service = NewAuthRequestServiceImpl( + authRequestsApi = authRequestsApi, + ) + + @Test + fun `createAuthRequest when request response is Failure should return Failure`() = runTest { + val response = MockResponse().setResponseCode(400) + server.enqueue(response) + val actual = service.createAuthRequest( + email = "test@gmail.com", + publicKey = "1234", + deviceId = "4321", + accessCode = "accessCode", + fingerprint = "fingerprint", + ) + assertTrue(actual.isFailure) + } + + @Test + fun `createAuthRequest when request response is Success should return Success`() = runTest { + val json = """ + { + "id": "1", + "publicKey": "2", + "requestDeviceType": "Android", + "requestIpAddress": "1.0.0.1", + "key": "key", + "masterPasswordHash": "verySecureHash", + "creationDate": "2024-09-13T01:00:00.00Z", + "requestApproved": true, + "origin": "www.bitwarden.com" + } + """ + val expected = AuthRequestsResponseJson.AuthRequest( + id = "1", + publicKey = "2", + platform = "Android", + ipAddress = "1.0.0.1", + key = "key", + masterPasswordHash = "verySecureHash", + creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), + responseDate = null, + requestApproved = true, + originUrl = "www.bitwarden.com", + ) + val response = MockResponse().setBody(json).setResponseCode(200) + server.enqueue(response) + val actual = service.createAuthRequest( + email = "test@gmail.com", + publicKey = "1234", + deviceId = "4321", + accessCode = "accessCode", + fingerprint = "fingerprint", + ) + assertEquals(Result.success(expected), actual) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 165fa708e4..e5218925cd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.service.AuthRequestsServ import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService +import com.x8bit.bitwarden.data.auth.datasource.network.service.NewAuthRequestService import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_0 import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_1 @@ -36,6 +37,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4 import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest +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 @@ -98,6 +100,7 @@ class AuthRepositoryTest { private val devicesService: DevicesService = mockk() private val identityService: IdentityService = mockk() private val haveIBeenPwnedService: HaveIBeenPwnedService = mockk() + private val newAuthRequestService: NewAuthRequestService = mockk() private val mutableVaultStateFlow = MutableStateFlow(VAULT_STATE) private val vaultRepository: VaultRepository = mockk { every { vaultStateFlow } returns mutableVaultStateFlow @@ -114,6 +117,11 @@ class AuthRepositoryTest { every { setDefaultsIfNecessary(any()) } just runs } private val authSdkSource = mockk { + coEvery { + getNewAuthRequest( + email = EMAIL, + ) + } returns Result.success(AUTH_REQUEST_RESPONSE) coEvery { hashPassword( email = EMAIL, @@ -151,6 +159,7 @@ class AuthRepositoryTest { devicesService = devicesService, identityService = identityService, haveIBeenPwnedService = haveIBeenPwnedService, + newAuthRequestService = newAuthRequestService, authSdkSource = authSdkSource, authDiskSource = fakeAuthDiskSource, environmentRepository = fakeEnvironmentRepository, @@ -1605,6 +1614,93 @@ class AuthRepositoryTest { ) } + @Test + fun `createAuthRequest should return failure when service returns failure`() = runTest { + val accessCode = "accessCode" + val fingerprint = "fingerprint" + coEvery { + newAuthRequestService.createAuthRequest( + email = EMAIL, + publicKey = PUBLIC_KEY, + deviceId = UNIQUE_APP_ID, + accessCode = accessCode, + fingerprint = fingerprint, + ) + } returns Throwable("Fail").asFailure() + + val result = repository.createAuthRequest( + email = EMAIL, + ) + + coVerify(exactly = 1) { + newAuthRequestService.createAuthRequest( + email = EMAIL, + publicKey = PUBLIC_KEY, + deviceId = UNIQUE_APP_ID, + accessCode = accessCode, + fingerprint = fingerprint, + ) + } + assertEquals(AuthRequestResult.Error, result) + } + + @Test + fun `createAuthRequest should return success when service returns success`() = runTest { + val accessCode = "accessCode" + val fingerprint = "fingerprint" + + val responseJson = 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 = true, + originUrl = "www.bitwarden.com", + ) + val expected = AuthRequestResult.Success( + authRequest = 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 = true, + originUrl = "www.bitwarden.com", + ), + ) + coEvery { + newAuthRequestService.createAuthRequest( + email = EMAIL, + publicKey = PUBLIC_KEY, + deviceId = UNIQUE_APP_ID, + accessCode = accessCode, + fingerprint = fingerprint, + ) + } returns responseJson.asSuccess() + + val result = repository.createAuthRequest( + email = EMAIL, + ) + + coVerify(exactly = 1) { + newAuthRequestService.createAuthRequest( + email = EMAIL, + publicKey = PUBLIC_KEY, + deviceId = UNIQUE_APP_ID, + accessCode = accessCode, + fingerprint = fingerprint, + ) + } + assertEquals(expected, result) + } + @Test fun `getAuthRequests should return failure when service returns failure`() = runTest { coEvery { @@ -1869,6 +1965,12 @@ class AuthRepositoryTest { private val PRE_LOGIN_SUCCESS = PreLoginResponseJson( kdfParams = PreLoginResponseJson.KdfParams.Pbkdf2(iterations = 1u), ) + private val AUTH_REQUEST_RESPONSE = AuthRequestResponse( + privateKey = PRIVATE_KEY, + publicKey = PUBLIC_KEY, + accessCode = "accessCode", + fingerprint = "fingerprint", + ) private val REFRESH_TOKEN_RESPONSE_JSON = RefreshTokenResponseJson( accessToken = ACCESS_TOKEN_2, expiresIn = 3600, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt index 5d2aae5170..73f1b42b6b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText @@ -20,6 +21,9 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { coEvery { getFingerprintPhrase(EMAIL) } returns UserFingerprintResult.Success("initialFingerprint") + coEvery { + createAuthRequest(EMAIL) + } returns mockk() } @Test @@ -28,12 +32,17 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) } + coVerify { authRepository.createAuthRequest(EMAIL) } coVerify { authRepository.getFingerprintPhrase(EMAIL) } } @Test fun `initial state should be correct when set`() = runTest { val newEmail = "newEmail@gmail.com" + + coEvery { + authRepository.createAuthRequest(newEmail) + } returns mockk() coEvery { authRepository.getFingerprintPhrase(newEmail) } returns UserFingerprintResult.Success("initialFingerprint") @@ -47,7 +56,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { viewModel.stateFlow.test { assertEquals(state, awaitItem()) } - coVerify { authRepository.getFingerprintPhrase(newEmail) } + coVerify { + authRepository.createAuthRequest(newEmail) + authRepository.getFingerprintPhrase(newEmail) + } } @Test