mirror of
https://github.com/bitwarden/android.git
synced 2026-02-03 18:17:54 -06:00
PM-26577: Support multiple schemes for Duo, WebAuthn, and SSO callbacks
This commit is contained in:
parent
1d35004999
commit
d702bbf52e
@ -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.
|
||||
*/
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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<Intent> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -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<Uri>()
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user