[PM-11649] Add service call to verify email to identity service and auth repository.

This commit is contained in:
André Bispo
2024-09-04 19:53:19 +01:00
parent 36f13e44a3
commit 15f72d8ba3
8 changed files with 153 additions and 0 deletions

View File

@@ -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>
}

View File

@@ -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?,
)

View File

@@ -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
}
}

View File

@@ -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>
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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()
}