diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt index 547add0878..343a86f6ae 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/UnauthenticatedIdentityApi.kt @@ -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 + + @POST("/accounts/register/verification-email-clicked") + suspend fun verifyEmailToken( + @Body body: VerifyEmailTokenRequestJson, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenRequestJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenRequestJson.kt new file mode 100644 index 0000000000..09cec2936d --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenRequestJson.kt @@ -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?, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenResponseJson.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenResponseJson.kt new file mode 100644 index 0000000000..30c3e40ba5 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/VerifyEmailTokenResponseJson.kt @@ -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>?, + ) : VerifyEmailTokenResponseJson() { + /** + * A generic error message. + */ + val message: String? get() = invalidMessage ?: errorMessage + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt index 1c2ada0dc4..4e4ac026d2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt @@ -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 + + /** + * Verifies that the token received by email is still valid + */ + suspend fun verifyEmailToken( + body: VerifyEmailTokenRequestJson, + ): Result } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt index 1132ac8b86..a31256c958 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt @@ -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 { + return unauthenticatedIdentityApi + .verifyEmailToken(body = body) + .map { VerifyEmailTokenResponseJson.Success } + .recoverCatching { throwable -> + val bitwardenError = throwable.toBitwardenError() + bitwardenError + .parseErrorBodyOrNull( + codes = (400..499).toList(), + json = json, + ) + ?: throw throwable + } + } } 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 0c77c36c97..c95b7f7068 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 @@ -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 } 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 ca05cb3b5b..df25de7c82 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 @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/VerifyEmailTokenResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/VerifyEmailTokenResult.kt new file mode 100644 index 0000000000..ded5b1da7c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/VerifyEmailTokenResult.kt @@ -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() +}