mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
Add requestOtp and verifyOtp API methods (#1275)
This commit is contained in:
parent
dc2a0d10b9
commit
ea01470d21
10 changed files with 202 additions and 0 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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> =
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue