PM-13301: Fix 2fa with key connector bug (#4059)

This commit is contained in:
David Perez 2024-10-09 16:16:20 -05:00 committed by GitHub
parent 79d2a00bf8
commit 537281f6c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 110 additions and 49 deletions

View file

@ -212,6 +212,7 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult
/**

View file

@ -628,6 +628,7 @@ class AuthRepositoryImpl(
password: String?,
twoFactorData: TwoFactorDataModel,
captchaToken: String?,
orgIdentifier: String?,
): LoginResult = identityTokenAuthModel
?.let {
loginCommon(
@ -637,6 +638,7 @@ class AuthRepositoryImpl(
twoFactorData = twoFactorData,
captchaToken = captchaToken ?: twoFactorResponse?.captchaToken,
deviceData = twoFactorDeviceData,
orgIdentifier = orgIdentifier,
)
}
?: LoginResult.Error(errorMessage = null)

View file

@ -108,10 +108,11 @@ fun NavGraphBuilder.authGraph(
enterpriseSignOnDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToSetPassword = { navController.navigateToSetPassword() },
onNavigateToTwoFactorLogin = { emailAddress ->
onNavigateToTwoFactorLogin = { emailAddress, orgIdentifier ->
navController.navigateToTwoFactorLogin(
emailAddress = emailAddress,
password = null,
orgIdentifier = orgIdentifier,
)
},
)
@ -155,6 +156,7 @@ fun NavGraphBuilder.authGraph(
navController.navigateToTwoFactorLogin(
emailAddress = emailAddress,
password = password,
orgIdentifier = null,
)
},
)
@ -164,6 +166,7 @@ fun NavGraphBuilder.authGraph(
navController.navigateToTwoFactorLogin(
emailAddress = it,
password = null,
orgIdentifier = null,
)
},
)

View file

@ -39,7 +39,7 @@ fun NavController.navigateToEnterpriseSignOn(
fun NavGraphBuilder.enterpriseSignOnDestination(
onNavigateBack: () -> Unit,
onNavigateToSetPassword: () -> Unit,
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
onNavigateToTwoFactorLogin: (emailAddress: String, orgIdentifier: String) -> Unit,
) {
composableWithSlideTransitions(
route = ENTERPRISE_SIGN_ON_ROUTE,

View file

@ -50,7 +50,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
fun EnterpriseSignOnScreen(
onNavigateBack: () -> Unit,
onNavigateToSetPassword: () -> Unit,
onNavigateToTwoFactorLogin: (String) -> Unit,
onNavigateToTwoFactorLogin: (email: String, orgIdentifier: String) -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: EnterpriseSignOnViewModel = hiltViewModel(),
) {
@ -72,7 +72,7 @@ fun EnterpriseSignOnScreen(
}
is EnterpriseSignOnEvent.NavigateToTwoFactorLogin -> {
onNavigateToTwoFactorLogin(event.emailAddress)
onNavigateToTwoFactorLogin(event.emailAddress, event.orgIdentifier)
}
}
}

View file

@ -170,6 +170,7 @@ class EnterpriseSignOnViewModel @Inject constructor(
sendEvent(
EnterpriseSignOnEvent.NavigateToTwoFactorLogin(
emailAddress = EnterpriseSignOnArgs(savedStateHandle).emailAddress,
orgIdentifier = state.orgIdentifierInput,
),
)
}
@ -481,7 +482,10 @@ sealed class EnterpriseSignOnEvent {
/**
* Navigates to the two-factor login screen.
*/
data class NavigateToTwoFactorLogin(val emailAddress: String) : EnterpriseSignOnEvent()
data class NavigateToTwoFactorLogin(
val emailAddress: String,
val orgIdentifier: String,
) : EnterpriseSignOnEvent()
}
/**

View file

@ -29,6 +29,7 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) {
navController.navigateToTwoFactorLogin(
emailAddress = it,
password = null,
orgIdentifier = null,
)
},
)

View file

@ -13,18 +13,26 @@ import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val EMAIL_ADDRESS = "email_address"
private const val PASSWORD = "password"
private const val ORG_IDENTIFIER = "org_identifier"
private const val TWO_FACTOR_LOGIN_PREFIX = "two_factor_login"
private const val TWO_FACTOR_LOGIN_ROUTE =
"$TWO_FACTOR_LOGIN_PREFIX/{${EMAIL_ADDRESS}}?$PASSWORD={$PASSWORD}"
"$TWO_FACTOR_LOGIN_PREFIX/{$EMAIL_ADDRESS}?" +
"$PASSWORD={$PASSWORD}&" +
"$ORG_IDENTIFIER={$ORG_IDENTIFIER}"
/**
* Class to retrieve Two-Factor Login arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class TwoFactorLoginArgs(val emailAddress: String, val password: String?) {
data class TwoFactorLoginArgs(
val emailAddress: String,
val password: String?,
val orgIdentifier: String?,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
emailAddress = checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
password = savedStateHandle.get<String>(PASSWORD)?.base64UrlDecodeOrNull(),
orgIdentifier = savedStateHandle.get<String>(ORG_IDENTIFIER)?.base64UrlDecodeOrNull(),
)
}
@ -34,10 +42,13 @@ data class TwoFactorLoginArgs(val emailAddress: String, val password: String?) {
fun NavController.navigateToTwoFactorLogin(
emailAddress: String,
password: String?,
orgIdentifier: String?,
navOptions: NavOptions? = null,
) {
this.navigate(
route = "$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?$PASSWORD=${password?.base64UrlEncode()}",
route = "$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?" +
"$PASSWORD=${password?.base64UrlEncode()}&" +
"$ORG_IDENTIFIER=${orgIdentifier?.base64UrlEncode()}",
navOptions = navOptions,
)
}
@ -56,6 +67,10 @@ fun NavGraphBuilder.twoFactorLoginDestination(
type = NavType.StringType
nullable = true
},
navArgument(ORG_IDENTIFIER) {
type = NavType.StringType
nullable = true
},
),
) {
TwoFactorLoginScreen(

View file

@ -301,25 +301,24 @@ private fun TwoFactorLoginScreenContent(
@Preview(showBackground = true)
private fun TwoFactorLoginScreenContentPreview() {
BitwardenTheme {
BitwardenScaffold {
TwoFactorLoginScreenContent(
state = TwoFactorLoginState(
TwoFactorAuthMethod.EMAIL,
availableAuthMethods = listOf(TwoFactorAuthMethod.EMAIL),
codeInput = "",
dialogState = null,
displayEmail = "email@dot.com",
isContinueButtonEnabled = true,
isRememberMeEnabled = true,
captchaToken = null,
email = "",
password = "",
),
onCodeInputChange = {},
onContinueButtonClick = {},
onRememberMeToggle = {},
onResendEmailButtonClick = {},
)
}
TwoFactorLoginScreenContent(
state = TwoFactorLoginState(
TwoFactorAuthMethod.EMAIL,
availableAuthMethods = listOf(TwoFactorAuthMethod.EMAIL),
codeInput = "",
dialogState = null,
displayEmail = "email@dot.com",
isContinueButtonEnabled = true,
isRememberMeEnabled = true,
captchaToken = null,
email = "",
password = "",
orgIdentifier = null,
),
onCodeInputChange = {},
onContinueButtonClick = {},
onRememberMeToggle = {},
onResendEmailButtonClick = {},
)
}
}

View file

@ -72,6 +72,7 @@ class TwoFactorLoginViewModel @Inject constructor(
captchaToken = null,
email = args.emailAddress,
password = args.password,
orgIdentifier = args.orgIdentifier,
)
},
) {
@ -539,6 +540,7 @@ class TwoFactorLoginViewModel @Inject constructor(
remember = state.isRememberMeEnabled,
),
captchaToken = state.captchaToken,
orgIdentifier = state.orgIdentifier,
)
sendAction(
TwoFactorLoginAction.Internal.ReceiveLoginResult(
@ -565,6 +567,7 @@ data class TwoFactorLoginState(
val captchaToken: String?,
val email: String,
val password: String?,
val orgIdentifier: String?,
) : Parcelable {
/**

View file

@ -1998,6 +1998,7 @@ class AuthRepositoryTest {
password = PASSWORD,
twoFactorData = TWO_FACTOR_DATA,
captchaToken = null,
orgIdentifier = null,
)
assertEquals(LoginResult.Success, finalResult)
assertNull(repository.twoFactorResponse)
@ -2092,6 +2093,7 @@ class AuthRepositoryTest {
password = PASSWORD,
twoFactorData = TWO_FACTOR_DATA,
captchaToken = null,
orgIdentifier = null,
)
assertEquals(LoginResult.Error(errorMessage = null), finalResult)
assertEquals(twoFactorResponse, repository.twoFactorResponse)
@ -2203,6 +2205,7 @@ class AuthRepositoryTest {
password = PASSWORD,
twoFactorData = TWO_FACTOR_DATA,
captchaToken = null,
orgIdentifier = null,
)
assertEquals(LoginResult.Error(errorMessage = null), result)
}
@ -2667,6 +2670,7 @@ class AuthRepositoryTest {
password = null,
twoFactorData = TWO_FACTOR_DATA,
captchaToken = null,
orgIdentifier = null,
)
assertEquals(LoginResult.Success, finalResult)
assertNull(repository.twoFactorResponse)
@ -3832,6 +3836,7 @@ class AuthRepositoryTest {
password = null,
twoFactorData = TWO_FACTOR_DATA,
captchaToken = null,
orgIdentifier = null,
)
assertEquals(LoginResult.Success, finalResult)
assertNull(repository.twoFactorResponse)

View file

@ -31,7 +31,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
class EnterpriseSignOnScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateToSetPasswordCalled = false
private var twoFactorLoginEmail: String? = null
private var onNavigateToTwoFactorLoginEmailAndOrgIdentifier: Pair<String, String>? = null
private val mutableEventFlow = bufferedMutableSharedFlow<EnterpriseSignOnEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<EnterpriseSignOnViewModel>(relaxed = true) {
@ -49,7 +49,9 @@ class EnterpriseSignOnScreenTest : BaseComposeTest() {
EnterpriseSignOnScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToSetPassword = { onNavigateToSetPasswordCalled = true },
onNavigateToTwoFactorLogin = { twoFactorLoginEmail = it },
onNavigateToTwoFactorLogin = { email, orgIdentifier ->
onNavigateToTwoFactorLoginEmailAndOrgIdentifier = email to orgIdentifier
},
viewModel = viewModel,
intentManager = intentManager,
)
@ -125,8 +127,11 @@ class EnterpriseSignOnScreenTest : BaseComposeTest() {
@Test
fun `NavigateToTwoFactorLogin should call onNavigateToTwoFactorLogin`() {
val email = "test@example.com"
mutableEventFlow.tryEmit(EnterpriseSignOnEvent.NavigateToTwoFactorLogin(email))
assertEquals(email, twoFactorLoginEmail)
val orgIdentifier = "org_identifier"
mutableEventFlow.tryEmit(
EnterpriseSignOnEvent.NavigateToTwoFactorLogin(email, orgIdentifier),
)
assertEquals(email to orgIdentifier, onNavigateToTwoFactorLoginEmailAndOrgIdentifier)
}
@Test

View file

@ -523,7 +523,7 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() {
)
assertEquals(
EnterpriseSignOnEvent.NavigateToTwoFactorLogin("test@gmail.com"),
EnterpriseSignOnEvent.NavigateToTwoFactorLogin("test@gmail.com", "Bitwarden"),
eventFlow.awaitItem(),
)
}

View file

@ -294,4 +294,5 @@ private val DEFAULT_STATE = TwoFactorLoginState(
captchaToken = null,
email = "example@email.com",
password = "password123",
orgIdentifier = "orgIdentifier",
)

View file

@ -71,6 +71,9 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
every {
DEFAULT_ENCODED_PASSWORD.base64UrlDecodeOrNull()
} returns DEFAULT_PASSWORD
every {
DEFAULT_ENCODED_ORG_IDENTIFIER.base64UrlDecodeOrNull()
} returns DEFAULT_ORG_IDENTIFIER
}
@AfterEach
@ -115,7 +118,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
val initialState = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.WEB_AUTH)
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = token,
@ -123,6 +126,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Success
val viewModel = createViewModel(state = initialState)
@ -135,7 +139,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
)
coVerify(exactly = 1) {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = token,
@ -143,6 +147,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -166,7 +171,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
fun `captchaTokenFlow success update should trigger a login`() = runTest {
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -174,13 +179,14 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = "token",
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Success
createViewModel()
mutableCaptchaTokenResultFlow.tryEmit(CaptchaCallbackTokenResult.Success("token"))
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -188,6 +194,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = "token",
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -196,7 +203,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
fun `duoTokenResultFlow success update should trigger a login`() = runTest {
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "token",
@ -204,6 +211,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Success
createViewModel(
@ -214,7 +222,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
mutableDuoTokenResultFlow.tryEmit(DuoCallbackTokenResult.Success("token"))
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "token",
@ -222,6 +230,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -282,7 +291,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
fun `ContinueButtonClick login returns success should update loadingDialogState`() = runTest {
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -290,6 +299,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Success
@ -313,7 +323,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -321,6 +331,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -478,7 +489,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
} returns mockkUri
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -486,6 +497,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.CaptchaRequired(captchaId = "mock_captcha_id")
val viewModel = createViewModel()
@ -504,7 +516,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -512,6 +524,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -520,7 +533,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
fun `ContinueButtonClick login returns Error should update dialogState`() = runTest {
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -528,6 +541,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Error(errorMessage = null)
@ -560,7 +574,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -568,6 +582,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -577,7 +592,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
runTest {
coEvery {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -585,6 +600,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
} returns LoginResult.Error(errorMessage = "Mock error message")
@ -617,7 +633,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
}
coVerify {
authRepository.login(
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
twoFactorData = TwoFactorDataModel(
code = "",
@ -625,6 +641,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
remember = false,
),
captchaToken = null,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)
}
}
@ -858,8 +875,9 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
resourceManager = resourceManager,
savedStateHandle = SavedStateHandle().also {
it["state"] = state
it["email_address"] = "example@email.com"
it["email_address"] = DEFAULT_EMAIL_ADDRESS
it["password"] = DEFAULT_ENCODED_PASSWORD
it["org_identifier"] = DEFAULT_ENCODED_ORG_IDENTIFIER
},
)
}
@ -877,6 +895,9 @@ private val TWO_FACTOR_RESPONSE = GetTokenResponseJson.TwoFactorRequired(
ssoToken = null,
twoFactorProviders = null,
)
private const val DEFAULT_EMAIL_ADDRESS = "example@email.com"
private const val DEFAULT_ORG_IDENTIFIER = "org_identifier"
private const val DEFAULT_ENCODED_ORG_IDENTIFIER = "org_identifier"
private const val DEFAULT_PASSWORD = "password123"
private const val DEFAULT_ENCODED_PASSWORD = "base64EncodedPassword"
private val DEFAULT_STATE = TwoFactorLoginState(
@ -892,6 +913,7 @@ private val DEFAULT_STATE = TwoFactorLoginState(
isContinueButtonEnabled = false,
isRememberMeEnabled = false,
captchaToken = null,
email = "example@email.com",
email = DEFAULT_EMAIL_ADDRESS,
password = DEFAULT_PASSWORD,
orgIdentifier = DEFAULT_ORG_IDENTIFIER,
)