Add requestOtp and verifyOtp API methods (#1275)

This commit is contained in:
Caleb Derosier 2024-04-16 11:40:38 -06:00 committed by Álison Fernandes
parent dc2a0d10b9
commit ea01470d21
10 changed files with 202 additions and 0 deletions

View file

@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.CreateAccountKeysR
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
import retrofit2.http.Body
import retrofit2.http.HTTP
import retrofit2.http.POST
@ -24,6 +25,14 @@ interface AuthenticatedAccountsApi {
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
@POST("/accounts/request-otp")
suspend fun requestOtp(): Result<Unit>
@POST("/accounts/verify-otp")
suspend fun verifyOtp(
@Body body: VerifyOtpRequestJson,
): Result<Unit>
/**
* Resets the temporary password.
*/

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Request body for verifying a passcode.
*
* @param oneTimePasscode The one-time passcode to verify.
*/
@Serializable
data class VerifyOtpRequestJson(
@SerialName("OTP")
val oneTimePasscode: String,
)

View file

@ -33,6 +33,16 @@ interface AccountsService {
*/
suspend fun register(body: RegisterRequestJson): Result<RegisterResponseJson>
/**
* Request a one-time passcode that is sent to the user's email.
*/
suspend fun requestOneTimePasscode(): Result<Unit>
/**
* Verify that the provided [passcode] is correct.
*/
suspend fun verifyOneTimePasscode(passcode: String): Result<Unit>
/**
* Request a password hint.
*/

View file

@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJs
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequestJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyOtpRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import kotlinx.serialization.json.Json
@ -58,6 +59,16 @@ class AccountsServiceImpl(
) ?: throw throwable
}
override suspend fun requestOneTimePasscode(): Result<Unit> =
authenticatedAccountsApi.requestOtp()
override suspend fun verifyOneTimePasscode(passcode: String): Result<Unit> =
authenticatedAccountsApi.verifyOtp(
VerifyOtpRequestJson(
oneTimePasscode = passcode,
),
)
override suspend fun requestPasswordHint(
email: String,
): Result<PasswordHintResponseJson> =

View file

@ -16,12 +16,14 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
@ -194,6 +196,18 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
*/
fun logout()
/**
* Requests that a one-time passcode be sent to the user's email.
*/
suspend fun requestOneTimePasscode(): RequestOtpResult
/**
* Verifies that the given one-time passcode is correct. A successful result will correspond to
* [VerifyOtpResult.Verified], while an error or failure to verify will return
* [VerifyOtpResult.NotVerified].
*/
suspend fun verifyOneTimePasscode(oneTimePasscode: String): VerifyOtpResult
/**
* Resend the email with the two-factor verification code.
*/

View file

@ -45,6 +45,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
@ -52,6 +53,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
@ -564,6 +566,23 @@ class AuthRepositoryImpl(
userLogoutManager.logout(userId = userId)
}
override suspend fun requestOneTimePasscode(): RequestOtpResult =
accountsService.requestOneTimePasscode()
.fold(
onFailure = { RequestOtpResult.Error(it.message) },
onSuccess = { RequestOtpResult.Success },
)
override suspend fun verifyOneTimePasscode(oneTimePasscode: String): VerifyOtpResult =
accountsService
.verifyOneTimePasscode(
passcode = oneTimePasscode,
)
.fold(
onFailure = { VerifyOtpResult.NotVerified(it.message) },
onSuccess = { VerifyOtpResult.Verified },
)
override suspend fun resendVerificationCodeEmail(): ResendEmailResult =
resendEmailRequestJson
?.let { jsonRequest ->

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of requesting a one-time passcode.
*/
sealed class RequestOtpResult {
/**
* Represents a successful send of the one-time passcode.
*/
data object Success : RequestOtpResult()
/**
* Represents a failure to send the one-time passcode.
*/
data class Error(val message: String?) : RequestOtpResult()
}

View file

@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.repository.model
/**
* Models result of verifying a one-time passcode.
*/
sealed class VerifyOtpResult {
/**
* Represents a successful verification of the one-time passcode.
*/
data object Verified : VerifyOtpResult()
/**
* Represents a failure to verify the one-time passcode.
*/
data class NotVerified(val errorMessage: String?) : VerifyOtpResult()
}

View file

@ -219,6 +219,46 @@ class AccountsServiceTest : BaseServiceTest() {
assertEquals(expectedResponse.asSuccess(), service.register(registerRequestBody))
}
@Test
fun `requestOtp success should return Success`() = runTest {
val response = MockResponse().setResponseCode(200)
server.enqueue(response)
val result = service.requestOneTimePasscode()
assertTrue(result.isSuccess)
}
@Test
fun `requestOtp failure should return Failure`() = runTest {
val response = MockResponse().setResponseCode(400)
server.enqueue(response)
val result = service.requestOneTimePasscode()
assertTrue(result.isFailure)
}
@Test
fun `verifyOtp success should return Success`() = runTest {
val response = MockResponse().setResponseCode(200)
server.enqueue(response)
val result = service.verifyOneTimePasscode("passcode")
assertTrue(result.isSuccess)
}
@Test
fun `verifyOtp failure should return Failure`() = runTest {
val response = MockResponse().setResponseCode(400)
server.enqueue(response)
val result = service.verifyOneTimePasscode("passcode")
assertTrue(result.isFailure)
}
@Test
fun `requestPasswordHint success should return Success`() = runTest {
val email = "test@example.com"

View file

@ -63,6 +63,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
@ -70,6 +71,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
@ -4045,6 +4047,54 @@ class AuthRepositoryTest {
verify { userLogoutManager.logout(userId = userId) }
}
@Test
fun `requestOneTimePasscode with success response should return Success`() = runTest {
coEvery {
accountsService.requestOneTimePasscode()
} returns Unit.asSuccess()
val result = repository.requestOneTimePasscode()
assertEquals(RequestOtpResult.Success, result)
}
@Test
fun `requestOneTimePasscode with error response should return Error`() = runTest {
val errorMessage = "Error message"
coEvery {
accountsService.requestOneTimePasscode()
} returns Throwable(errorMessage).asFailure()
val result = repository.requestOneTimePasscode()
assertEquals(RequestOtpResult.Error(errorMessage), result)
}
@Test
fun `verifyOneTimePasscode with success response should return Verified result`() = runTest {
val passcode = "passcode"
coEvery {
accountsService.verifyOneTimePasscode(passcode)
} returns Unit.asSuccess()
val result = repository.verifyOneTimePasscode(passcode)
assertEquals(VerifyOtpResult.Verified, result)
}
@Test
fun `verifyOneTimePasscode with error response should return NotVerified result`() = runTest {
val errorMessage = "Error message"
val passcode = "passcode"
coEvery {
accountsService.verifyOneTimePasscode(passcode)
} returns Throwable(errorMessage).asFailure()
val result = repository.verifyOneTimePasscode(passcode)
assertEquals(VerifyOtpResult.NotVerified(errorMessage), result)
}
@Test
fun `resendVerificationCodeEmail uses cached request data to make api call`() = runTest {
// Attempt a normal login with a two factor error first, so that the necessary