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 56a1555767..fecbbd0c92 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.auth.repository.util import android.content.Intent import android.net.Uri import androidx.browser.auth.AuthTabIntent +import androidx.core.net.toUri import com.bitwarden.annotation.OmitFromCoverage private const val BITWARDEN_EU_HOST: String = "bitwarden.eu" @@ -78,6 +79,14 @@ private fun Uri?.getDuoCallbackTokenResult(): DuoCallbackTokenResult { } } +/** + * Generates a [Uri] to display a duo challenge for Bitwarden authentication. + */ +fun generateUriForDuo( + authUrl: String, + appLinksScheme: String, +): Uri = "$authUrl&deeplinkScheme=$appLinksScheme".toUri() + /** * Sealed class representing the result of Duo callback token extraction. */ 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 94113ce789..860a520236 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 @@ -17,25 +17,26 @@ 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") 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 8bf85dadf0..64cf1e9f69 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,7 +8,6 @@ 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" @@ -17,8 +16,6 @@ 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. * @@ -79,29 +76,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/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt index 6f16ed4ee4..d9e424bd39 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 @@ -4,8 +4,10 @@ import android.net.Uri import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.data.repository.util.appLinksScheme import com.bitwarden.data.repository.util.baseIdentityUrl import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault +import com.bitwarden.data.repository.util.ssoAppLinksRedirectUrl import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text @@ -14,7 +16,6 @@ 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 @@ -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 redirectUrl = environmentData.ssoAppLinksRedirectUrl // Save this for later so that we can validate the SSO callback response val generatedSsoState = generatorRepository .generateRandomString(RANDOM_STRING_LENGTH) .also { ssoResponseData = SsoResponseData( + redirectUri = redirectUrl, codeVerifier = codeVerifier, state = it, ) } val uri = generateUriForSso( - identityBaseUrl = environmentRepository.environment.environmentUrlData.baseIdentityUrl, + identityBaseUrl = environmentData.baseIdentityUrl, + redirectUrl = redirectUrl, organizationIdentifier = organizationIdentifier, token = prevalidateSsoResult.token, state = generatedSsoState, @@ -408,7 +412,7 @@ class EnterpriseSignOnViewModel @Inject constructor( sendAction( EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult( uri = uri, - scheme = "bitwarden", + scheme = environmentData.appLinksScheme, ), ) } @@ -612,6 +616,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 +624,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/TwoFactorLoginViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index 93a02ceba1..1638e264a4 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 @@ -6,6 +6,7 @@ import androidx.annotation.DrawableRes import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.data.repository.util.appLinksScheme import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.network.model.TwoFactorAuthMethod import com.bitwarden.network.model.TwoFactorDataModel @@ -23,6 +24,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult +import com.x8bit.bitwarden.data.auth.repository.util.generateUriForDuo 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 @@ -173,71 +175,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 +194,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 + ?.let { + val environmentData = environmentRepository.environment.environmentUrlData + val appLinksScheme = environmentData.appLinksScheme + sendEvent( + event = TwoFactorLoginEvent.NavigateToDuo( + uri = generateUriForDuo(authUrl = it, appLinksScheme = appLinksScheme), + scheme = appLinksScheme, + ), + ) + } + ?: 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 appLinksScheme = environmentData.appLinksScheme + val uri = generateUriForWebAuth( + baseUrl = environmentData.baseWebVaultUrlOrDefault, + callbackScheme = appLinksScheme, + 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 = appLinksScheme) + } + ?: TwoFactorLoginEvent.ShowSnackbar( + message = BitwardenString + .there_was_an_error_starting_web_authn_two_factor_authentication + .asText(), + ), + ) + } + /** * Dismiss the view. */ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtilsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtilsTest.kt index e625c9489e..be11502736 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtilsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtilsTest.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.auth.repository.util import android.content.Intent +import android.net.Uri import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals @@ -9,6 +10,14 @@ import org.junit.jupiter.api.assertNull class DuoUtilsTest { + @Test + fun `generateUriForDuo should return a valid URI`() { + val authUrl = "https://vault.bitwarden.com" + val appLinksScheme = "https" + val actualUri = generateUriForDuo(authUrl = authUrl, appLinksScheme = appLinksScheme) + assertEquals(Uri.parse("$authUrl&deeplinkScheme=$appLinksScheme"), actualUri) + } + @Test fun `getDuoCallbackTokenResult should return null when action is not VIEW`() { val intent = mockk { 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 71fcbb6e7b..d522cc4bc6 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 7b0661a857..fa48276d59 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/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt index b79cfc9018..e16701fc71 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 @@ -163,7 +163,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 +186,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { ) assertEquals( - EnterpriseSignOnEvent.NavigateToSsoLogin(uri = ssoUri, scheme = "bitwarden"), + EnterpriseSignOnEvent.NavigateToSsoLogin(uri = ssoUri, scheme = "https"), eventFlow.awaitItem(), ) } @@ -385,7 +385,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 +451,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 +474,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 +548,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 +622,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 +739,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 +792,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 +848,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 +912,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 +1269,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/TwoFactorLoginViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt index f27318fc74..b1ec622fc3 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,6 +6,7 @@ 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 @@ -18,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult +import com.x8bit.bitwarden.data.auth.repository.util.generateUriForDuo 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 @@ -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 @@ -67,6 +68,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { fun setUp() { mockkStatic( ::generateUriForWebAuth, + ::generateUriForDuo, SavedStateHandle::toTwoFactorLoginArgs, ) mockkStatic(Uri::class) @@ -76,6 +78,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { fun tearDown() { unmockkStatic( ::generateUriForWebAuth, + ::generateUriForDuo, SavedStateHandle::toTwoFactorLoginArgs, ) unmockkStatic(Uri::class) @@ -418,22 +421,21 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { ) every { authRepository.twoFactorResponse } returns response val mockkUri = mockk() + every { + generateUriForDuo(authUrl = "bitwarden.com", appLinksScheme = "https") + } 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, scheme = "https"), awaitItem(), ) } - verify { - Uri.parse("bitwarden.com") - } } @Suppress("MaxLineLength") @@ -500,6 +502,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { every { generateUriForWebAuth( baseUrl = Environment.Us.environmentUrlData.baseWebVaultUrlOrDefault, + callbackScheme = Environment.Us.environmentUrlData.appLinksScheme, data = data, headerText = headerText, buttonText = buttonText, @@ -512,7 +515,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { viewModel.eventFlow.test { viewModel.trySendAction(TwoFactorLoginAction.ContinueButtonClick) assertEquals( - TwoFactorLoginEvent.NavigateToWebAuth(uri = mockkUri, scheme = "bitwarden"), + TwoFactorLoginEvent.NavigateToWebAuth(uri = mockkUri, scheme = "https"), awaitItem(), ) } 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 ecc0a1a1ae..6b5ccb3bee 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 @@ -31,6 +31,34 @@ val EnvironmentUrlDataJson.baseApiUrl: String } } +/** + * Returns the scheme used for app-links within the app. + */ +val EnvironmentUrlDataJson.appLinksScheme: String + get() = when (this.environmentRegion) { + EnvironmentRegion.UNITED_STATES, + EnvironmentRegion.EUROPEAN_UNION, + -> "https" + + EnvironmentRegion.SELF_HOSTED -> "bitwarden" + } + +/** + * Returns the sso app-link URI for the current environment. + */ +val EnvironmentUrlDataJson.ssoAppLinksRedirectUrl: String + get() = appLinksRedirectUrl(kind = "sso") + +/** + * Returns the app-link URI for the current environment and [kind]. + */ +private fun EnvironmentUrlDataJson.appLinksRedirectUrl(kind: String): String = + when (this.environmentRegion) { + EnvironmentRegion.UNITED_STATES -> "https://bitwarden.com/$kind-callback" + EnvironmentRegion.EUROPEAN_UNION -> "https://bitwarden.eu/$kind-callback" + EnvironmentRegion.SELF_HOSTED -> "bitwarden://$kind-callback" + } + /** * Returns the base events URL or the default value if one is not present. */ 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 6f6a4a982a..4addaad12b 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,69 @@ 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 custom environment`() { + val expectedScheme = "bitwarden" + + assertEquals( + expectedScheme, + DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.appLinksScheme, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `ssoAppLinksRedirectUrl should return the correct webauthn redirect url for US environment`() { + val expectedUrl = "https://bitwarden.com/sso-callback" + + assertEquals( + expectedUrl, + EnvironmentUrlDataJson.DEFAULT_US.ssoAppLinksRedirectUrl, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `ssoAppLinksRedirectUrl should return the correct webauthn redirect url for EU environment`() { + val expectedUrl = "https://bitwarden.eu/sso-callback" + + assertEquals( + expectedUrl, + EnvironmentUrlDataJson.DEFAULT_EU.ssoAppLinksRedirectUrl, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `ssoAppLinksRedirectUrl should return the correct webauthn redirect url for custom environment`() { + val expectedUrl = "bitwarden://sso-callback" + + assertEquals( + expectedUrl, + DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.ssoAppLinksRedirectUrl, + ) + } } private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(