PM-31953: Support multiple schemes for Duo, WebAuthn, and SSO callbacks (#6498)

This commit is contained in:
David Perez
2026-02-10 14:21:40 -06:00
committed by GitHub
parent 43940102ff
commit 31d480d6b4
28 changed files with 608 additions and 157 deletions

View File

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

View File

@@ -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<String> = 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

View File

@@ -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<String> = 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

View File

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

View File

@@ -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 -> {
// TODO: PM-26577 Update this to use a "HttpsScheme"
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
)
}
EnvironmentRegion.EUROPEAN_UNION -> {
// TODO: PM-26577 Update this to use a "HttpsScheme"
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
)
}
EnvironmentRegion.INTERNAL -> {
// TODO: PM-26577 Update this to use a "HttpsScheme"
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
)
}
EnvironmentRegion.SELF_HOSTED -> {
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
)
}
}

View File

@@ -66,7 +66,7 @@ fun EnterpriseSignOnScreen(
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
intentManager.startAuthTab(
uri = event.uri,
redirectScheme = event.scheme,
authTabData = event.authTabData,
launcher = authTabLaunchers.sso,
)
}

View File

@@ -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,12 +343,11 @@ class EnterpriseSignOnViewModel @Inject constructor(
if (ssoCallbackResult.state == ssoData.state) {
showLoading()
viewModelScope.launch {
val result = authRepository
.login(
val result = authRepository.login(
email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
ssoCode = ssoCallbackResult.code,
ssoCodeVerifier = ssoData.codeVerifier,
ssoRedirectUri = SSO_URI,
ssoRedirectUri = ssoData.redirectUri,
organizationIdentifier = state.orgIdentifierInput,
)
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
@@ -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

View File

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

View File

@@ -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,22 +176,40 @@ 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
-> handleDuoContinueButtonClick()
TwoFactorAuthMethod.WEB_AUTH -> handleWebAuthnContinueButtonClick()
TwoFactorAuthMethod.AUTHENTICATOR_APP,
TwoFactorAuthMethod.EMAIL,
TwoFactorAuthMethod.YUBI_KEY,
TwoFactorAuthMethod.U2F,
TwoFactorAuthMethod.REMEMBER,
TwoFactorAuthMethod.RECOVERY_CODE,
-> initiateLogin()
}
}
/**
* 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.
authUrl
authRepository
.twoFactorResponse
.twoFactorDuoAuthUrl
?.toUri()
?.let {
val environmentData = environmentRepository.environment.environmentUrlData
sendEvent(
event = TwoFactorLoginEvent.NavigateToDuo(
uri = it.toUri(),
scheme = "bitwarden",
uri = it,
authTabData = environmentData.duoAuthTabData,
),
)
}
@@ -205,18 +226,21 @@ class TwoFactorLoginViewModel @Inject constructor(
}
}
TwoFactorAuthMethod.WEB_AUTH -> {
/**
* 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 = environmentRepository
.environment
.environmentUrlData
.baseWebVaultUrlOrDefault,
baseUrl = environmentData.baseWebVaultUrlOrDefault,
callbackScheme = authTabData.callbackScheme,
data = it,
headerText = resourceManager.getString(
resId = BitwardenString.fido2_title,
@@ -228,7 +252,7 @@ class TwoFactorLoginViewModel @Inject constructor(
resId = BitwardenString.fido2_return_to_app,
),
)
TwoFactorLoginEvent.NavigateToWebAuth(uri = uri, scheme = "bitwarden")
TwoFactorLoginEvent.NavigateToWebAuth(uri = uri, authTabData = authTabData)
}
?: TwoFactorLoginEvent.ShowSnackbar(
message = BitwardenString
@@ -238,16 +262,6 @@ class TwoFactorLoginViewModel @Inject constructor(
)
}
TwoFactorAuthMethod.AUTHENTICATOR_APP,
TwoFactorAuthMethod.EMAIL,
TwoFactorAuthMethod.YUBI_KEY,
TwoFactorAuthMethod.U2F,
TwoFactorAuthMethod.REMEMBER,
TwoFactorAuthMethod.RECOVERY_CODE,
-> initiateLogin()
}
}
/**
* 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.

View File

@@ -190,9 +190,7 @@ class AuthRepositoryTest {
}
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val fakeEnvironmentRepository =
FakeEnvironmentRepository()
.apply {
private val fakeEnvironmentRepository = FakeEnvironmentRepository().apply {
environment = Environment.Us
}
private val settingsRepository: SettingsRepository = mockk {
@@ -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 = "bitwarden"
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val NAME = "Example Name"
private const val EMAIL = "test@bitwarden.com"

View File

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

View File

@@ -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",
@@ -23,11 +23,12 @@ class WebAuthUtilsTest : BitwardenComposeTest() {
)
val expectedUrl = baseUrl +
"/webauthn-mobile-connector.html" +
"?data=eyJjYWxsYmFja1VyaSI6ImJpdHdhcmRlbjovL3dlYmF1dGhuLWNhbGxiYWNrIiwiZ" +
"GF0YSI6Int9IiwiaGVhZGVyVGV4dCI6ImhlYWRlciIsImJ0blRleHQiOiJidXR0b24iLCJi" +
"dG5SZXR1cm5UZXh0IjoicmV0dXJuQnV0dG9uIn0=" +
"&parent=bitwarden%3A%2F%2Fwebauthn-callback" +
"&v=2"
"?data=eyJkYXRhIjoie30iLCJoZWFkZXJUZXh0IjoiaGVh" +
"ZGVyIiwiYnRuVGV4dCI6ImJ1dHRvbiIsImJ0blJldHVybl" +
"RleHQiOiJyZXR1cm5CdXR0b24iLCJtb2JpbGUiOnRydWV9" +
"&client=mobile" +
"&v=2" +
"&deeplinkScheme=https"
val expectedUri = Uri.parse(expectedUrl)
assertEquals(expectedUri, actualUri)
}

View File

@@ -0,0 +1,122 @@
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`() {
// TODO: PM-26577 Update these to use a "HttpsScheme"
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://duo-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_US.duoAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://duo-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_EU.duoAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://duo-callback",
callbackScheme = "bitwarden",
),
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`() {
// TODO: PM-26577 Update these to use a "HttpsScheme"
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://webauthn-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_US.webAuthnAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://webauthn-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_EU.webAuthnAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://webauthn-callback",
callbackScheme = "bitwarden",
),
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`() {
// TODO: PM-26577 Update these to use a "HttpsScheme"
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://sso-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_US.ssoAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://sso-callback",
callbackScheme = "bitwarden",
),
EnvironmentUrlDataJson.DEFAULT_EU.ssoAuthTabData,
)
assertEquals(
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://sso-callback",
callbackScheme = "bitwarden",
),
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",
)

View File

@@ -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<AuthTabData>()
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToSsoLogin(ssoUri, authTabData))
verify(exactly = 1) {
intentManager.startAuthTab(
uri = ssoUri,
redirectScheme = scheme,
authTabData = authTabData,
launcher = ssoLauncher,
)
}

View File

@@ -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.CustomScheme(
callbackUrl = "bitwarden://sso-callback",
callbackScheme = "bitwarden",
),
),
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",
)

View File

@@ -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<Intent> = mockk()
private val intentManager = mockk<IntentManager> {
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<Uri>()
val scheme = "bitwarden"
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToDuo(mockUri, scheme))
val authTabData = mockk<AuthTabData>()
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<Uri>()
val scheme = "bitwarden"
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToWebAuth(mockUri, scheme))
val authTabData = mockk<AuthTabData>()
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToWebAuth(mockUri, authTabData))
verify(exactly = 1) {
intentManager.startAuthTab(
uri = mockUri,
redirectScheme = scheme,
authTabData = authTabData,
launcher = webAuthnLauncher,
)
}

View File

@@ -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<Uri>()
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.CustomScheme(
callbackUrl = "bitwarden://duo-callback",
callbackScheme = "bitwarden",
),
),
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.CustomScheme(
callbackUrl = "bitwarden://webauthn-callback",
callbackScheme = "bitwarden",
),
),
awaitItem(),
)
}

View File

@@ -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.
*/

View File

@@ -6,5 +6,6 @@ package com.bitwarden.data.repository.model
enum class EnvironmentRegion {
UNITED_STATES,
EUROPEAN_UNION,
INTERNAL,
SELF_HOSTED,
}

View File

@@ -24,13 +24,31 @@ 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,
-> {
// TODO: PM-26577 Update this to use "https"
"bitwarden"
}
EnvironmentRegion.SELF_HOSTED -> "bitwarden"
}
/**
* Returns the base events URL or the default value if one is not present.
*/
@@ -38,7 +56,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 +72,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 +90,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 +110,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 +131,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

View File

@@ -336,6 +336,49 @@ class EnvironmentUrlsDataJsonExtensionsTest {
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.toBaseWebVaultImportUrl,
)
}
@Test
fun `appLinksScheme should return the correct scheme for US environment`() {
// TODO: PM-26577 Update this to use "https"
val expectedScheme = "bitwarden"
assertEquals(
expectedScheme,
EnvironmentUrlDataJson.DEFAULT_US.appLinksScheme,
)
}
@Test
fun `appLinksScheme should return the correct scheme for EU environment`() {
// TODO: PM-26577 Update this to use "https"
val expectedScheme = "bitwarden"
assertEquals(
expectedScheme,
EnvironmentUrlDataJson.DEFAULT_EU.appLinksScheme,
)
}
@Test
fun `appLinksScheme should return the correct scheme for internal environment`() {
// TODO: PM-26577 Update this to use "https"
val expectedScheme = "bitwarden"
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(

View File

@@ -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<GetTokenResponseJson.Success>
@GET("/sso/prevalidate")

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Intent>,
) {
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")

View File

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