mirror of
https://github.com/bitwarden/android.git
synced 2026-06-06 14:28:45 -05:00
BIT-816: Handle login attempt of SSO flow (#797)
This commit is contained in:
committed by
Álison Fernandes
parent
7a163d82ed
commit
c765de99f1
@@ -21,6 +21,11 @@ interface AuthDiskSource {
|
||||
*/
|
||||
var rememberedEmailAddress: String?
|
||||
|
||||
/**
|
||||
* The currently persisted organization identifier (or `null` if not set).
|
||||
*/
|
||||
var rememberedOrgIdentifier: String?
|
||||
|
||||
/**
|
||||
* The currently persisted user state information (or `null` if not set).
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ private const val BIOMETRICS_UNLOCK_KEY = "$ENCRYPTED_BASE_KEY:userKeyBiometricU
|
||||
private const val USER_AUTO_UNLOCK_KEY_KEY = "$ENCRYPTED_BASE_KEY:userKeyAutoUnlock"
|
||||
private const val UNIQUE_APP_ID_KEY = "$BASE_KEY:appId"
|
||||
private const val REMEMBERED_EMAIL_ADDRESS_KEY = "$BASE_KEY:rememberedEmail"
|
||||
private const val REMEMBERED_ORG_IDENTIFIER_KEY = "$BASE_KEY:rememberedOrgIdentifier"
|
||||
private const val STATE_KEY = "$BASE_KEY:state"
|
||||
private const val LAST_ACTIVE_TIME_KEY = "$BASE_KEY:lastActiveTime"
|
||||
private const val INVALID_UNLOCK_ATTEMPTS_KEY = "$BASE_KEY:invalidUnlockAttempts"
|
||||
@@ -67,6 +68,15 @@ class AuthDiskSourceImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override var rememberedOrgIdentifier: String?
|
||||
get() = getString(key = REMEMBERED_ORG_IDENTIFIER_KEY)
|
||||
set(value) {
|
||||
putString(
|
||||
key = REMEMBERED_ORG_IDENTIFIER_KEY,
|
||||
value = value,
|
||||
)
|
||||
}
|
||||
|
||||
override var userState: UserStateJson?
|
||||
get() = getString(key = STATE_KEY)?.let { json.decodeFromString(it) }
|
||||
set(value) {
|
||||
|
||||
@@ -62,6 +62,11 @@ interface AuthRepository : AuthenticatorProvider {
|
||||
*/
|
||||
var rememberedEmailAddress: String?
|
||||
|
||||
/**
|
||||
* The currently persisted organization identifier (or `null` if not set).
|
||||
*/
|
||||
var rememberedOrgIdentifier: String?
|
||||
|
||||
/**
|
||||
* Tracks whether there is an additional account that is pending login/registration in order to
|
||||
* have multiple accounts available.
|
||||
@@ -103,6 +108,17 @@ interface AuthRepository : AuthenticatorProvider {
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Attempt to login using a SSO flow. Updated access token will be reflected in [authStateFlow].
|
||||
*/
|
||||
suspend fun login(
|
||||
email: String,
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult
|
||||
|
||||
/**
|
||||
* Log out the current user.
|
||||
*/
|
||||
|
||||
@@ -186,6 +186,8 @@ class AuthRepositoryImpl(
|
||||
|
||||
override var rememberedEmailAddress: String? by authDiskSource::rememberedEmailAddress
|
||||
|
||||
override var rememberedOrgIdentifier: String? by authDiskSource::rememberedOrgIdentifier
|
||||
|
||||
override var hasPendingAccountAddition: Boolean
|
||||
by mutableHasPendingAccountAdditionStateFlow::value
|
||||
|
||||
@@ -258,6 +260,22 @@ class AuthRepositoryImpl(
|
||||
)
|
||||
} ?: LoginResult.Error(errorMessage = null)
|
||||
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
ssoCode: String,
|
||||
ssoCodeVerifier: String,
|
||||
ssoRedirectUri: String,
|
||||
captchaToken: String?,
|
||||
): LoginResult = loginCommon(
|
||||
email = email,
|
||||
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||
ssoCode = ssoCode,
|
||||
ssoCodeVerifier = ssoCodeVerifier,
|
||||
ssoRedirectUri = ssoRedirectUri,
|
||||
),
|
||||
captchaToken = captchaToken,
|
||||
)
|
||||
|
||||
/**
|
||||
* A helper function to extract the common logic of logging in through
|
||||
* any of the available methods.
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.URLEncoder
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
|
||||
private const val SSO_HOST: String = "sso-callback"
|
||||
private const val SSO_URI = "bitwarden://$SSO_HOST"
|
||||
const val SSO_URI: String = "bitwarden://$SSO_HOST"
|
||||
|
||||
/**
|
||||
* Generates a URI for the SSO custom tab.
|
||||
@@ -28,7 +30,7 @@ fun generateUriForSso(
|
||||
val encodedOrganizationIdentifier = URLEncoder.encode(organizationIdentifier, "UTF-8")
|
||||
val encodedToken = URLEncoder.encode(token, "UTF-8")
|
||||
|
||||
val codeChallenge = Base64.getEncoder().encodeToString(
|
||||
val codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(
|
||||
MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(codeVerifier.toByteArray()),
|
||||
@@ -77,10 +79,11 @@ fun Intent.getSsoCallbackResult(): SsoCallbackResult? {
|
||||
/**
|
||||
* Sealed class representing the result of an SSO callback data extraction.
|
||||
*/
|
||||
sealed class SsoCallbackResult {
|
||||
sealed class SsoCallbackResult : Parcelable {
|
||||
/**
|
||||
* Represents an SSO callback object with a missing code value.
|
||||
*/
|
||||
@Parcelize
|
||||
data object MissingCode : SsoCallbackResult()
|
||||
|
||||
/**
|
||||
@@ -88,6 +91,7 @@ sealed class SsoCallbackResult {
|
||||
* present doesn't guarantee it is correct, and should be checked against the known state before
|
||||
* being used.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Success(
|
||||
val state: String?,
|
||||
val code: String,
|
||||
|
||||
@@ -48,6 +48,12 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||
)
|
||||
enterpriseSignOnDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToTwoFactorLogin = { emailAddress ->
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = emailAddress,
|
||||
password = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
landingDestination(
|
||||
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
|
||||
@@ -68,7 +74,11 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||
emailAddress = emailAddress,
|
||||
)
|
||||
},
|
||||
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
||||
onNavigateToEnterpriseSignOn = { emailAddress ->
|
||||
navController.navigateToEnterpriseSignOn(
|
||||
emailAddress = emailAddress,
|
||||
)
|
||||
},
|
||||
onNavigateToLoginWithDevice = { emailAddress ->
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = emailAddress,
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val ENTERPRISE_SIGN_ON_ROUTE = "enterprise_sign_on"
|
||||
private const val ENTERPRISE_SIGN_ON_PREFIX = "enterprise_sign_on "
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val ENTERPRISE_SIGN_ON_ROUTE = "$ENTERPRISE_SIGN_ON_PREFIX/{$EMAIL_ADDRESS}"
|
||||
|
||||
/**
|
||||
* Class to retrieve login arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class EnterpriseSignOnArgs(val emailAddress: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the enterprise single sign on screen.
|
||||
*/
|
||||
fun NavController.navigateToEnterpriseSignOn(navOptions: NavOptions? = null) {
|
||||
this.navigate(ENTERPRISE_SIGN_ON_ROUTE, navOptions)
|
||||
fun NavController.navigateToEnterpriseSignOn(
|
||||
emailAddress: String,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$ENTERPRISE_SIGN_ON_PREFIX/$emailAddress", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,12 +38,17 @@ fun NavController.navigateToEnterpriseSignOn(navOptions: NavOptions? = null) {
|
||||
*/
|
||||
fun NavGraphBuilder.enterpriseSignOnDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = ENTERPRISE_SIGN_ON_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
EnterpriseSignOnScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToTwoFactorLogin = onNavigateToTwoFactorLogin,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.enterprisesignon
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -22,7 +21,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -53,21 +51,26 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
@Composable
|
||||
fun EnterpriseSignOnScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToTwoFactorLogin: (String) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: EnterpriseSignOnViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
EnterpriseSignOnEvent.NavigateBack -> onNavigateBack()
|
||||
is EnterpriseSignOnEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
is EnterpriseSignOnEvent.NavigateToSsoLogin -> {
|
||||
intentManager.startCustomTabsActivity(event.uri)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnEvent.NavigateToCaptcha -> {
|
||||
intentManager.startCustomTabsActivity(event.uri)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnEvent.NavigateToTwoFactorLogin -> {
|
||||
onNavigateToTwoFactorLogin(event.emailAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
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.util.CaptchaCallbackTokenResult
|
||||
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.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForSso
|
||||
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
@@ -25,13 +29,15 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_SSO_STATE = "ssoState"
|
||||
private const val KEY_SSO_DATA = "ssoData"
|
||||
private const val KEY_SSO_CALLBACK_RESULT = "ssoCallbackResult"
|
||||
private const val KEY_STATE = "state"
|
||||
private const val RANDOM_STRING_LENGTH = 64
|
||||
|
||||
/**
|
||||
* Manages application state for the enterprise single sign on screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class EnterpriseSignOnViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
@@ -43,25 +49,41 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: EnterpriseSignOnState(
|
||||
dialogState = null,
|
||||
orgIdentifierInput = "",
|
||||
orgIdentifierInput = authRepository.rememberedOrgIdentifier ?: "",
|
||||
captchaToken = null,
|
||||
),
|
||||
) {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Data needed once a response is received from the SSO backend.
|
||||
*/
|
||||
private var ssoState: String?
|
||||
get() = savedStateHandle[KEY_SSO_STATE]
|
||||
private var ssoResponseData: SsoResponseData?
|
||||
get() = savedStateHandle[KEY_SSO_DATA]
|
||||
set(value) {
|
||||
savedStateHandle[KEY_SSO_STATE] = value
|
||||
savedStateHandle[KEY_SSO_DATA] = value
|
||||
}
|
||||
|
||||
private var savedSsoCallbackResult: SsoCallbackResult?
|
||||
get() = savedStateHandle[KEY_SSO_CALLBACK_RESULT]
|
||||
set(value) {
|
||||
savedStateHandle[KEY_SSO_CALLBACK_RESULT] = value
|
||||
}
|
||||
|
||||
init {
|
||||
authRepository
|
||||
.ssoCallbackResultFlow
|
||||
.onEach {
|
||||
handleSsoCallbackResult(it)
|
||||
sendAction(EnterpriseSignOnAction.Internal.OnSsoCallbackResult(it))
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Automatically attempt to login again if a captcha token is received.
|
||||
authRepository
|
||||
.captchaTokenResultFlow
|
||||
.onEach {
|
||||
sendAction(
|
||||
EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken(it),
|
||||
)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
@@ -82,6 +104,18 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
EnterpriseSignOnAction.Internal.OnSsoPrevalidationFailure -> {
|
||||
handleOnSsoPrevalidationFailure()
|
||||
}
|
||||
|
||||
is EnterpriseSignOnAction.Internal.OnSsoCallbackResult -> {
|
||||
handleOnSsoCallbackResult(action)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnAction.Internal.OnLoginResult -> {
|
||||
handleOnLoginResult(action)
|
||||
}
|
||||
|
||||
is EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken -> {
|
||||
handleOnReceiveCaptchaToken(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,9 +128,6 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleLogInClicked() {
|
||||
// TODO BIT-816: submit request for single sign on
|
||||
sendEvent(EnterpriseSignOnEvent.ShowToast("Not yet implemented."))
|
||||
|
||||
if (!networkConnectionManager.isNetworkConnected) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@@ -123,13 +154,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
R.string.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
showLoading()
|
||||
|
||||
viewModelScope.launch {
|
||||
val prevalidateSsoResult = authRepository.prevalidateSso(organizationIdentifier)
|
||||
@@ -148,6 +173,44 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnLoginResult(action: EnterpriseSignOnAction.Internal.OnLoginResult) {
|
||||
when (val loginResult = action.loginResult) {
|
||||
is LoginResult.CaptchaRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
event = EnterpriseSignOnEvent.NavigateToCaptcha(
|
||||
uri = generateUriForCaptcha(captchaId = loginResult.captchaId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is LoginResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = loginResult.errorMessage?.asText()
|
||||
?: R.string.login_sso_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is LoginResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
authRepository.rememberedOrgIdentifier = state.orgIdentifierInput
|
||||
}
|
||||
|
||||
is LoginResult.TwoFactorRequired -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
sendEvent(
|
||||
EnterpriseSignOnEvent.NavigateToTwoFactorLogin(
|
||||
emailAddress = EnterpriseSignOnArgs(savedStateHandle).emailAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnGenerateUriForSsoResult(
|
||||
action: EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult,
|
||||
) {
|
||||
@@ -156,13 +219,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleOnSsoPrevalidationFailure() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.login_sso_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
showDefaultError()
|
||||
}
|
||||
|
||||
private fun handleOrgIdentifierInputChanged(
|
||||
@@ -171,8 +228,64 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(orgIdentifierInput = action.input) }
|
||||
}
|
||||
|
||||
private fun handleSsoCallbackResult(ssoCallbackResult: SsoCallbackResult) {
|
||||
// TODO Handle result as last part of BIT-816
|
||||
private fun handleOnSsoCallbackResult(
|
||||
action: EnterpriseSignOnAction.Internal.OnSsoCallbackResult,
|
||||
) {
|
||||
savedSsoCallbackResult = action.ssoCallbackResult
|
||||
attemptLogin()
|
||||
}
|
||||
|
||||
private fun handleOnReceiveCaptchaToken(
|
||||
action: EnterpriseSignOnAction.Internal.OnReceiveCaptchaToken,
|
||||
) {
|
||||
when (val tokenResult = action.tokenResult) {
|
||||
CaptchaCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
title = R.string.log_in_denied.asText(),
|
||||
message = R.string.captcha_failed.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is CaptchaCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
attemptLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attemptLogin() {
|
||||
val ssoCallbackResult = requireNotNull(savedSsoCallbackResult)
|
||||
val ssoData = requireNotNull(ssoResponseData)
|
||||
|
||||
when (ssoCallbackResult) {
|
||||
is SsoCallbackResult.MissingCode -> {
|
||||
showDefaultError()
|
||||
}
|
||||
is SsoCallbackResult.Success -> {
|
||||
if (ssoCallbackResult.state == ssoData.state) {
|
||||
showLoading()
|
||||
viewModelScope.launch {
|
||||
val result = authRepository
|
||||
.login(
|
||||
email = EnterpriseSignOnArgs(savedStateHandle).emailAddress,
|
||||
ssoCode = ssoCallbackResult.code,
|
||||
ssoCodeVerifier = ssoData.codeVerifier,
|
||||
ssoRedirectUri = SSO_URI,
|
||||
captchaToken = mutableStateFlow.value.captchaToken,
|
||||
)
|
||||
sendAction(EnterpriseSignOnAction.Internal.OnLoginResult(result))
|
||||
}
|
||||
} else {
|
||||
showDefaultError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareAndLaunchCustomTab(
|
||||
@@ -184,7 +297,12 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
// Save this for later so that we can validate the SSO callback response
|
||||
val generatedSsoState = generatorRepository
|
||||
.generateRandomString(RANDOM_STRING_LENGTH)
|
||||
.also { ssoState = it }
|
||||
.also {
|
||||
ssoResponseData = SsoResponseData(
|
||||
codeVerifier = codeVerifier,
|
||||
state = it,
|
||||
)
|
||||
}
|
||||
|
||||
val uri = generateUriForSso(
|
||||
identityBaseUrl = environmentRepository.environment.environmentUrlData.baseIdentityUrl,
|
||||
@@ -198,6 +316,26 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
// a result due to user intervention
|
||||
sendAction(EnterpriseSignOnAction.Internal.OnGenerateUriForSsoResult(Uri.parse(uri)))
|
||||
}
|
||||
|
||||
private fun showDefaultError() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Error(
|
||||
message = R.string.login_sso_error.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoading() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = EnterpriseSignOnState.DialogState.Loading(
|
||||
R.string.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,6 +345,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
|
||||
data class EnterpriseSignOnState(
|
||||
val dialogState: DialogState?,
|
||||
val orgIdentifierInput: String,
|
||||
val captchaToken: String?,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
@@ -247,11 +386,14 @@ sealed class EnterpriseSignOnEvent {
|
||||
data class NavigateToSsoLogin(val uri: Uri) : EnterpriseSignOnEvent()
|
||||
|
||||
/**
|
||||
* Shows a toast with the given [message].
|
||||
* Navigates to the captcha verification screen.
|
||||
*/
|
||||
data class ShowToast(
|
||||
val message: String,
|
||||
) : EnterpriseSignOnEvent()
|
||||
data class NavigateToCaptcha(val uri: Uri) : EnterpriseSignOnEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the two-factor login screen.
|
||||
*/
|
||||
data class NavigateToTwoFactorLogin(val emailAddress: String) : EnterpriseSignOnEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,9 +431,38 @@ sealed class EnterpriseSignOnAction {
|
||||
*/
|
||||
data class OnGenerateUriForSsoResult(val uri: Uri) : Internal()
|
||||
|
||||
/**
|
||||
* A login result has been received.
|
||||
*/
|
||||
data class OnLoginResult(val loginResult: LoginResult) : Internal()
|
||||
|
||||
/**
|
||||
* An SSO callback result has been received.
|
||||
*/
|
||||
data class OnSsoCallbackResult(val ssoCallbackResult: SsoCallbackResult) : Internal()
|
||||
|
||||
/**
|
||||
* SSO prevalidation failed.
|
||||
*/
|
||||
data object OnSsoPrevalidationFailure : Internal()
|
||||
|
||||
/**
|
||||
* A captcha callback result has been received
|
||||
*/
|
||||
data class OnReceiveCaptchaToken(val tokenResult: CaptchaCallbackTokenResult) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data needed by the SSO flow to verify and continue the process after receiving a response.
|
||||
*
|
||||
* @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 state: String,
|
||||
val codeVerifier: String,
|
||||
) : Parcelable
|
||||
|
||||
@@ -44,7 +44,7 @@ fun NavController.navigateToLogin(
|
||||
fun NavGraphBuilder.loginDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (emailAddress: String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToEnterpriseSignOn: (emailAddress: String) -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: (emailAddress: String, password: String?) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -65,7 +65,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
fun LoginScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToEnterpriseSignOn: (String) -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: (String, String?) -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
@@ -84,7 +84,10 @@ fun LoginScreen(
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn()
|
||||
is LoginEvent.NavigateToEnterpriseSignOn -> {
|
||||
onNavigateToEnterpriseSignOn(event.emailAddress)
|
||||
}
|
||||
|
||||
is LoginEvent.NavigateToLoginWithDevice -> {
|
||||
onNavigateToLoginWithDevice(event.emailAddress)
|
||||
}
|
||||
|
||||
@@ -257,7 +257,8 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleSingleSignOnClicked() {
|
||||
sendEvent(LoginEvent.NavigateToEnterpriseSignOn)
|
||||
val email = mutableStateFlow.value.emailAddress
|
||||
sendEvent(LoginEvent.NavigateToEnterpriseSignOn(email))
|
||||
}
|
||||
|
||||
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
|
||||
@@ -310,7 +311,7 @@ sealed class LoginEvent {
|
||||
/**
|
||||
* Navigates to the enterprise single sign on screen.
|
||||
*/
|
||||
data object NavigateToEnterpriseSignOn : LoginEvent()
|
||||
data class NavigateToEnterpriseSignOn(val emailAddress: String) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the login with device screen.
|
||||
|
||||
Reference in New Issue
Block a user