Skip to content

Commit ad2fef3

Browse files
PM-26577: Support multiple schemes for Duo, WebAuthn, and SSO callbacks
1 parent f728c15 commit ad2fef3

File tree

24 files changed

+460
-137
lines changed

24 files changed

+460
-137
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
<data android:scheme="https" />
170170
<data android:host="bitwarden.com" />
171171
<data android:host="bitwarden.eu" />
172+
<data android:host="bitwarden.pw" />
172173
<data android:pathPattern="/duo-callback" />
173174
<data android:pathPattern="/sso-callback" />
174175
<data android:pathPattern="/webauthn-callback" />

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.bitwarden.core.data.util.flatMap
1414
import com.bitwarden.crypto.HashPurpose
1515
import com.bitwarden.crypto.Kdf
1616
import com.bitwarden.data.datasource.disk.ConfigDiskSource
17+
import com.bitwarden.data.repository.util.appLinksScheme
1718
import com.bitwarden.data.repository.util.toEnvironmentUrls
1819
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
1920
import com.bitwarden.network.model.CreateAccountKeysResponseJson
@@ -1573,6 +1574,7 @@ class AuthRepositoryImpl(
15731574
): LoginResult = identityService
15741575
.getToken(
15751576
uniqueAppId = authDiskSource.uniqueAppId,
1577+
deeplinkScheme = environmentRepository.environment.environmentUrlData.appLinksScheme,
15761578
email = email,
15771579
authModel = authModel,
15781580
twoFactorData = twoFactorData ?: getRememberedTwoFactorData(email),

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/DuoUtils.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import android.net.Uri
55
import androidx.browser.auth.AuthTabIntent
66
import com.bitwarden.annotation.OmitFromCoverage
77

8-
private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
9-
private const val BITWARDEN_US_HOST: String = "bitwarden.com"
8+
private val BITWARDEN_HOSTS: List<String> = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
109
private const val APP_LINK_SCHEME: String = "https"
1110
private const val DEEPLINK_SCHEME: String = "bitwarden"
1211
private const val CALLBACK: String = "duo-callback"
@@ -34,9 +33,7 @@ fun Intent.getDuoCallbackTokenResult(): DuoCallbackTokenResult? {
3433
}
3534

3635
APP_LINK_SCHEME -> {
37-
if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
38-
localData.path == "/$CALLBACK"
39-
) {
36+
if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
4037
localData.getDuoCallbackTokenResult()
4138
} else {
4239
null

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/SsoUtils.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,31 @@ import java.net.URLEncoder
1111
import java.security.MessageDigest
1212
import java.util.Base64
1313

14-
private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
15-
private const val BITWARDEN_US_HOST: String = "bitwarden.com"
14+
private val BITWARDEN_HOSTS: List<String> = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
1615
private const val APP_LINK_SCHEME: String = "https"
1716
private const val DEEPLINK_SCHEME: String = "bitwarden"
1817
private const val CALLBACK: String = "sso-callback"
1918

20-
const val SSO_URI: String = "bitwarden://$CALLBACK"
21-
2219
/**
2320
* Generates a URI for the SSO custom tab.
2421
*
2522
* @param identityBaseUrl The base URl for the identity service.
23+
* @param redirectUrl The redirect URI used in the SSO request.
2624
* @param organizationIdentifier The SSO organization identifier.
2725
* @param token The prevalidated SSO token.
2826
* @param state Random state used to verify the validity of the response.
2927
* @param codeVerifier A random string used to generate the code challenge.
3028
*/
29+
@Suppress("LongParameterList")
3130
fun generateUriForSso(
3231
identityBaseUrl: String,
32+
redirectUrl: String,
3333
organizationIdentifier: String,
3434
token: String,
3535
state: String,
3636
codeVerifier: String,
3737
): Uri {
38-
val redirectUri = URLEncoder.encode(SSO_URI, "UTF-8")
38+
val redirectUri = URLEncoder.encode(redirectUrl, "UTF-8")
3939
val encodedOrganizationIdentifier = URLEncoder.encode(organizationIdentifier, "UTF-8")
4040
val encodedToken = URLEncoder.encode(token, "UTF-8")
4141

@@ -81,9 +81,7 @@ fun Intent.getSsoCallbackResult(): SsoCallbackResult? {
8181
}
8282

8383
APP_LINK_SCHEME -> {
84-
if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
85-
localData.path == "/$CALLBACK"
86-
) {
84+
if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
8785
localData.getSsoCallbackResult()
8886
} else {
8987
null

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/WebAuthUtils.kt

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,13 @@ import com.bitwarden.annotation.OmitFromCoverage
88
import kotlinx.serialization.json.JsonObject
99
import kotlinx.serialization.json.buildJsonObject
1010
import kotlinx.serialization.json.put
11-
import java.net.URLEncoder
1211
import java.util.Base64
1312

14-
private const val BITWARDEN_EU_HOST: String = "bitwarden.eu"
15-
private const val BITWARDEN_US_HOST: String = "bitwarden.com"
13+
private val BITWARDEN_HOSTS: List<String> = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw")
1614
private const val APP_LINK_SCHEME: String = "https"
1715
private const val DEEPLINK_SCHEME: String = "bitwarden"
1816
private const val CALLBACK: String = "webauthn-callback"
1917

20-
private const val CALLBACK_URI = "bitwarden://$CALLBACK"
21-
2218
/**
2319
* Retrieves an [WebAuthResult] from an [Intent]. There are three possible cases.
2420
*
@@ -39,9 +35,7 @@ fun Intent.getWebAuthResultOrNull(): WebAuthResult? {
3935
}
4036

4137
APP_LINK_SCHEME -> {
42-
if ((localData.host == BITWARDEN_US_HOST || localData.host == BITWARDEN_EU_HOST) &&
43-
localData.path == "/$CALLBACK"
44-
) {
38+
if (localData.host in BITWARDEN_HOSTS && localData.path == "/$CALLBACK") {
4539
localData.getWebAuthResult()
4640
} else {
4741
null
@@ -79,29 +73,31 @@ private fun Uri?.getWebAuthResult(): WebAuthResult =
7973
/**
8074
* Generates a [Uri] to display a web authn challenge for Bitwarden authentication.
8175
*/
76+
@Suppress("LongParameterList")
8277
fun generateUriForWebAuth(
8378
baseUrl: String,
79+
callbackScheme: String,
8480
data: JsonObject,
8581
headerText: String,
8682
buttonText: String,
8783
returnButtonText: String,
8884
): Uri {
8985
val json = buildJsonObject {
90-
put(key = "callbackUri", value = CALLBACK_URI)
9186
put(key = "data", value = data.toString())
9287
put(key = "headerText", value = headerText)
9388
put(key = "btnText", value = buttonText)
9489
put(key = "btnReturnText", value = returnButtonText)
90+
put(key = "mobile", value = true)
9591
}
9692
val base64Data = Base64
9793
.getEncoder()
9894
.encodeToString(json.toString().toByteArray(Charsets.UTF_8))
99-
val parentParam = URLEncoder.encode(CALLBACK_URI, "UTF-8")
10095
val url = baseUrl +
10196
"/webauthn-mobile-connector.html" +
10297
"?data=$base64Data" +
103-
"&parent=$parentParam" +
104-
"&v=2"
98+
"&client=mobile" +
99+
"&v=2" +
100+
"&deeplinkScheme=$callbackScheme"
105101
return url.toUri()
106102
}
107103

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.x8bit.bitwarden.data.platform.util
2+
3+
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
4+
import com.bitwarden.data.repository.model.EnvironmentRegion
5+
import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
6+
7+
/**
8+
* Creates the appropriate Duo [AuthTabData] for the given [EnvironmentUrlDataJson].
9+
*/
10+
val EnvironmentUrlDataJson.duoAuthTabData: AuthTabData get() = authTabData(kind = "duo")
11+
12+
/**
13+
* Creates the appropriate WebAuthn [AuthTabData] for the given [EnvironmentUrlDataJson].
14+
*/
15+
val EnvironmentUrlDataJson.webAuthnAuthTabData: AuthTabData get() = authTabData(kind = "webauthn")
16+
17+
/**
18+
* Creates the appropriate SSO [AuthTabData] for the given [EnvironmentUrlDataJson].
19+
*/
20+
val EnvironmentUrlDataJson.ssoAuthTabData: AuthTabData get() = authTabData(kind = "sso")
21+
22+
private fun EnvironmentUrlDataJson.authTabData(
23+
kind: String,
24+
): AuthTabData = when (this.environmentRegion) {
25+
EnvironmentRegion.UNITED_STATES -> {
26+
AuthTabData.HttpsScheme(
27+
host = "bitwarden.com",
28+
path = "\\$kind-callback",
29+
)
30+
}
31+
32+
EnvironmentRegion.EUROPEAN_UNION -> {
33+
AuthTabData.HttpsScheme(
34+
host = "bitwarden.eu",
35+
path = "\\$kind-callback",
36+
)
37+
}
38+
39+
EnvironmentRegion.SELF_HOSTED -> {
40+
if (this.base.contains("bitwarden.pw")) {
41+
AuthTabData.HttpsScheme(
42+
host = "bitwarden.pw",
43+
path = "\\$kind-callback",
44+
)
45+
} else {
46+
AuthTabData.CustomScheme(
47+
callbackUrl = "bitwarden://$kind-callback",
48+
)
49+
}
50+
}
51+
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fun EnterpriseSignOnScreen(
6666
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
6767
intentManager.startAuthTab(
6868
uri = event.uri,
69-
redirectScheme = event.scheme,
69+
authTabData = event.authTabData,
7070
launcher = authTabLaunchers.sso,
7171
)
7272
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ import androidx.lifecycle.viewModelScope
77
import com.bitwarden.data.repository.util.baseIdentityUrl
88
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
99
import com.bitwarden.ui.platform.base.BaseViewModel
10+
import com.bitwarden.ui.platform.manager.intent.model.AuthTabData
1011
import com.bitwarden.ui.platform.resource.BitwardenString
1112
import com.bitwarden.ui.util.Text
1213
import com.bitwarden.ui.util.asText
1314
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
1415
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
1516
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
1617
import com.x8bit.bitwarden.data.auth.repository.model.VerifiedOrganizationDomainSsoDetailsResult
17-
import com.x8bit.bitwarden.data.auth.repository.util.SSO_URI
1818
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
1919
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
2020
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
2121
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
22+
import com.x8bit.bitwarden.data.platform.util.ssoAuthTabData
2223
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
2324
import com.x8bit.bitwarden.data.tools.generator.repository.GeneratorRepository
2425
import com.x8bit.bitwarden.data.tools.generator.repository.utils.generateRandomString
@@ -208,7 +209,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
208209
sendEvent(
209210
EnterpriseSignOnEvent.NavigateToSsoLogin(
210211
uri = action.uri,
211-
scheme = action.scheme,
212+
authTabData = action.authTabData,
212213
),
213214
)
214215
}
@@ -342,14 +343,13 @@ class EnterpriseSignOnViewModel @Inject constructor(
342343
if (ssoCallbackResult.state == ssoData.state) {
343344
showLoading()
344345
viewModelScope.launch {
345-
val result = authRepository
346-
.login(
347-
email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
348-
ssoCode = ssoCallbackResult.code,
349-
ssoCodeVerifier = ssoData.codeVerifier,
350-
ssoRedirectUri = SSO_URI,
351-
organizationIdentifier = state.orgIdentifierInput,
352-
)
346+
val result = authRepository.login(
347+
email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress,
348+
ssoCode = ssoCallbackResult.code,
349+
ssoCodeVerifier = ssoData.codeVerifier,
350+
ssoRedirectUri = ssoData.redirectUri,
351+
organizationIdentifier = state.orgIdentifierInput,
352+
)
353353
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
354354
}
355355
} else {
@@ -385,18 +385,22 @@ class EnterpriseSignOnViewModel @Inject constructor(
385385
) {
386386
val codeVerifier = generatorRepository.generateRandomString(RANDOM_STRING_LENGTH)
387387

388+
val environmentData = environmentRepository.environment.environmentUrlData
389+
val authTabData = environmentData.ssoAuthTabData
388390
// Save this for later so that we can validate the SSO callback response
389391
val generatedSsoState = generatorRepository
390392
.generateRandomString(RANDOM_STRING_LENGTH)
391393
.also {
392394
ssoResponseData = SsoResponseData(
395+
redirectUri = authTabData.callbackUrl,
393396
codeVerifier = codeVerifier,
394397
state = it,
395398
)
396399
}
397400

398401
val uri = generateUriForSso(
399-
identityBaseUrl = environmentRepository.environment.environmentUrlData.baseIdentityUrl,
402+
identityBaseUrl = environmentData.baseIdentityUrl,
403+
redirectUrl = authTabData.callbackUrl,
400404
organizationIdentifier = organizationIdentifier,
401405
token = prevalidateSsoResult.token,
402406
state = generatedSsoState,
@@ -408,7 +412,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
408412
sendAction(
409413
EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult(
410414
uri = uri,
411-
scheme = "bitwarden",
415+
authTabData = authTabData,
412416
),
413417
)
414418
}
@@ -518,7 +522,7 @@ sealed class EnterpriseSignOnEvent {
518522
*/
519523
data class NavigateToSsoLogin(
520524
val uri: Uri,
521-
val scheme: String,
525+
val authTabData: AuthTabData,
522526
) : EnterpriseSignOnEvent()
523527

524528
/**
@@ -580,7 +584,10 @@ sealed class EnterpriseSignOnAction {
580584
/**
581585
* A [uri] has been generated to request an SSO result.
582586
*/
583-
data class OnGenerateUriForSsoResult(val uri: Uri, val scheme: String) : Internal()
587+
data class OnGenerateUriForSsoResult(
588+
val uri: Uri,
589+
val authTabData: AuthTabData,
590+
) : Internal()
584591

585592
/**
586593
* A login result has been received.
@@ -612,13 +619,15 @@ sealed class EnterpriseSignOnAction {
612619
/**
613620
* Data needed by the SSO flow to verify and continue the process after receiving a response.
614621
*
622+
* @property redirectUri The redirect URI used in the SSO request.
615623
* @property state A "state" maintained throughout the SSO process to verify that the response from
616624
* the server is valid and matches what was originally sent in the request.
617625
* @property codeVerifier A random string used to generate the code challenge for the initial SSO
618626
* request.
619627
*/
620628
@Parcelize
621629
data class SsoResponseData(
630+
val redirectUri: String,
622631
val state: String,
623632
val codeVerifier: String,
624633
) : Parcelable

app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginScreen.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ fun TwoFactorLoginScreen(
104104
is TwoFactorLoginEvent.NavigateToDuo -> {
105105
intentManager.startAuthTab(
106106
uri = event.uri,
107-
redirectScheme = event.scheme,
107+
authTabData = event.authTabData,
108108
launcher = authTabLaunchers.duo,
109109
)
110110
}
111111

112112
is TwoFactorLoginEvent.NavigateToWebAuth -> {
113113
intentManager.startAuthTab(
114114
uri = event.uri,
115-
redirectScheme = event.scheme,
115+
authTabData = event.authTabData,
116116
launcher = authTabLaunchers.webAuthn,
117117
)
118118
}

0 commit comments

Comments
 (0)