mirror of
https://github.com/bitwarden/android.git
synced 2026-05-10 16:45:43 -05:00
Compare commits
4 Commits
QA-1126b/a
...
pm-11649/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fd3db1804 | ||
|
|
087e97335d | ||
|
|
5b438e4535 | ||
|
|
15f72d8ba3 |
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterFinishRequ
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
@@ -79,4 +80,9 @@ interface UnauthenticatedIdentityApi {
|
||||
suspend fun sendVerificationEmail(
|
||||
@Body body: SendVerificationEmailRequestJson,
|
||||
): Result<JsonPrimitive?>
|
||||
|
||||
@POST("/accounts/register/verification-email-clicked")
|
||||
suspend fun verifyEmailToken(
|
||||
@Body body: VerifyEmailTokenRequestJson,
|
||||
): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request body to verify the email token.
|
||||
*
|
||||
* @param email The email being used to create the account.
|
||||
* @param emailVerificationToken The token used to verify the email.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailTokenRequestJson(
|
||||
@SerialName("email")
|
||||
val email: String,
|
||||
|
||||
@SerialName("emailVerificationToken")
|
||||
val emailVerificationToken: String?,
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models response bodies for the verification of the email token.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VerifyEmailTokenResponseJson {
|
||||
/**
|
||||
* Models a successful json response of the verify email request.
|
||||
*/
|
||||
@Serializable
|
||||
data object Success : VerifyEmailTokenResponseJson()
|
||||
|
||||
/**
|
||||
* Represents the json body of an invalid register request.
|
||||
*
|
||||
* @param validationErrors a map where each value is a list of error messages for each key.
|
||||
* The values in the array should be used for display to the user, since the keys tend to come
|
||||
* back as nonsense. (eg: empty string key)
|
||||
*/
|
||||
@Serializable
|
||||
data class Invalid(
|
||||
@SerialName("message")
|
||||
private val invalidMessage: String? = null,
|
||||
|
||||
@SerialName("Message")
|
||||
private val errorMessage: String? = null,
|
||||
|
||||
@SerialName("validationErrors")
|
||||
val validationErrors: Map<String, List<String>>?,
|
||||
) : VerifyEmailTokenResponseJson() {
|
||||
/**
|
||||
* A generic error message.
|
||||
*/
|
||||
val message: String? get() = invalidMessage ?: errorMessage
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying identity endpoints.
|
||||
@@ -72,4 +74,11 @@ interface IdentityService {
|
||||
* Register a new account to Bitwarden using email verification flow.
|
||||
*/
|
||||
suspend fun registerFinish(body: RegisterFinishRequestJson): Result<RegisterResponseJson>
|
||||
|
||||
/**
|
||||
* Verifies that the token received by email is still valid
|
||||
*/
|
||||
suspend fun verifyEmailToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson>
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJso
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEmailRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
@@ -127,4 +129,22 @@ class IdentityServiceImpl(
|
||||
.sendVerificationEmail(body = body)
|
||||
.map { it?.content }
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun verifyEmailToken(
|
||||
body: VerifyEmailTokenRequestJson,
|
||||
): Result<VerifyEmailTokenResponseJson> {
|
||||
return unauthenticatedIdentityApi
|
||||
.verifyEmailToken(body = body)
|
||||
.map { VerifyEmailTokenResponseJson.Success }
|
||||
.recoverCatching { throwable ->
|
||||
val bitwardenError = throwable.toBitwardenError()
|
||||
bitwardenError
|
||||
.parseErrorBodyOrNull<VerifyEmailTokenResponseJson.Invalid>(
|
||||
codes = (400..499).toList(),
|
||||
json = json,
|
||||
)
|
||||
?: throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,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.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyEmailTokenResult
|
||||
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
|
||||
@@ -377,4 +378,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
||||
name: String,
|
||||
receiveMarketingEmails: Boolean,
|
||||
): SendVerificationEmailResult
|
||||
|
||||
/**
|
||||
* Verifies token received by email.
|
||||
*/
|
||||
suspend fun verifyEmailToken(
|
||||
email: String,
|
||||
emailVerificationToken: String,
|
||||
): VerifyEmailTokenResult
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SetPasswordRequest
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
@@ -64,6 +65,7 @@ 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.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyEmailTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
@@ -1256,6 +1258,32 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun verifyEmailToken(
|
||||
email: String,
|
||||
emailVerificationToken: String,
|
||||
): VerifyEmailTokenResult =
|
||||
identityService
|
||||
.verifyEmailToken(
|
||||
VerifyEmailTokenRequestJson(
|
||||
email = email,
|
||||
emailVerificationToken = emailVerificationToken,
|
||||
),
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
VerifyEmailTokenResult.Verified
|
||||
},
|
||||
onFailure = {
|
||||
// Server sends a message if the link is expired, this is the only way
|
||||
// to check if the token is expired
|
||||
if (it.message?.contains("Expired link", ignoreCase = true) == true) {
|
||||
return VerifyEmailTokenResult.LinkExpired
|
||||
} else {
|
||||
return VerifyEmailTokenResult.Error
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of verifying the email token.
|
||||
*/
|
||||
sealed class VerifyEmailTokenResult {
|
||||
|
||||
/**
|
||||
* Represents a successful verification of email token.
|
||||
*/
|
||||
data object Verified : VerifyEmailTokenResult()
|
||||
|
||||
/**
|
||||
* Represents an expired email verification token.
|
||||
*/
|
||||
data object LinkExpired : VerifyEmailTokenResult()
|
||||
|
||||
/**
|
||||
* There was an error verifying email token.
|
||||
*/
|
||||
data object Error : VerifyEmailTokenResult()
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.SendVerificationEm
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
@@ -369,6 +371,35 @@ class IdentityServiceTest : BaseServiceTest() {
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken should return null when response is empty success`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(200))
|
||||
val result = identityService.verifyEmailToken(VERIFY_EMAIL_REQUEST)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken should return an error when response is an error`() = runTest {
|
||||
server.enqueue(MockResponse().setResponseCode(400))
|
||||
val result = identityService.verifyEmailToken(VERIFY_EMAIL_REQUEST)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken failure with expired link should return Invalid`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400).setBody(EXPIRED_LINK_RESPONSE_JSON)
|
||||
server.enqueue(response)
|
||||
val result = identityService.verifyEmailToken(VERIFY_EMAIL_REQUEST)
|
||||
assertEquals(
|
||||
@Suppress("MaxLineLength")
|
||||
VerifyEmailTokenResponseJson.Invalid(
|
||||
errorMessage = "Expired link. Please restart registration or try logging in. You may already have an account.",
|
||||
validationErrors = null,
|
||||
),
|
||||
result.getOrThrow(),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||
private const val REFRESH_TOKEN = "refreshToken"
|
||||
@@ -569,6 +600,14 @@ private const val CAPTCHA_BYPASS_TOKEN_RESPONSE_JSON = """
|
||||
{
|
||||
"captchaBypassToken": "mock_token"
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
private const val EXPIRED_LINK_RESPONSE_JSON = """
|
||||
{
|
||||
"Object": "error",
|
||||
"Message": "Expired link. Please restart registration or try logging in. You may already have an account."
|
||||
}
|
||||
"""
|
||||
|
||||
private val INVALID_LOGIN = GetTokenResponseJson.Invalid(
|
||||
@@ -582,3 +621,8 @@ private val SEND_VERIFICATION_EMAIL_REQUEST = SendVerificationEmailRequestJson(
|
||||
name = "Name Example",
|
||||
receiveMarketingEmails = true,
|
||||
)
|
||||
|
||||
private val VERIFY_EMAIL_REQUEST = VerifyEmailTokenRequestJson(
|
||||
email = "email@example.com",
|
||||
emailVerificationToken = "mock_token",
|
||||
)
|
||||
|
||||
@@ -42,6 +42,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserD
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.VerifyEmailTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.DevicesService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.HaveIBeenPwnedService
|
||||
@@ -81,6 +83,7 @@ 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.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyEmailTokenResult
|
||||
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
|
||||
@@ -5999,6 +6002,90 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken success should return success`() = runTest {
|
||||
coEvery {
|
||||
identityService.verifyEmailToken(
|
||||
VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns VerifyEmailTokenResponseJson.Success.asSuccess()
|
||||
|
||||
val result = repository.verifyEmailToken(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResult.Verified,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken failure with expired link message should return ExpiredLink`() = runTest {
|
||||
coEvery {
|
||||
identityService.verifyEmailToken(
|
||||
VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns Throwable("Expired link").asFailure()
|
||||
|
||||
val result = repository.verifyEmailToken(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResult.LinkExpired,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken generic failure should return error`() = runTest {
|
||||
coEvery {
|
||||
identityService.verifyEmailToken(
|
||||
VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns Throwable("generic fail").asFailure()
|
||||
|
||||
val result = repository.verifyEmailToken(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResult.Error,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verifyEmailToken failure without message should return error`() = runTest {
|
||||
coEvery {
|
||||
identityService.verifyEmailToken(
|
||||
VerifyEmailTokenRequestJson(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
),
|
||||
)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
val result = repository.verifyEmailToken(
|
||||
email = EMAIL,
|
||||
emailVerificationToken = EMAIL_VERIFICATION_TOKEN,
|
||||
)
|
||||
assertEquals(
|
||||
VerifyEmailTokenResult.Error,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNIQUE_APP_ID = "testUniqueAppId"
|
||||
private const val NAME = "Example Name"
|
||||
|
||||
Reference in New Issue
Block a user