mirror of
https://github.com/bitwarden/android.git
synced 2026-04-30 12:59:02 -05:00
PM-31953: Support multiple schemes for Duo, WebAuthn, and SSO callbacks (#6498)
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ fun EnterpriseSignOnScreen(
|
||||
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
|
||||
intentManager.startAuthTab(
|
||||
uri = event.uri,
|
||||
redirectScheme = event.scheme,
|
||||
authTabData = event.authTabData,
|
||||
launcher = authTabLaunchers.sso,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -6,5 +6,6 @@ package com.bitwarden.data.repository.model
|
||||
enum class EnvironmentRegion {
|
||||
UNITED_STATES,
|
||||
EUROPEAN_UNION,
|
||||
INTERNAL,
|
||||
SELF_HOSTED,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user