diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 269eb5cf550..ac62a2639f9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -169,6 +169,7 @@
+
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
index c6bcf78fbea..4081c6a68c4 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt
@@ -14,6 +14,7 @@ import com.bitwarden.core.data.util.flatMap
import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf
import com.bitwarden.data.datasource.disk.ConfigDiskSource
+import com.bitwarden.data.repository.util.appLinksScheme
import com.bitwarden.data.repository.util.toEnvironmentUrls
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
import com.bitwarden.network.model.CreateAccountKeysResponseJson
@@ -1573,6 +1574,7 @@ class AuthRepositoryImpl(
): LoginResult = identityService
.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
+ deeplinkScheme = environmentRepository.environment.environmentUrlData.appLinksScheme,
email = email,
authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtils.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtils.kt
index 56a1555767c..4aa97283691 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtils.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtils.kt
@@ -5,8 +5,7 @@ import android.net.Uri
import androidx.browser.auth.AuthTabIntent
import com.bitwarden.annotation.OmitFromCoverage
-private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
-private const val BITWARDEN_US_HOST: String = "bitwarden.com"
+private val BITWARDEN_HOSTS: List = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
private const val APP_LINK_SCHEME: String = "https"
private const val DEEPLINK_SCHEME: String = "bitwarden"
private const val CALLBACK: String = "duo-callback"
@@ -34,9 +33,7 @@ fun Intent.getDuoCallbackTokenResult(): DuoCallbackTokenResult? {
}
APP_LINK_SCHEME -> {
- if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
- localData.path == "/$CALLBACK"
- ) {
+ if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
localData.getDuoCallbackTokenResult()
} else {
null
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtils.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtils.kt
index 94113ce7899..13f64907bc3 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtils.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtils.kt
@@ -11,31 +11,31 @@ import java.net.URLEncoder
import java.security.MessageDigest
import java.util.Base64
-private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
-private const val BITWARDEN_US_HOST: String = "bitwarden.com"
+private val BITWARDEN_HOSTS: List = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
private const val APP_LINK_SCHEME: String = "https"
private const val DEEPLINK_SCHEME: String = "bitwarden"
private const val CALLBACK: String = "sso-callback"
-const val SSO_URI: String = "bitwarden://$CALLBACK"
-
/**
* Generates a URI for the SSO custom tab.
*
* @param identityBaseUrl The base URl for the identity service.
+ * @param redirectUrl The redirect URI used in the SSO request.
* @param organizationIdentifier The SSO organization identifier.
* @param token The prevalidated SSO token.
* @param state Random state used to verify the validity of the response.
* @param codeVerifier A random string used to generate the code challenge.
*/
+@Suppress("LongParameterList")
fun generateUriForSso(
identityBaseUrl: String,
+ redirectUrl: String,
organizationIdentifier: String,
token: String,
state: String,
codeVerifier: String,
): Uri {
- val redirectUri = URLEncoder.encode(SSO_URI, "UTF-8")
+ val redirectUri = URLEncoder.encode(redirectUrl, "UTF-8")
val encodedOrganizationIdentifier = URLEncoder.encode(organizationIdentifier, "UTF-8")
val encodedToken = URLEncoder.encode(token, "UTF-8")
@@ -81,9 +81,7 @@ fun Intent.getSsoCallbackResult(): SsoCallbackResult? {
}
APP_LINK_SCHEME -> {
- if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
- localData.path == "/$CALLBACK"
- ) {
+ if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
localData.getSsoCallbackResult()
} else {
null
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt
index 8bf85dadf08..eda4938b7bb 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt
@@ -8,17 +8,13 @@ import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
-import java.net.URLEncoder
import java.util.Base64
-private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
-private const val BITWARDEN_US_HOST: String = "bitwarden.com"
+private val BITWARDEN_HOSTS: List = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
private const val APP_LINK_SCHEME: String = "https"
private const val DEEPLINK_SCHEME: String = "bitwarden"
private const val CALLBACK: String = "webauthn-callback"
-private const val CALLBACK_URI = "bitwarden://$CALLBACK"
-
/**
* Retrieves an [WebAuthResult] from an [Intent]. There are three possible cases.
*
@@ -39,9 +35,7 @@ fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
}
APP_LINK_SCHEME -> {
- if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
- localData.path == "/$CALLBACK"
- ) {
+ if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
localData.getWebAuthResult()
} else {
null
@@ -79,29 +73,31 @@ private fun Uri?.getWebAuthResult(): WebAuthResult =
/**
* Generates a [Uri] to display a web authn challenge for Bitwarden authentication.
*/
+@Suppress("LongParameterList")
fun generateUriForWebAuth(
baseUrl: String,
+ callbackScheme: String,
data: JsonObject,
headerText: String,
buttonText: String,
returnButtonText: String,
): Uri {
val json = buildJsonObject {
- put(key = "callbackUri", value = CALLBACK_URI)
put(key = "data", value = data.toString())
put(key = "headerText", value = headerText)
put(key = "btnText", value = buttonText)
put(key = "btnReturnText", value = returnButtonText)
+ put(key = "mobile", value = true)
}
val base64Data = Base64
.getEncoder()
.encodeToString(json.toString().toByteArray(Charsets.UTF_8))
- val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
val url = baseUrl +
"/webauthn-mobile-connector.html" +
"?data=$base64Data" +
- "&parent=$parentParam" +
- "&v=2"
+ "&client=mobile" +
+ "&v=2" +
+ "&deeplinkScheme=$callbackScheme"
return url.toUri()
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt
new file mode 100644
index 00000000000..a27c9daf825
--- /dev/null
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensions.kt
@@ -0,0 +1,51 @@
+package com.x8bit.bitwarden.data.platform.util
+
+import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
+import com.bitwarden.data.repository.model.EnvironmentRegion
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
+
+/**
+ * Creates the appropriate Duo [AuthTabData] for the given [EnvironmentUrlDataJson].
+ */
+val EnvironmentUrlDataJson.duoAuthTabData: AuthTabData get() = authTabData(kind = "duo")
+
+/**
+ * Creates the appropriate WebAuthn [AuthTabData] for the given [EnvironmentUrlDataJson].
+ */
+val EnvironmentUrlDataJson.webAuthnAuthTabData: AuthTabData get() = authTabData(kind = "webauthn")
+
+/**
+ * Creates the appropriate SSO [AuthTabData] for the given [EnvironmentUrlDataJson].
+ */
+val EnvironmentUrlDataJson.ssoAuthTabData: AuthTabData get() = authTabData(kind = "sso")
+
+private fun EnvironmentUrlDataJson.authTabData(
+ kind: String,
+): AuthTabData = when (this.environmentRegion) {
+ EnvironmentRegion.UNITED_STATES -> {
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "$kind-callback",
+ )
+ }
+
+ EnvironmentRegion.EUROPEAN_UNION -> {
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.eu",
+ path = "$kind-callback",
+ )
+ }
+
+ EnvironmentRegion.INTERNAL -> {
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.pw",
+ path = "$kind-callback",
+ )
+ }
+
+ EnvironmentRegion.SELF_HOSTED -> {
+ AuthTabData.CustomScheme(
+ callbackUrl = "bitwarden://$kind-callback",
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt
index d7acc1d32ab..e7dbf2a2b88 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt
@@ -66,7 +66,7 @@ fun EnterpriseSignOnScreen(
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
intentManager.startAuthTab(
uri = event.uri,
- redirectScheme = event.scheme,
+ authTabData = event.authTabData,
launcher = authTabLaunchers.sso,
)
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt
index 6f16ed4ee4f..56a93fe689d 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.data.repository.util.baseIdentityUrl
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.ui.platform.base.BaseViewModel
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
@@ -14,11 +15,11 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
-import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
+import com.x8bit.bitwarden.data.platform.util.ssoAuthTabData
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
import com.x8bit.bitwarden.data.tools.generator.repository.utils.generateRandomString
@@ -208,7 +209,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
sendEvent(
EnterpriseSignOnEvent.NavigateToSsoLogin(
uri = action.uri,
- scheme = action.scheme,
+ authTabData = action.authTabData,
),
)
}
@@ -342,14 +343,13 @@ class EnterpriseSignOnViewModel @Inject constructor(
if (ssoCallbackResult.state == ssoData.state) {
showLoading()
viewModelScope.launch {
- val result = authRepository
- .login(
- email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
- ssoCode = ssoCallbackResult.code,
- ssoCodeVerifier = ssoData.codeVerifier,
- ssoRedirectUri = SSO_URI,
- organizationIdentifier = state.orgIdentifierInput,
- )
+ val result = authRepository.login(
+ email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
+ ssoCode = ssoCallbackResult.code,
+ ssoCodeVerifier = ssoData.codeVerifier,
+ ssoRedirectUri = ssoData.redirectUri,
+ organizationIdentifier = state.orgIdentifierInput,
+ )
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
}
} else {
@@ -385,18 +385,22 @@ class EnterpriseSignOnViewModel @Inject constructor(
) {
val codeVerifier = generatorRepository.generateRandomString(RANDOM_STRING_LENGTH)
+ val environmentData = environmentRepository.environment.environmentUrlData
+ val authTabData = environmentData.ssoAuthTabData
// Save this for later so that we can validate the SSO callback response
val generatedSsoState = generatorRepository
.generateRandomString(RANDOM_STRING_LENGTH)
.also {
ssoResponseData = SsoResponseData(
+ redirectUri = authTabData.callbackUrl,
codeVerifier = codeVerifier,
state = it,
)
}
val uri = generateUriForSso(
- identityBaseUrl = environmentRepository.environment.environmentUrlData.baseIdentityUrl,
+ identityBaseUrl = environmentData.baseIdentityUrl,
+ redirectUrl = authTabData.callbackUrl,
organizationIdentifier = organizationIdentifier,
token = prevalidateSsoResult.token,
state = generatedSsoState,
@@ -408,7 +412,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
sendAction(
EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult(
uri = uri,
- scheme = "bitwarden",
+ authTabData = authTabData,
),
)
}
@@ -518,7 +522,7 @@ sealed class EnterpriseSignOnEvent {
*/
data class NavigateToSsoLogin(
val uri: Uri,
- val scheme: String,
+ val authTabData: AuthTabData,
) : EnterpriseSignOnEvent()
/**
@@ -580,7 +584,10 @@ sealed class EnterpriseSignOnAction {
/**
* A [uri] has been generated to request an SSO result.
*/
- data class OnGenerateUriForSsoResult(val uri: Uri, val scheme: String) : Internal()
+ data class OnGenerateUriForSsoResult(
+ val uri: Uri,
+ val authTabData: AuthTabData,
+ ) : Internal()
/**
* A login result has been received.
@@ -612,6 +619,7 @@ sealed class EnterpriseSignOnAction {
/**
* Data needed by the SSO flow to verify and continue the process after receiving a response.
*
+ * @property redirectUri The redirect URI used in the SSO request.
* @property state A "state" maintained throughout the SSO process to verify that the response from
* the server is valid and matches what was originally sent in the request.
* @property codeVerifier A random string used to generate the code challenge for the initial SSO
@@ -619,6 +627,7 @@ sealed class EnterpriseSignOnAction {
*/
@Parcelize
data class SsoResponseData(
+ val redirectUri: String,
val state: String,
val codeVerifier: String,
) : Parcelable
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
index 071436edecb..00119e6e3c7 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt
@@ -104,7 +104,7 @@ fun TwoFactorLoginScreen(
is TwoFactorLoginEvent.NavigateToDuo -> {
intentManager.startAuthTab(
uri = event.uri,
- redirectScheme = event.scheme,
+ authTabData = event.authTabData,
launcher = authTabLaunchers.duo,
)
}
@@ -112,7 +112,7 @@ fun TwoFactorLoginScreen(
is TwoFactorLoginEvent.NavigateToWebAuth -> {
intentManager.startAuthTab(
uri = event.uri,
- redirectScheme = event.scheme,
+ authTabData = event.authTabData,
launcher = authTabLaunchers.webAuthn,
)
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
index 93a02ceba15..206186572d8 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt
@@ -15,6 +15,7 @@ import com.bitwarden.network.util.twoFactorDisplayEmail
import com.bitwarden.network.util.twoFactorDuoAuthUrl
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
@@ -26,6 +27,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForWebAuth
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
+import com.x8bit.bitwarden.data.platform.util.duoAuthTabData
+import com.x8bit.bitwarden.data.platform.util.webAuthnAuthTabData
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.button
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.imageRes
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.isContinueButtonEnabled
@@ -173,71 +176,15 @@ class TwoFactorLoginViewModel @Inject constructor(
}
/**
- * Navigates to the Duo webpage if appropriate, else processes the login.
+ * Navigates to the two-factor auth webpage if appropriate, else processes the login.
*/
- @Suppress("LongMethod")
private fun handleContinueButtonClick() {
when (state.authMethod) {
TwoFactorAuthMethod.DUO,
TwoFactorAuthMethod.DUO_ORGANIZATION,
- -> {
- val authUrl = authRepository.twoFactorResponse.twoFactorDuoAuthUrl
- // The url should not be empty unless the environment is somehow not supported.
- authUrl
- ?.let {
- sendEvent(
- event = TwoFactorLoginEvent.NavigateToDuo(
- uri = it.toUri(),
- scheme = "bitwarden",
- ),
- )
- }
- ?: mutableStateFlow.update {
- @Suppress("MaxLineLength")
- it.copy(
- dialogState = TwoFactorLoginState.DialogState.Error(
- title = BitwardenString.an_error_has_occurred.asText(),
- message = BitwardenString
- .error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance
- .asText(),
- ),
- )
- }
- }
-
- TwoFactorAuthMethod.WEB_AUTH -> {
- sendEvent(
- event = authRepository
- .twoFactorResponse
- ?.authMethodsData
- ?.get(TwoFactorAuthMethod.WEB_AUTH)
- ?.let {
- val uri = generateUriForWebAuth(
- baseUrl = environmentRepository
- .environment
- .environmentUrlData
- .baseWebVaultUrlOrDefault,
- data = it,
- headerText = resourceManager.getString(
- resId = BitwardenString.fido2_title,
- ),
- buttonText = resourceManager.getString(
- resId = BitwardenString.fido2_authenticate_web_authn,
- ),
- returnButtonText = resourceManager.getString(
- resId = BitwardenString.fido2_return_to_app,
- ),
- )
- TwoFactorLoginEvent.NavigateToWebAuth(uri = uri, scheme = "bitwarden")
- }
- ?: TwoFactorLoginEvent.ShowSnackbar(
- message = BitwardenString
- .there_was_an_error_starting_web_authn_two_factor_authentication
- .asText(),
- ),
- )
- }
+ -> handleDuoContinueButtonClick()
+ TwoFactorAuthMethod.WEB_AUTH -> handleWebAuthnContinueButtonClick()
TwoFactorAuthMethod.AUTHENTICATOR_APP,
TwoFactorAuthMethod.EMAIL,
TwoFactorAuthMethod.YUBI_KEY,
@@ -248,6 +195,73 @@ class TwoFactorLoginViewModel @Inject constructor(
}
}
+ /**
+ * Navigates to the Duo webpage if appropriate, or displays the error dialog.
+ */
+ private fun handleDuoContinueButtonClick() {
+ // The url should not be empty unless the environment is somehow not supported.
+ authRepository
+ .twoFactorResponse
+ .twoFactorDuoAuthUrl
+ ?.toUri()
+ ?.let {
+ val environmentData = environmentRepository.environment.environmentUrlData
+ sendEvent(
+ event = TwoFactorLoginEvent.NavigateToDuo(
+ uri = it,
+ authTabData = environmentData.duoAuthTabData,
+ ),
+ )
+ }
+ ?: mutableStateFlow.update {
+ @Suppress("MaxLineLength")
+ it.copy(
+ dialogState = TwoFactorLoginState.DialogState.Error(
+ title = BitwardenString.an_error_has_occurred.asText(),
+ message = BitwardenString
+ .error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance
+ .asText(),
+ ),
+ )
+ }
+ }
+
+ /**
+ * Navigates to the Web Authn webpage if appropriate, or displays the error snackbar.
+ */
+ private fun handleWebAuthnContinueButtonClick() {
+ sendEvent(
+ event = authRepository
+ .twoFactorResponse
+ ?.authMethodsData
+ ?.get(TwoFactorAuthMethod.WEB_AUTH)
+ ?.let {
+ val environmentData = environmentRepository.environment.environmentUrlData
+ val authTabData = environmentData.webAuthnAuthTabData
+ val uri = generateUriForWebAuth(
+ baseUrl = environmentData.baseWebVaultUrlOrDefault,
+ callbackScheme = authTabData.callbackScheme,
+ data = it,
+ headerText = resourceManager.getString(
+ resId = BitwardenString.fido2_title,
+ ),
+ buttonText = resourceManager.getString(
+ resId = BitwardenString.fido2_authenticate_web_authn,
+ ),
+ returnButtonText = resourceManager.getString(
+ resId = BitwardenString.fido2_return_to_app,
+ ),
+ )
+ TwoFactorLoginEvent.NavigateToWebAuth(uri = uri, authTabData = authTabData)
+ }
+ ?: TwoFactorLoginEvent.ShowSnackbar(
+ message = BitwardenString
+ .there_was_an_error_starting_web_authn_two_factor_authentication
+ .asText(),
+ ),
+ )
+ }
+
/**
* Dismiss the view.
*/
@@ -677,12 +691,18 @@ sealed class TwoFactorLoginEvent {
/**
* Navigates to the Duo 2-factor authentication screen.
*/
- data class NavigateToDuo(val uri: Uri, val scheme: String) : TwoFactorLoginEvent()
+ data class NavigateToDuo(
+ val uri: Uri,
+ val authTabData: AuthTabData,
+ ) : TwoFactorLoginEvent()
/**
* Navigates to the WebAuth authentication screen.
*/
- data class NavigateToWebAuth(val uri: Uri, val scheme: String) : TwoFactorLoginEvent()
+ data class NavigateToWebAuth(
+ val uri: Uri,
+ val authTabData: AuthTabData,
+ ) : TwoFactorLoginEvent()
/**
* Navigates to the recovery code help page.
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
index 4bfa97937fb..939fb858534 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
@@ -190,11 +190,9 @@ class AuthRepositoryTest {
}
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
- private val fakeEnvironmentRepository =
- FakeEnvironmentRepository()
- .apply {
- environment = Environment.Us
- }
+ private val fakeEnvironmentRepository = FakeEnvironmentRepository().apply {
+ environment = Environment.Us
+ }
private val settingsRepository: SettingsRepository = mockk {
every { setDefaultsIfNecessary(any()) } just runs
every { hasUserLoggedInOrCreatedAccount = true } just runs
@@ -411,6 +409,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -493,6 +492,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -1761,6 +1761,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns error.asFailure()
val result = repository.login(email = EMAIL, password = PASSWORD)
@@ -1775,6 +1776,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -1795,6 +1797,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns RuntimeException().asFailure()
val result = repository.login(email = EMAIL, password = PASSWORD)
@@ -1818,6 +1821,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns SSLHandshakeException("error").asFailure()
val result = repository.login(email = EMAIL, password = PASSWORD)
@@ -1851,6 +1855,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.Invalid(
@@ -1872,6 +1877,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -1891,6 +1897,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.Invalid(
@@ -1924,6 +1931,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -1987,6 +1995,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -2043,6 +2052,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -2100,6 +2110,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -2149,6 +2160,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
val error = Throwable("Fail")
@@ -2219,6 +2231,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
@@ -2284,6 +2297,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -2312,6 +2326,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
@@ -2370,6 +2385,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -2443,6 +2459,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -2494,6 +2511,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -2522,6 +2540,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -2539,6 +2558,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -2558,6 +2578,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -2574,6 +2595,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = TWO_FACTOR_DATA,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -2646,6 +2668,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns twoFactorResponse.asSuccess()
@@ -2664,6 +2687,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -2680,6 +2704,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = TWO_FACTOR_DATA,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
val error = Throwable("Fail")
@@ -2755,6 +2780,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = rememberedTwoFactorData,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -2815,6 +2841,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = rememberedTwoFactorData,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -2883,6 +2910,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.Invalid(
@@ -2905,6 +2933,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -2921,6 +2950,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns error.asFailure()
val result = repository.login(
@@ -2942,6 +2972,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -2958,6 +2989,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.Invalid(
@@ -2989,6 +3021,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -3007,6 +3040,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -3076,6 +3110,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
vaultRepository.unlockVault(
@@ -3131,6 +3166,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -3200,6 +3236,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
vaultRepository.unlockVault(
@@ -3252,6 +3289,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -3287,6 +3325,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -3304,6 +3343,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -3330,6 +3370,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -3347,6 +3388,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = TWO_FACTOR_DATA,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -3414,6 +3456,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns error.asFailure()
val result = repository.login(
@@ -3434,6 +3477,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -3449,6 +3493,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.Invalid(
@@ -3476,6 +3521,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -3494,6 +3540,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -3533,6 +3580,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
settingsRepository.storeUserHasLoggedInValue(userId = USER_ID_1)
@@ -3568,6 +3616,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -3598,6 +3647,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
}
@@ -3630,6 +3680,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -3666,6 +3717,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
keyConnectorManager.getMasterKeyFromKeyConnector(
url = keyConnectorUrl,
@@ -3699,6 +3751,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -3753,6 +3806,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
keyConnectorManager.getMasterKeyFromKeyConnector(
url = keyConnectorUrl,
@@ -3809,6 +3863,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -3863,6 +3918,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
keyConnectorManager.getMasterKeyFromKeyConnector(
url = keyConnectorUrl,
@@ -3918,6 +3974,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -3963,6 +4020,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
keyConnectorManager.migrateNewUserToKeyConnector(
url = keyConnectorUrl,
@@ -4007,6 +4065,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -4070,6 +4129,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
keyConnectorManager.migrateNewUserToKeyConnector(
url = keyConnectorUrl,
@@ -4129,6 +4189,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
every {
@@ -4180,6 +4241,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4246,6 +4308,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
}
@@ -4270,6 +4333,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4338,6 +4402,7 @@ class AuthRepositoryTest {
accessCode = DEVICE_ACCESS_CODE,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
vaultRepository.unlockVault(
@@ -4396,6 +4461,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4430,6 +4496,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
}
@@ -4492,6 +4559,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4526,6 +4594,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -4617,6 +4686,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4650,6 +4720,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.unlockVault(
accountCryptographicState = createWrappedAccountCryptographicState(
@@ -4702,6 +4773,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4743,6 +4815,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
}
@@ -4770,6 +4843,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -4804,6 +4878,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
}
@@ -4821,6 +4896,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -4847,6 +4923,7 @@ class AuthRepositoryTest {
ssoRedirectUri = SSO_REDIRECT_URI,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -4864,6 +4941,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = TWO_FACTOR_DATA,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4909,6 +4987,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = rememberedTwoFactorData,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery { vaultRepository.syncIfNecessary() } just runs
@@ -4949,6 +5028,7 @@ class AuthRepositoryTest {
),
uniqueAppId = UNIQUE_APP_ID,
twoFactorData = rememberedTwoFactorData,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
vaultRepository.syncIfNecessary()
}
@@ -6323,6 +6403,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -6342,6 +6423,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -6384,6 +6466,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns GetTokenResponseJson
.TwoFactorRequired(
@@ -6403,6 +6486,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
}
@@ -7245,6 +7329,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -7323,6 +7408,7 @@ class AuthRepositoryTest {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
} returns successResponse.asSuccess()
coEvery {
@@ -7445,6 +7531,7 @@ class AuthRepositoryTest {
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
+ private const val DEEPLINK_SCHEME = "https"
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val NAME = "Example Name"
private const val EMAIL = "test@bitwarden.com"
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtilsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtilsTest.kt
index 71fcbb6e7b0..d522cc4bc6d 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtilsTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtilsTest.kt
@@ -4,15 +4,16 @@ import android.content.Intent
import android.net.Uri
import io.mockk.every
import io.mockk.mockk
-import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Test
class SsoUtilsTest {
@Test
fun `generateUriForSso should generate the correct URI`() {
val identityBaseUrl = "https://identity.bitwarden.com"
+ val redirectUrl = "https://bitwarden.com/sso-callback"
val organizationIdentifier = "Test Organization"
val token = "Test Token"
val state = "test_state"
@@ -31,6 +32,7 @@ class SsoUtilsTest {
val uri = generateUriForSso(
identityBaseUrl = identityBaseUrl,
+ redirectUrl = redirectUrl,
organizationIdentifier = organizationIdentifier,
token = token,
state = state,
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt
index 7b0661a8576..fa48276d59e 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtilsTest.kt
@@ -2,20 +2,20 @@ package com.x8bit.bitwarden.data.auth.repository.util
import android.content.Intent
import android.net.Uri
-import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import io.mockk.every
import io.mockk.mockk
import kotlinx.serialization.json.JsonObject
-import org.junit.Assert.assertEquals
-import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
-class WebAuthUtilsTest : BitwardenComposeTest() {
+class WebAuthUtilsTest {
@Test
fun `generateUriForWebAuth should return valid Uri`() {
val baseUrl = "https://vault.bitwarden.com"
val actualUri = generateUriForWebAuth(
baseUrl = baseUrl,
+ callbackScheme = "https",
data = JsonObject(emptyMap()),
headerText = "header",
buttonText = "button",
@@ -26,8 +26,9 @@ class WebAuthUtilsTest : BitwardenComposeTest() {
"?data=eyJjYWxsYmFja1VyaSI6ImJpdHdhcmRlbjovL3dlYmF1dGhuLWNhbGxiYWNrIiwiZ" +
"GF0YSI6Int9IiwiaGVhZGVyVGV4dCI6ImhlYWRlciIsImJ0blRleHQiOiJidXR0b24iLCJi" +
"dG5SZXR1cm5UZXh0IjoicmV0dXJuQnV0dG9uIn0=" +
- "&parent=bitwarden%3A%2F%2Fwebauthn-callback" +
- "&v=2"
+ "&client=mobile" +
+ "&v=2" +
+ "&deeplinkScheme=https"
val expectedUri = Uri.parse(expectedUrl)
assertEquals(expectedUri, actualUri)
}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt
new file mode 100644
index 00000000000..4a146d2b636
--- /dev/null
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/util/EnvironmentUrlDataJsonExtensionsTest.kt
@@ -0,0 +1,119 @@
+package com.x8bit.bitwarden.data.platform.util
+
+import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class EnvironmentUrlDataJsonExtensionsTest {
+
+ @Test
+ fun `duoAuthTabData should return the correct AuthTabData for all environments`() {
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "duo-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_US.duoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.eu",
+ path = "duo-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_EU.duoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.pw",
+ path = "duo-callback",
+ ),
+ DEFAULT_INTERNAL_ENVIRONMENT_URL_DATA.duoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.CustomScheme(
+ callbackUrl = "bitwarden://duo-callback",
+ callbackScheme = "bitwarden",
+ ),
+ DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.duoAuthTabData,
+ )
+ }
+
+ @Test
+ fun `webAuthnAuthTabData should return the correct AuthTabData for all environments`() {
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "webauthn-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_US.webAuthnAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.eu",
+ path = "webauthn-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_EU.webAuthnAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.pw",
+ path = "webauthn-callback",
+ ),
+ DEFAULT_INTERNAL_ENVIRONMENT_URL_DATA.webAuthnAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.CustomScheme(
+ callbackUrl = "bitwarden://webauthn-callback",
+ callbackScheme = "bitwarden",
+ ),
+ DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.webAuthnAuthTabData,
+ )
+ }
+
+ @Test
+ fun `ssoAuthTabData should return the correct AuthTabData for all environments`() {
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "sso-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_US.ssoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.eu",
+ path = "sso-callback",
+ ),
+ EnvironmentUrlDataJson.DEFAULT_EU.ssoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.HttpsScheme(
+ host = "bitwarden.pw",
+ path = "sso-callback",
+ ),
+ DEFAULT_INTERNAL_ENVIRONMENT_URL_DATA.ssoAuthTabData,
+ )
+ assertEquals(
+ AuthTabData.CustomScheme(
+ callbackUrl = "bitwarden://sso-callback",
+ callbackScheme = "bitwarden",
+ ),
+ DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.ssoAuthTabData,
+ )
+ }
+}
+
+private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(
+ base = "base",
+ api = "api",
+ identity = "identity",
+ icon = "icon",
+ notifications = "notifications",
+ webVault = "webVault",
+ events = "events",
+)
+
+private val DEFAULT_INTERNAL_ENVIRONMENT_URL_DATA = DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.copy(
+ base = "qa.vault.bitwarden.pw",
+)
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt
index b5ac60270d9..4a81afb229f 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreenTest.kt
@@ -16,6 +16,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.assertNoDialogExists
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
@@ -45,7 +46,7 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
}
private val intentManager: IntentManager = mockk {
- every { startAuthTab(uri = any(), redirectScheme = any(), launcher = any()) } just runs
+ every { startAuthTab(uri = any(), authTabData = any(), launcher = any()) } just runs
}
@Before
@@ -114,12 +115,12 @@ class EnterpriseSignOnScreenTest : BitwardenComposeTest() {
@Test
fun `NavigateToSsoLogin should call startCustomTabsActivity`() {
val ssoUri = Uri.parse("https://identity.bitwarden.com/sso-test")
- val scheme = "bitwarden"
- mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSsoLogin(ssoUri, scheme))
+ val authTabData = mockk()
+ mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSsoLogin(ssoUri, authTabData))
verify(exactly = 1) {
intentManager.startAuthTab(
uri = ssoUri,
- redirectScheme = scheme,
+ authTabData = authTabData,
launcher = ssoLauncher,
)
}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt
index b79cfc90182..03100c4ff68 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt
@@ -8,6 +8,7 @@ import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.network.model.VerifiedOrganizationDomainSsoDetailsResponse
import com.bitwarden.ui.platform.base.BaseViewModelTest
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -163,7 +164,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
val ssoUri: Uri = mockk()
every {
- generateUriForSso(any(), any(), any(), any(), any())
+ generateUriForSso(any(), any(), any(), any(), any(), any())
} returns ssoUri
val viewModel = createViewModel(state)
@@ -186,7 +187,13 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
)
assertEquals(
- EnterpriseSignOnEvent.NavigateToSsoLogin(uri = ssoUri, scheme = "bitwarden"),
+ EnterpriseSignOnEvent.NavigateToSsoLogin(
+ uri = ssoUri,
+ authTabData = AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "sso-callback",
+ ),
+ ),
eventFlow.awaitItem(),
)
}
@@ -385,7 +392,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = orgIdentifier,
)
}
@@ -451,7 +458,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = orgIdentifier,
)
}
@@ -474,7 +481,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
)
val viewModel = createViewModel(
- ssoData = DEFAULT_SSO_DATA,
+ ssoData = DEFAULT_SSO_DATA.copy(redirectUri = "bitwarden://sso-callback"),
)
val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn")
@@ -548,7 +555,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
)
val viewModel = createViewModel(
- ssoData = DEFAULT_SSO_DATA,
+ ssoData = DEFAULT_SSO_DATA.copy(redirectUri = "bitwarden://sso-callback"),
)
val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn")
@@ -622,7 +629,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
)
val viewModel = createViewModel(
- ssoData = DEFAULT_SSO_DATA,
+ ssoData = DEFAULT_SSO_DATA.copy(redirectUri = "bitwarden://sso-callback"),
)
val ssoCallbackResult = SsoCallbackResult.Success(state = "abc", code = "lmn")
@@ -739,7 +746,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = orgIdentifier,
)
}
@@ -792,7 +799,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = "Bitwarden",
)
}
@@ -848,7 +855,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = "Bitwarden",
)
}
@@ -912,7 +919,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
email = "test@gmail.com",
ssoCode = "lmn",
ssoCodeVerifier = "def",
- ssoRedirectUri = "bitwarden://sso-callback",
+ ssoRedirectUri = "https://bitwarden.com/sso-callback",
organizationIdentifier = orgIdentifier,
)
}
@@ -1269,6 +1276,7 @@ private val DEFAULT_STATE = EnterpriseSignOnState(
orgIdentifierInput = "",
)
private val DEFAULT_SSO_DATA = SsoResponseData(
+ redirectUri = "https://bitwarden.com/sso-callback",
state = "abc",
codeVerifier = "def",
)
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
index a88f0eed279..c91c3761f8e 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreenTest.kt
@@ -19,6 +19,7 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.IntentManager
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.manager.nfc.NfcManager
@@ -39,7 +40,7 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
private val webAuthnLauncher: ActivityResultLauncher = mockk()
private val intentManager = mockk {
every { launchUri(uri = any()) } just runs
- every { startAuthTab(uri = any(), redirectScheme = any(), launcher = any()) } just runs
+ every { startAuthTab(uri = any(), authTabData = any(), launcher = any()) } just runs
}
private val nfcManager: NfcManager = mockk {
every { start() } just runs
@@ -283,12 +284,12 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
@Test
fun `NavigateToDuo should call intentManager startAuthTab`() {
val mockUri = mockk()
- val scheme = "bitwarden"
- mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToDuo(mockUri, scheme))
+ val authTabData = mockk()
+ mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToDuo(mockUri, authTabData))
verify(exactly = 1) {
intentManager.startAuthTab(
uri = mockUri,
- redirectScheme = scheme,
+ authTabData = authTabData,
launcher = duoLauncher,
)
}
@@ -297,12 +298,12 @@ class TwoFactorLoginScreenTest : BitwardenComposeTest() {
@Test
fun `NavigateToWebAuth should call intentManager startCustomTabsActivity`() {
val mockUri = mockk()
- val scheme = "bitwarden"
- mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToWebAuth(mockUri, scheme))
+ val authTabData = mockk()
+ mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToWebAuth(mockUri, authTabData))
verify(exactly = 1) {
intentManager.startAuthTab(
uri = mockUri,
- redirectScheme = scheme,
+ authTabData = authTabData,
launcher = webAuthnLauncher,
)
}
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
index f27318fc74a..162decd964e 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt
@@ -6,11 +6,13 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.model.Environment
+import com.bitwarden.data.repository.util.appLinksScheme
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
import com.bitwarden.network.model.GetTokenResponseJson
import com.bitwarden.network.model.TwoFactorAuthMethod
import com.bitwarden.network.model.TwoFactorDataModel
import com.bitwarden.ui.platform.base.BaseViewModelTest
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@@ -28,7 +30,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
-import io.mockk.verify
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
@@ -418,22 +419,25 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
)
every { authRepository.twoFactorResponse } returns response
val mockkUri = mockk()
+ every { "bitwarden.com".toUri() } returns mockkUri
val viewModel = createViewModel(
state = DEFAULT_STATE.copy(
authMethod = TwoFactorAuthMethod.DUO,
),
)
- every { Uri.parse("bitwarden.com") } returns mockkUri
viewModel.eventFlow.test {
viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
assertEquals(
- TwoFactorLoginEvent.NavigateToDuo(uri = mockkUri, scheme = "bitwarden"),
+ TwoFactorLoginEvent.NavigateToDuo(
+ uri = mockkUri,
+ authTabData = AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "duo-callback",
+ ),
+ ),
awaitItem(),
)
}
- verify {
- Uri.parse("bitwarden.com")
- }
}
@Suppress("MaxLineLength")
@@ -500,6 +504,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
every {
generateUriForWebAuth(
baseUrl = Environment.Us.environmentUrlData.baseWebVaultUrlOrDefault,
+ callbackScheme = Environment.Us.environmentUrlData.appLinksScheme,
data = data,
headerText = headerText,
buttonText = buttonText,
@@ -512,7 +517,13 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick)
assertEquals(
- TwoFactorLoginEvent.NavigateToWebAuth(uri = mockkUri, scheme = "bitwarden"),
+ TwoFactorLoginEvent.NavigateToWebAuth(
+ uri = mockkUri,
+ authTabData = AuthTabData.HttpsScheme(
+ host = "bitwarden.com",
+ path = "webauthn-callback",
+ ),
+ ),
awaitItem(),
)
}
diff --git a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt
index 140236a2a64..9ee4f12a235 100644
--- a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt
+++ b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/model/EnvironmentUrlDataJson.kt
@@ -49,11 +49,22 @@ data class EnvironmentUrlDataJson(
get() = when (base) {
DEFAULT_US.base -> EnvironmentRegion.UNITED_STATES
DEFAULT_EU.base -> EnvironmentRegion.EUROPEAN_UNION
- else -> EnvironmentRegion.SELF_HOSTED
+ else -> {
+ if (base.contains(BITWARDEN_INTERNAL_DOMAIN)) {
+ EnvironmentRegion.INTERNAL
+ } else {
+ EnvironmentRegion.SELF_HOSTED
+ }
+ }
}
@Suppress("UndocumentedPublicClass")
companion object {
+ /**
+ * The domain used for internal Bitwarden environments.
+ */
+ private const val BITWARDEN_INTERNAL_DOMAIN: String = "bitwarden.pw"
+
/**
* Default [EnvironmentUrlDataJson] for the US region.
*/
diff --git a/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt b/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt
index 250eb979603..a24f6d64c6e 100644
--- a/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt
+++ b/data/src/main/kotlin/com/bitwarden/data/repository/model/EnvironmentRegion.kt
@@ -6,5 +6,6 @@ package com.bitwarden.data.repository.model
enum class EnvironmentRegion {
UNITED_STATES,
EUROPEAN_UNION,
+ INTERNAL,
SELF_HOSTED,
}
diff --git a/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt b/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt
index ecc0a1a1ae2..ecb47ad9253 100644
--- a/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt
+++ b/data/src/main/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlDataJsonExtensions.kt
@@ -24,13 +24,28 @@ val EnvironmentUrlDataJson.baseApiUrl: String
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_API_URL
EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_API_URL
- EnvironmentRegion.SELF_HOSTED -> {
+ EnvironmentRegion.INTERNAL,
+ EnvironmentRegion.SELF_HOSTED,
+ -> {
this.api.sanitizeUrl
?: this.base.sanitizeUrl?.let { "$it/api" }
?: DEFAULT_US_API_URL
}
}
+/**
+ * Returns the scheme used for app-links within the app.
+ */
+val EnvironmentUrlDataJson.appLinksScheme: String
+ get() = when (this.environmentRegion) {
+ EnvironmentRegion.UNITED_STATES,
+ EnvironmentRegion.EUROPEAN_UNION,
+ EnvironmentRegion.INTERNAL,
+ -> "https"
+
+ EnvironmentRegion.SELF_HOSTED -> "bitwarden"
+ }
+
/**
* Returns the base events URL or the default value if one is not present.
*/
@@ -38,7 +53,9 @@ val EnvironmentUrlDataJson.baseEventsUrl: String
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_EVENTS_URL
EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_EVENTS_URL
- EnvironmentRegion.SELF_HOSTED -> {
+ EnvironmentRegion.INTERNAL,
+ EnvironmentRegion.SELF_HOSTED,
+ -> {
this.events.sanitizeUrl
?: this.base.sanitizeUrl?.let { "$it/events" }
?: DEFAULT_US_EVENTS_URL
@@ -52,7 +69,9 @@ val EnvironmentUrlDataJson.baseIdentityUrl: String
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_IDENTITY_URL
EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_IDENTITY_URL
- EnvironmentRegion.SELF_HOSTED -> {
+ EnvironmentRegion.INTERNAL,
+ EnvironmentRegion.SELF_HOSTED,
+ -> {
this.identity.sanitizeUrl
?: this.base.sanitizeUrl?.let { "$it/identity" }
?: DEFAULT_US_IDENTITY_URL
@@ -68,7 +87,9 @@ val EnvironmentUrlDataJson.baseWebVaultUrlOrNull: String?
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_WEB_VAULT_URL
EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_WEB_VAULT_URL
- EnvironmentRegion.SELF_HOSTED -> this.webVault.sanitizeUrl ?: this.base.sanitizeUrl
+ EnvironmentRegion.INTERNAL,
+ EnvironmentRegion.SELF_HOSTED,
+ -> this.webVault.sanitizeUrl ?: this.base.sanitizeUrl
}
/**
@@ -86,6 +107,7 @@ val EnvironmentUrlDataJson.baseWebSendUrl: String
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_WEB_SEND_URL
EnvironmentRegion.EUROPEAN_UNION,
+ EnvironmentRegion.INTERNAL,
EnvironmentRegion.SELF_HOSTED,
-> this.baseWebVaultUrlOrNull?.let { "$it/#/send/" } ?: DEFAULT_US_WEB_SEND_URL
}
@@ -106,7 +128,9 @@ val EnvironmentUrlDataJson.baseIconUrl: String
get() = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> DEFAULT_US_ICON_URL
EnvironmentRegion.EUROPEAN_UNION -> DEFAULT_EU_ICON_URL
- EnvironmentRegion.SELF_HOSTED -> {
+ EnvironmentRegion.INTERNAL,
+ EnvironmentRegion.SELF_HOSTED,
+ -> {
this.icon.sanitizeUrl
?: this.base.sanitizeUrl?.let { "$it/icons" }
?: DEFAULT_US_ICON_URL
diff --git a/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt b/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt
index 6f6a4a982a6..9a171732af9 100644
--- a/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt
+++ b/data/src/test/kotlin/com/bitwarden/data/repository/util/EnvironmentUrlsDataJsonExtensionsTest.kt
@@ -336,6 +336,46 @@ class EnvironmentUrlsDataJsonExtensionsTest {
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.toBaseWebVaultImportUrl,
)
}
+
+ @Test
+ fun `appLinksScheme should return the correct scheme for US environment`() {
+ val expectedScheme = "https"
+
+ assertEquals(
+ expectedScheme,
+ EnvironmentUrlDataJson.DEFAULT_US.appLinksScheme,
+ )
+ }
+
+ @Test
+ fun `appLinksScheme should return the correct scheme for EU environment`() {
+ val expectedScheme = "https"
+
+ assertEquals(
+ expectedScheme,
+ EnvironmentUrlDataJson.DEFAULT_EU.appLinksScheme,
+ )
+ }
+
+ @Test
+ fun `appLinksScheme should return the correct scheme for internal environment`() {
+ val expectedScheme = "https"
+
+ assertEquals(
+ expectedScheme,
+ DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.copy(base = "qa.vault.bitwarden.pw").appLinksScheme,
+ )
+ }
+
+ @Test
+ fun `appLinksScheme should return the correct scheme for custom environment`() {
+ val expectedScheme = "bitwarden"
+
+ assertEquals(
+ expectedScheme,
+ DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.appLinksScheme,
+ )
+ }
}
private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(
diff --git a/network/src/main/kotlin/com/bitwarden/network/api/UnauthenticatedIdentityApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/UnauthenticatedIdentityApi.kt
index 501d453f4d8..5f191876f9c 100644
--- a/network/src/main/kotlin/com/bitwarden/network/api/UnauthenticatedIdentityApi.kt
+++ b/network/src/main/kotlin/com/bitwarden/network/api/UnauthenticatedIdentityApi.kt
@@ -45,6 +45,7 @@ internal interface UnauthenticatedIdentityApi {
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
@Field(value = "authRequest") authRequestId: String?,
@Field(value = "newDeviceOtp") newDeviceOtp: String?,
+ @Field(value = "deeplinkScheme") deeplinkScheme: String,
): NetworkResult
@GET("/sso/prevalidate")
diff --git a/network/src/main/kotlin/com/bitwarden/network/service/IdentityService.kt b/network/src/main/kotlin/com/bitwarden/network/service/IdentityService.kt
index ea47d2f2ea8..254c44f30d5 100644
--- a/network/src/main/kotlin/com/bitwarden/network/service/IdentityService.kt
+++ b/network/src/main/kotlin/com/bitwarden/network/service/IdentityService.kt
@@ -33,6 +33,7 @@ interface IdentityService {
* Make request to get an access token.
*
* @param uniqueAppId applications unique identifier.
+ * @param deeplinkScheme deeplink scheme to use for duo two-factor logins.
* @param email user's email address.
* @param authModel information necessary to authenticate with any
* of the available login methods.
@@ -41,6 +42,7 @@ interface IdentityService {
@Suppress("LongParameterList")
suspend fun getToken(
uniqueAppId: String,
+ deeplinkScheme: String,
email: String,
authModel: IdentityTokenAuthModel,
twoFactorData: TwoFactorDataModel? = null,
diff --git a/network/src/main/kotlin/com/bitwarden/network/service/IdentityServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/IdentityServiceImpl.kt
index 2223da2f171..65887be2bc3 100644
--- a/network/src/main/kotlin/com/bitwarden/network/service/IdentityServiceImpl.kt
+++ b/network/src/main/kotlin/com/bitwarden/network/service/IdentityServiceImpl.kt
@@ -54,6 +54,7 @@ internal class IdentityServiceImpl(
override suspend fun getToken(
uniqueAppId: String,
+ deeplinkScheme: String,
email: String,
authModel: IdentityTokenAuthModel,
twoFactorData: TwoFactorDataModel?,
@@ -76,6 +77,7 @@ internal class IdentityServiceImpl(
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
authRequestId = authModel.authRequestId,
newDeviceOtp = newDeviceOtp,
+ deeplinkScheme = deeplinkScheme,
)
.toResult()
.recoverCatching { throwable ->
diff --git a/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt
index 3ff50f2dad6..e07bbf3afab 100644
--- a/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt
+++ b/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt
@@ -184,6 +184,7 @@ class IdentityServiceTest : BaseServiceTest() {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
assertEquals(LOGIN_SUCCESS.asSuccess(), result)
}
@@ -198,6 +199,7 @@ class IdentityServiceTest : BaseServiceTest() {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
assertTrue(result.isFailure)
}
@@ -212,6 +214,7 @@ class IdentityServiceTest : BaseServiceTest() {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
assertEquals(TWO_FACTOR_BODY.asSuccess(), result)
}
@@ -226,6 +229,7 @@ class IdentityServiceTest : BaseServiceTest() {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
assertEquals(INVALID_LOGIN.asSuccess(), result)
}
@@ -241,6 +245,7 @@ class IdentityServiceTest : BaseServiceTest() {
password = PASSWORD_HASH,
),
uniqueAppId = UNIQUE_APP_ID,
+ deeplinkScheme = DEEPLINK_SCHEME,
)
assertEquals(INVALID_LOGIN.asSuccess(), result)
}
@@ -438,6 +443,7 @@ class IdentityServiceTest : BaseServiceTest() {
}
companion object {
+ private const val DEEPLINK_SCHEME = "deeplinkScheme"
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val REFRESH_TOKEN = "refreshToken"
private const val EMAIL_TOKEN = "emailToken"
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManager.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManager.kt
index d773f7c33b4..d2bea223005 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManager.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManager.kt
@@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.data.manager.BuildInfoManager
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.model.FileData
import java.time.Clock
@@ -47,11 +48,11 @@ interface IntentManager {
fun launchUri(uri: Uri)
/**
- * Start an Auth Tab Activity using the provided [Uri].
+ * Start an Auth Tab Activity using the provided [Uri] and [AuthTabData].
*/
fun startAuthTab(
uri: Uri,
- redirectScheme: String,
+ authTabData: AuthTabData,
launcher: ActivityResultLauncher,
)
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManagerImpl.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManagerImpl.kt
index 4d14b75bfce..f4aadf22749 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManagerImpl.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/IntentManagerImpl.kt
@@ -27,6 +27,7 @@ import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.util.toFormattedPattern
import com.bitwarden.core.util.isBuildVersionAtLeast
+import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
import com.bitwarden.ui.platform.manager.util.deviceData
import com.bitwarden.ui.platform.manager.util.fileProviderAuthority
import com.bitwarden.ui.platform.model.FileData
@@ -77,13 +78,25 @@ internal class IntentManagerImpl(
override fun startAuthTab(
uri: Uri,
- redirectScheme: String,
+ authTabData: AuthTabData,
launcher: ActivityResultLauncher,
) {
val providerPackageName = CustomTabsClient.getPackageName(activity, null).toString()
if (CustomTabsClient.isAuthTabSupported(activity, providerPackageName)) {
Timber.d("Launching uri with AuthTab for $providerPackageName")
- AuthTabIntent.Builder().build().launch(launcher, uri, redirectScheme)
+ when (authTabData) {
+ is AuthTabData.CustomScheme -> {
+ AuthTabIntent.Builder()
+ .build()
+ .launch(launcher, uri, authTabData.callbackScheme)
+ }
+
+ is AuthTabData.HttpsScheme -> {
+ AuthTabIntent.Builder()
+ .build()
+ .launch(launcher, uri, authTabData.host, "\\${authTabData.path}")
+ }
+ }
} else {
// Fall back to a Custom Tab.
Timber.d("Launching uri with CustomTabs fallback for $providerPackageName")
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/intent/model/AuthTabData.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/intent/model/AuthTabData.kt
new file mode 100644
index 00000000000..69db637409c
--- /dev/null
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/manager/intent/model/AuthTabData.kt
@@ -0,0 +1,38 @@
+package com.bitwarden.ui.platform.manager.intent.model
+
+import androidx.browser.auth.AuthTabIntent
+import androidx.browser.customtabs.CustomTabsIntent
+
+/**
+ * Represents all data required to launch an [AuthTabIntent] or a fallback [CustomTabsIntent].
+ */
+sealed class AuthTabData {
+ /**
+ * The scheme being used for the callback.
+ */
+ abstract val callbackScheme: String
+
+ /**
+ * The url to be used for the callback.
+ */
+ abstract val callbackUrl: String
+
+ /**
+ * A representation of a custom "Bitwarden" scheme callback.
+ */
+ data class CustomScheme(
+ override val callbackUrl: String,
+ override val callbackScheme: String = "bitwarden",
+ ) : AuthTabData()
+
+ /**
+ * A representation of a "https" app link scheme callback.
+ */
+ data class HttpsScheme(
+ val host: String,
+ val path: String,
+ ) : AuthTabData() {
+ override val callbackScheme: String = "https"
+ override val callbackUrl: String = "$callbackScheme://$host/$path"
+ }
+}