Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
<data android:scheme="https" />
<data android:host="bitwarden.com" />
<data android:host="bitwarden.eu" />
<data android:host="bitwarden.pw" />
<data android:pathPattern="/duo-callback" />
<data android:pathPattern="/sso-callback" />
<data android:pathPattern="/webauthn-callback" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1573,6 +1574,7 @@ class AuthRepositoryImpl(
): LoginResult = identityService
.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
deeplinkScheme = environmentRepository.environment.environmentUrlData.appLinksScheme,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed specifically for Duo support, since the server generates the redirect url.
The property is fully ignored if the user does not have Duo configured but we do not know that at this time so we must always send it.

email = email,
authModel = authModel,
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.x8bit.bitwarden.data.platform.util

import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
import com.bitwarden.data.repository.model.EnvironmentRegion
import com.bitwarden.ui.platform.manager.intent.model.AuthTabData

/**
* Creates the appropriate Duo [AuthTabData] for the given [EnvironmentUrlDataJson].
*/
val EnvironmentUrlDataJson.duoAuthTabData: AuthTabData get() = authTabData(kind = "duo")

/**
* Creates the appropriate WebAuthn [AuthTabData] for the given [EnvironmentUrlDataJson].
*/
val EnvironmentUrlDataJson.webAuthnAuthTabData: AuthTabData get() = authTabData(kind = "webauthn")

/**
* Creates the appropriate SSO [AuthTabData] for the given [EnvironmentUrlDataJson].
*/
val EnvironmentUrlDataJson.ssoAuthTabData: AuthTabData get() = authTabData(kind = "sso")

private fun EnvironmentUrlDataJson.authTabData(
kind: String,
): AuthTabData = when (this.environmentRegion) {
EnvironmentRegion.UNITED_STATES -> {
AuthTabData.HttpsScheme(
host = "bitwarden.com",
path = "$kind-callback",
)
}

EnvironmentRegion.EUROPEAN_UNION -> {
AuthTabData.HttpsScheme(
host = "bitwarden.eu",
path = "$kind-callback",
)
}

EnvironmentRegion.INTERNAL -> {
AuthTabData.HttpsScheme(
host = "bitwarden.pw",
path = "$kind-callback",
)
}

EnvironmentRegion.SELF_HOSTED -> {
AuthTabData.CustomScheme(
callbackUrl = "bitwarden://$kind-callback",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ fun EnterpriseSignOnScreen(
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
intentManager.startAuthTab(
uri = event.uri,
redirectScheme = event.scheme,
authTabData = event.authTabData,
launcher = authTabLaunchers.sso,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ 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
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
Expand Down Expand Up @@ -208,7 +209,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
sendEvent(
EnterpriseSignOnEvent.NavigateToSsoLogin(
uri = action.uri,
scheme = action.scheme,
authTabData = action.authTabData,
),
)
}
Expand Down Expand Up @@ -342,14 +343,13 @@ class EnterpriseSignOnViewModel @Inject constructor(
if (ssoCallbackResult.state == ssoData.state) {
showLoading()
viewModelScope.launch {
val result = authRepository
.login(
email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
ssoCode = ssoCallbackResult.code,
ssoCodeVerifier = ssoData.codeVerifier,
ssoRedirectUri = SSO_URI,
organizationIdentifier = state.orgIdentifierInput,
)
val result = authRepository.login(
email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
ssoCode = ssoCallbackResult.code,
ssoCodeVerifier = ssoData.codeVerifier,
ssoRedirectUri = ssoData.redirectUri,
organizationIdentifier = state.orgIdentifierInput,
)
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
}
} else {
Expand Down Expand Up @@ -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,
Expand All @@ -408,7 +412,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
sendAction(
EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult(
uri = uri,
scheme = "bitwarden",
authTabData = authTabData,
),
)
}
Expand Down Expand Up @@ -518,7 +522,7 @@ sealed class EnterpriseSignOnEvent {
*/
data class NavigateToSsoLogin(
val uri: Uri,
val scheme: String,
val authTabData: AuthTabData,
) : EnterpriseSignOnEvent()

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -612,13 +619,15 @@ 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
* request.
*/
@Parcelize
data class SsoResponseData(
val redirectUri: String,
val state: String,
val codeVerifier: String,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ fun TwoFactorLoginScreen(
is TwoFactorLoginEvent.NavigateToDuo -> {
intentManager.startAuthTab(
uri = event.uri,
redirectScheme = event.scheme,
authTabData = event.authTabData,
launcher = authTabLaunchers.duo,
)
}

is TwoFactorLoginEvent.NavigateToWebAuth -> {
intentManager.startAuthTab(
uri = event.uri,
redirectScheme = event.scheme,
authTabData = event.authTabData,
launcher = authTabLaunchers.webAuthn,
)
}
Expand Down
Loading
Loading