mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-1917: Add Duo 2-factor authentication (#1036)
This commit is contained in:
parent
946565ae54
commit
1953c40b26
15 changed files with 485 additions and 60 deletions
|
@ -78,6 +78,16 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="duo-callback"
|
||||
android:scheme="bitwarden" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="sso-callback"
|
||||
android:scheme="bitwarden" />
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden
|
|||
import android.content.Intent
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getYubiKeyResultOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -25,6 +26,7 @@ class AuthCallbackViewModel @Inject constructor(
|
|||
private fun handleIntentReceived(action: AuthCallbackAction.IntentReceive) {
|
||||
val yubiKeyResult = action.intent.getYubiKeyResultOrNull()
|
||||
val captchaCallbackTokenResult = action.intent.getCaptchaCallbackTokenResult()
|
||||
val duoCallbackTokenResult = action.intent.getDuoCallbackTokenResult()
|
||||
val ssoCallbackResult = action.intent.getSsoCallbackResult()
|
||||
when {
|
||||
yubiKeyResult != null -> {
|
||||
|
@ -37,6 +39,12 @@ class AuthCallbackViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
duoCallbackTokenResult != null -> {
|
||||
authRepository.setDuoCallbackTokenResult(
|
||||
tokenResult = duoCallbackTokenResult,
|
||||
)
|
||||
}
|
||||
|
||||
ssoCallbackResult != null -> {
|
||||
authRepository.setSsoCallbackResult(
|
||||
result = ssoCallbackResult,
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.network.util
|
|||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
|
@ -28,6 +29,18 @@ val GetTokenResponseJson.TwoFactorRequired?.preferredAuthMethod: TwoFactorAuthMe
|
|||
?.maxByOrNull { it.priority }
|
||||
?: TwoFactorAuthMethod.EMAIL
|
||||
|
||||
/**
|
||||
* If it exists, return the value of the Duo auth url.
|
||||
*/
|
||||
val GetTokenResponseJson.TwoFactorRequired?.twoFactorDuoAuthUrl: String
|
||||
get() = this
|
||||
?.authMethodsData
|
||||
?.duo
|
||||
?.get("AuthUrl")
|
||||
?.jsonPrimitive
|
||||
?.contentOrNull
|
||||
.orEmpty()
|
||||
|
||||
/**
|
||||
* If it exists, return the value to display for the email used with two-factor authentication.
|
||||
*/
|
||||
|
@ -38,4 +51,11 @@ val GetTokenResponseJson.TwoFactorRequired?.twoFactorDisplayEmail: String
|
|||
?.get("Email")
|
||||
?.jsonPrimitive
|
||||
?.contentOrNull
|
||||
?: ""
|
||||
.orEmpty()
|
||||
|
||||
/**
|
||||
* Gets the [TwoFactorAuthMethod.DUO] [JsonObject], if it exists, else the
|
||||
* [TwoFactorAuthMethod.DUO_ORGANIZATION] [JsonObject], if that exists.
|
||||
*/
|
||||
private val Map<TwoFactorAuthMethod, JsonObject?>.duo: JsonObject?
|
||||
get() = get(TwoFactorAuthMethod.DUO) ?: get(TwoFactorAuthMethod.DUO_ORGANIZATION)
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
|
||||
|
@ -48,6 +49,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
*/
|
||||
val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult>
|
||||
|
||||
/**
|
||||
* Flow of the current [DuoCallbackTokenResult]. Subscribers should listen to the flow
|
||||
* in order to receive updates whenever [setDuoCallbackTokenResult] is called.
|
||||
*/
|
||||
val duoTokenResultFlow: Flow<DuoCallbackTokenResult>
|
||||
|
||||
/**
|
||||
* Flow of the current [SsoCallbackResult]. Subscribers should listen to the flow in order to
|
||||
* receive updates whenever [setSsoCallbackResult] is called.
|
||||
|
@ -206,6 +213,11 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
*/
|
||||
fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult)
|
||||
|
||||
/**
|
||||
* Set the value of [duoTokenResultFlow].
|
||||
*/
|
||||
fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult)
|
||||
|
||||
/**
|
||||
* Set the value of [yubiKeyResultFlow].
|
||||
*/
|
||||
|
|
|
@ -49,6 +49,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
|
@ -210,6 +211,10 @@ class AuthRepositoryImpl(
|
|||
override val captchaTokenResultFlow: Flow<CaptchaCallbackTokenResult> =
|
||||
captchaTokenChannel.receiveAsFlow()
|
||||
|
||||
private val duoTokenChannel = Channel<DuoCallbackTokenResult>(capacity = Int.MAX_VALUE)
|
||||
override val duoTokenResultFlow: Flow<DuoCallbackTokenResult> =
|
||||
duoTokenChannel.receiveAsFlow()
|
||||
|
||||
private val yubiKeyResultChannel = Channel<YubiKeyResult>(capacity = Int.MAX_VALUE)
|
||||
override val yubiKeyResultFlow: Flow<YubiKeyResult> = yubiKeyResultChannel.receiveAsFlow()
|
||||
|
||||
|
@ -770,6 +775,10 @@ class AuthRepositoryImpl(
|
|||
captchaTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
|
||||
override fun setDuoCallbackTokenResult(tokenResult: DuoCallbackTokenResult) {
|
||||
duoTokenChannel.trySend(tokenResult)
|
||||
}
|
||||
|
||||
override fun setYubiKeyResult(yubiKeyResult: YubiKeyResult) {
|
||||
yubiKeyResultChannel.trySend(yubiKeyResult)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
private const val DUO_HOST: String = "duo-callback"
|
||||
|
||||
/**
|
||||
* Retrieves a [DuoCallbackTokenResult] from an Intent. There are three possible cases.
|
||||
*
|
||||
* - [null]: Intent is not a Duo callback, or data is null.
|
||||
*
|
||||
* - [DuoCallbackTokenResult.MissingToken]: Intent is the Duo callback, but it's missing the code or
|
||||
* state value.
|
||||
*
|
||||
* - [DuoCallbackTokenResult.Success]: Intent is the Duo callback, and it has a token.
|
||||
*/
|
||||
fun Intent.getDuoCallbackTokenResult(): DuoCallbackTokenResult? {
|
||||
val localData = data
|
||||
return if (
|
||||
action == Intent.ACTION_VIEW && localData != null && localData.host == DUO_HOST
|
||||
) {
|
||||
val code = localData.getQueryParameter("code")
|
||||
val state = localData.getQueryParameter("state")
|
||||
if (code != null && state != null) {
|
||||
DuoCallbackTokenResult.Success(token = "$code|$state")
|
||||
} else {
|
||||
DuoCallbackTokenResult.MissingToken
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed class representing the result of Duo callback token extraction.
|
||||
*/
|
||||
sealed class DuoCallbackTokenResult {
|
||||
/**
|
||||
* Represents a missing token in the Duo callback.
|
||||
*/
|
||||
data object MissingToken : DuoCallbackTokenResult()
|
||||
|
||||
/**
|
||||
* Represents a token present in the Duo callback.
|
||||
*/
|
||||
data class Success(val token: String) : DuoCallbackTokenResult()
|
||||
}
|
|
@ -100,6 +100,10 @@ fun TwoFactorLoginScreen(
|
|||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is TwoFactorLoginEvent.NavigateToDuo -> {
|
||||
intentManager.startCustomTabsActivity(uri = event.uri)
|
||||
}
|
||||
|
||||
is TwoFactorLoginEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -191,6 +195,7 @@ private fun TwoFactorLoginDialogs(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun TwoFactorLoginScreenContent(
|
||||
state: TwoFactorLoginState,
|
||||
|
@ -218,19 +223,21 @@ private fun TwoFactorLoginScreenContent(
|
|||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
BitwardenPasswordField(
|
||||
value = state.codeInput,
|
||||
onValueChange = onCodeInputChange,
|
||||
label = stringResource(id = R.string.verification_code),
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done,
|
||||
autoFocus = true,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
if (state.shouldShowCodeInput) {
|
||||
BitwardenPasswordField(
|
||||
value = state.codeInput,
|
||||
onValueChange = onCodeInputChange,
|
||||
label = stringResource(id = R.string.verification_code),
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = ImeAction.Done,
|
||||
autoFocus = true,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
BitwardenWideSwitch(
|
||||
label = stringResource(id = R.string.remember_me),
|
||||
|
@ -244,7 +251,7 @@ private fun TwoFactorLoginScreenContent(
|
|||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
label = state.buttonText(),
|
||||
onClick = onContinueButtonClick,
|
||||
isEnabled = state.isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
|
|
|
@ -10,12 +10,15 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.util.availableAuthMethods
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.preferredAuthMethod
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.twoFactorDisplayEmail
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.util.twoFactorDuoAuthUrl
|
||||
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.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.isDuo
|
||||
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util.shouldUseNfc
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
@ -47,7 +50,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
codeInput = "",
|
||||
displayEmail = authRepository.twoFactorResponse.twoFactorDisplayEmail,
|
||||
dialogState = null,
|
||||
isContinueButtonEnabled = false,
|
||||
isContinueButtonEnabled = authRepository.twoFactorResponse.preferredAuthMethod.isDuo,
|
||||
isRememberMeEnabled = false,
|
||||
captchaToken = null,
|
||||
email = TwoFactorLoginArgs(savedStateHandle).emailAddress,
|
||||
|
@ -66,6 +69,14 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
.map { TwoFactorLoginAction.Internal.ReceiveCaptchaToken(tokenResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Process the Duo result when it is received.
|
||||
authRepository
|
||||
.duoTokenResultFlow
|
||||
.map { TwoFactorLoginAction.Internal.ReceiveDuoResult(duoResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
// Fill in the verification code input field when a Yubi Key code is received.
|
||||
authRepository
|
||||
.yubiKeyResultFlow
|
||||
|
@ -94,6 +105,10 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
handleCaptchaTokenReceived(action)
|
||||
}
|
||||
|
||||
is TwoFactorLoginAction.Internal.ReceiveDuoResult -> {
|
||||
handleReceiveDuoResult(action)
|
||||
}
|
||||
|
||||
is TwoFactorLoginAction.Internal.ReceiveYubiKeyResult -> {
|
||||
handleReceiveYubiKeyResult(action)
|
||||
}
|
||||
|
@ -123,7 +138,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
mutableStateFlow.update {
|
||||
it.copy(captchaToken = tokenResult.token)
|
||||
}
|
||||
handleContinueButtonClick()
|
||||
initiateLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,49 +156,17 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
/**
|
||||
* Verify the input and attempt to authenticate with the code.
|
||||
* Navigates to the Duo webpage if appropriate, else processes the login.
|
||||
*/
|
||||
private fun handleContinueButtonClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// If the user is manually entering a code, remove any white spaces, just in case.
|
||||
val code = when (state.authMethod) {
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP,
|
||||
TwoFactorAuthMethod.EMAIL,
|
||||
-> state.codeInput.replace(" ", "")
|
||||
|
||||
TwoFactorAuthMethod.YUBI_KEY,
|
||||
TwoFactorAuthMethod.DUO,
|
||||
TwoFactorAuthMethod.U2F,
|
||||
TwoFactorAuthMethod.REMEMBER,
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION,
|
||||
TwoFactorAuthMethod.FIDO_2_WEB_APP,
|
||||
TwoFactorAuthMethod.RECOVERY_CODE,
|
||||
-> state.codeInput
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.login(
|
||||
email = state.email,
|
||||
password = state.password,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = code,
|
||||
method = state.authMethod.value.toString(),
|
||||
remember = state.isRememberMeEnabled,
|
||||
),
|
||||
captchaToken = state.captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
TwoFactorLoginAction.Internal.ReceiveLoginResult(
|
||||
loginResult = result,
|
||||
if (state.authMethod.isDuo) {
|
||||
sendEvent(
|
||||
event = TwoFactorLoginEvent.NavigateToDuo(
|
||||
uri = Uri.parse(authRepository.twoFactorResponse.twoFactorDuoAuthUrl),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
initiateLogin()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,6 +221,35 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the Duo callback result.
|
||||
*/
|
||||
private fun handleReceiveDuoResult(
|
||||
action: TwoFactorLoginAction.Internal.ReceiveDuoResult,
|
||||
) {
|
||||
when (val result = action.duoResult) {
|
||||
is DuoCallbackTokenResult.MissingToken -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is DuoCallbackTokenResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
codeInput = result.token,
|
||||
)
|
||||
}
|
||||
initiateLogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Yubi Key result.
|
||||
*/
|
||||
|
@ -339,6 +351,53 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the input and attempt to authenticate with the code.
|
||||
*/
|
||||
private fun initiateLogin() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TwoFactorLoginState.DialogState.Loading(
|
||||
message = R.string.logging_in.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// If the user is manually entering a code, remove any white spaces, just in case.
|
||||
val code = when (state.authMethod) {
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP,
|
||||
TwoFactorAuthMethod.EMAIL,
|
||||
-> state.codeInput.replace(" ", "")
|
||||
|
||||
TwoFactorAuthMethod.DUO,
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION,
|
||||
TwoFactorAuthMethod.YUBI_KEY,
|
||||
TwoFactorAuthMethod.U2F,
|
||||
TwoFactorAuthMethod.REMEMBER,
|
||||
TwoFactorAuthMethod.FIDO_2_WEB_APP,
|
||||
TwoFactorAuthMethod.RECOVERY_CODE,
|
||||
-> state.codeInput
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.login(
|
||||
email = state.email,
|
||||
password = state.password,
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = code,
|
||||
method = state.authMethod.value.toString(),
|
||||
remember = state.isRememberMeEnabled,
|
||||
),
|
||||
captchaToken = state.captchaToken,
|
||||
)
|
||||
sendAction(
|
||||
TwoFactorLoginAction.Internal.ReceiveLoginResult(
|
||||
loginResult = result,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -359,11 +418,26 @@ data class TwoFactorLoginState(
|
|||
val password: String?,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* The text to display for the button given the [authMethod].
|
||||
*/
|
||||
val buttonText: Text
|
||||
get() = if (authMethod.isDuo) {
|
||||
"Launch Duo".asText() // TODO BIT-1927 replace with string resource
|
||||
} else {
|
||||
R.string.continue_text.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the screen should be listening for NFC events from the operating system.
|
||||
*/
|
||||
val shouldListenForNfc: Boolean get() = authMethod.shouldUseNfc
|
||||
|
||||
/**
|
||||
* Indicates whether the code input should be displayed.
|
||||
*/
|
||||
val shouldShowCodeInput: Boolean get() = !authMethod.isDuo
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
*/
|
||||
|
@ -402,6 +476,11 @@ sealed class TwoFactorLoginEvent {
|
|||
*/
|
||||
data class NavigateToCaptcha(val uri: Uri) : TwoFactorLoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the Duo 2-factor authentication screen.
|
||||
*/
|
||||
data class NavigateToDuo(val uri: Uri) : TwoFactorLoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the recovery code help page.
|
||||
*/
|
||||
|
@ -471,6 +550,13 @@ sealed class TwoFactorLoginAction {
|
|||
val tokenResult: CaptchaCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a Dup callback token has been received.
|
||||
*/
|
||||
data class ReceiveDuoResult(
|
||||
val duoResult: DuoCallbackTokenResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates a Yubi Key result has been received.
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.x8bit.bitwarden.R
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
|
||||
/**
|
||||
* Get the title for the given auth method.
|
||||
|
@ -11,6 +12,10 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
|||
val TwoFactorAuthMethod.title: Text
|
||||
get() = when (this) {
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP -> R.string.authenticator_app_title.asText()
|
||||
TwoFactorAuthMethod.DUO -> "Duo".asText() // TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION -> "Duo (".asText()
|
||||
.concat(R.string.organization.asText())
|
||||
.concat(")".asText()) // TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.EMAIL -> R.string.email.asText()
|
||||
TwoFactorAuthMethod.RECOVERY_CODE -> R.string.recovery_code_title.asText()
|
||||
TwoFactorAuthMethod.YUBI_KEY -> R.string.yubi_key_title.asText()
|
||||
|
@ -22,11 +27,27 @@ val TwoFactorAuthMethod.title: Text
|
|||
*/
|
||||
fun TwoFactorAuthMethod.description(email: String): Text = when (this) {
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP -> R.string.enter_verification_code_app.asText()
|
||||
TwoFactorAuthMethod.DUO,
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION,
|
||||
-> "Follow the steps from Duo to finish logging in."
|
||||
.asText() // TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.EMAIL -> R.string.enter_verification_code_email.asText(email)
|
||||
TwoFactorAuthMethod.YUBI_KEY -> R.string.yubi_key_instruction.asText()
|
||||
else -> "".asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a boolean indicating if the given auth method uses Duo.
|
||||
*/
|
||||
val TwoFactorAuthMethod.isDuo: Boolean
|
||||
get() = when (this) {
|
||||
TwoFactorAuthMethod.DUO,
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a boolean indicating if the given auth method uses NFC.
|
||||
*/
|
||||
|
|
|
@ -3,8 +3,10 @@ package com.x8bit.bitwarden
|
|||
import android.content.Intent
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getCaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getDuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.getSsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.auth.util.getYubiKeyResultOrNull
|
||||
|
@ -24,6 +26,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
private val authRepository = mockk<AuthRepository> {
|
||||
every { setCaptchaCallbackTokenResult(any()) } just runs
|
||||
every { setSsoCallbackResult(any()) } just runs
|
||||
every { setDuoCallbackTokenResult(any()) } just runs
|
||||
every { setYubiKeyResult(any()) } just runs
|
||||
}
|
||||
|
||||
|
@ -32,6 +35,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
mockkStatic(
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getCaptchaCallbackTokenResult,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
Intent::getSsoCallbackResult,
|
||||
)
|
||||
}
|
||||
|
@ -41,6 +45,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
unmockkStatic(
|
||||
Intent::getYubiKeyResultOrNull,
|
||||
Intent::getCaptchaCallbackTokenResult,
|
||||
Intent::getDuoCallbackTokenResult,
|
||||
Intent::getSsoCallbackResult,
|
||||
)
|
||||
}
|
||||
|
@ -51,6 +56,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
val mockIntent = mockk<Intent>()
|
||||
val captchaCallbackTokenResult = CaptchaCallbackTokenResult.Success(token = "mockk_token")
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns captchaCallbackTokenResult
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
|
@ -60,6 +66,22 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on IntentReceive with duo host should call setDuoCallbackToken`() {
|
||||
val viewModel = createViewModel()
|
||||
val mockIntent = mockk<Intent>()
|
||||
val duoCallbackTokenResult = DuoCallbackTokenResult.Success(token = "mockk_token")
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns duoCallbackTokenResult
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
authRepository.setDuoCallbackTokenResult(tokenResult = duoCallbackTokenResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on IntentReceive with sso host should call setSsoCallbackResult`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -71,6 +93,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
every { mockIntent.getSsoCallbackResult() } returns sseCallbackResult
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns null
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
verify(exactly = 1) {
|
||||
|
@ -85,6 +108,7 @@ class AuthCallbackViewModelTest : BaseViewModelTest() {
|
|||
val yubiKeyResult = mockk<YubiKeyResult>()
|
||||
every { mockIntent.getYubiKeyResultOrNull() } returns yubiKeyResult
|
||||
every { mockIntent.getCaptchaCallbackTokenResult() } returns null
|
||||
every { mockIntent.getDuoCallbackTokenResult() } returns null
|
||||
every { mockIntent.getSsoCallbackResult() } returns null
|
||||
|
||||
viewModel.trySendAction(AuthCallbackAction.IntentReceive(intent = mockIntent))
|
||||
|
|
|
@ -31,6 +31,48 @@ class TwoFactorRequiredExtensionTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twoFactorDuoAuthUrl returns the expected value when auth method is DUO`() {
|
||||
val subject = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("AuthUrl" to JsonPrimitive("Bitwarden")),
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
assertEquals("Bitwarden", subject.twoFactorDuoAuthUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twoFactorDuoAuthUrl returns the expected value when auth method is DUO_ORGANIZATION`() {
|
||||
val subject = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to JsonObject(
|
||||
mapOf("AuthUrl" to JsonPrimitive("Bitwarden")),
|
||||
),
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
assertEquals("Bitwarden", subject.twoFactorDuoAuthUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twoFactorDuoAuthUrl returns empty string when no DUO AuthUrl is present`() {
|
||||
val subject = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to JsonObject(mapOf("AuthUrl" to JsonNull)),
|
||||
),
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
assertEquals("", subject.twoFactorDuoAuthUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `twoFactorDisplayEmail returns the expected value`() {
|
||||
val subject = GetTokenResponseJson.TwoFactorRequired(
|
||||
|
|
|
@ -59,6 +59,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
|
@ -2461,7 +2462,7 @@ class AuthRepositoryTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `setCaptchaCallbackToken should change the value of captchaTokenFlow`() = runTest {
|
||||
fun `setCaptchaCallbackToken should change the value of captchaTokenResultFlow`() = runTest {
|
||||
repository.captchaTokenResultFlow.test {
|
||||
repository.setCaptchaCallbackTokenResult(CaptchaCallbackTokenResult.Success("mockk"))
|
||||
assertEquals(
|
||||
|
@ -2471,6 +2472,17 @@ class AuthRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setDuoCallbackToken should change the value of duoTokenResultFlow`() = runTest {
|
||||
repository.duoTokenResultFlow.test {
|
||||
repository.setDuoCallbackTokenResult(DuoCallbackTokenResult.Success("mockk"))
|
||||
assertEquals(
|
||||
DuoCallbackTokenResult.Success("mockk"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setSsoCallbackResult should change the value of ssoCallbackResultFlow`() = runTest {
|
||||
repository.ssoCallbackResultFlow.test {
|
||||
|
|
|
@ -112,6 +112,18 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithText("Continue").assertIsEnabled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `continue button text should update according to the state`() {
|
||||
composeTestRule.onNodeWithText("Continue").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(authMethod = TwoFactorAuthMethod.DUO)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("Launch Duo").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Continue").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `description text should update according to state`() {
|
||||
val emailDetails =
|
||||
|
@ -180,6 +192,16 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithText(buttonText).assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `input field visibility should update according to state`() {
|
||||
composeTestRule.onNodeWithText("Verification code").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(authMethod = TwoFactorAuthMethod.DUO)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Verification code").assertIsNotDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `options menu icon click should show the auth method options`() {
|
||||
composeTestRule.onNodeWithContentDescription("More").performClick()
|
||||
|
@ -224,6 +246,13 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
|
|||
verify { intentManager.startCustomTabsActivity(mockUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToDuo should call intentManager startCustomTabsActivity`() {
|
||||
val mockUri = mockk<Uri>()
|
||||
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToDuo(mockUri))
|
||||
verify { intentManager.startCustomTabsActivity(mockUri) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToRecoveryCode should launch the recovery code uri`() {
|
||||
mutableEventFlow.tryEmit(TwoFactorLoginEvent.NavigateToRecoveryCode)
|
||||
|
|
|
@ -11,6 +11,7 @@ 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.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
|
@ -22,6 +23,7 @@ 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
|
||||
|
@ -34,21 +36,26 @@ import org.junit.jupiter.api.Test
|
|||
class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
private val mutableCaptchaTokenResultFlow =
|
||||
bufferedMutableSharedFlow<CaptchaCallbackTokenResult>()
|
||||
private val mutableDuoTokenResultFlow =
|
||||
bufferedMutableSharedFlow<DuoCallbackTokenResult>()
|
||||
private val mutableYubiKeyResultFlow = bufferedMutableSharedFlow<YubiKeyResult>()
|
||||
private val authRepository: AuthRepository = mockk(relaxed = true) {
|
||||
every { twoFactorResponse } returns TWO_FACTOR_RESPONSE
|
||||
every { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow
|
||||
every { duoTokenResultFlow } returns mutableDuoTokenResultFlow
|
||||
every { yubiKeyResultFlow } returns mutableYubiKeyResultFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(::generateUriForCaptcha)
|
||||
mockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::generateUriForCaptcha)
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -104,6 +111,40 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `duoTokenResultFlow success update should trigger a login`() = runTest {
|
||||
coEvery {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "token",
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
)
|
||||
} returns LoginResult.Success
|
||||
createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
mutableDuoTokenResultFlow.tryEmit(DuoCallbackTokenResult.Success("token"))
|
||||
coVerify {
|
||||
authRepository.login(
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
twoFactorData = TwoFactorDataModel(
|
||||
code = "token",
|
||||
method = TwoFactorAuthMethod.DUO.value.toString(),
|
||||
remember = false,
|
||||
),
|
||||
captchaToken = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseButtonClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -208,6 +249,38 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login should emit NavigateToDuo when auth method is Duo`() = runTest {
|
||||
val authMethodsData = mapOf(
|
||||
TwoFactorAuthMethod.DUO to JsonObject(
|
||||
mapOf("AuthUrl" to JsonPrimitive("bitwarden.com")),
|
||||
),
|
||||
)
|
||||
val response = GetTokenResponseJson.TwoFactorRequired(
|
||||
authMethodsData = authMethodsData,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
)
|
||||
every { authRepository.twoFactorResponse } returns response
|
||||
val mockkUri = mockk<Uri>()
|
||||
val viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.DUO,
|
||||
),
|
||||
)
|
||||
every { Uri.parse("bitwarden.com") } returns mockkUri
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(TwoFactorLoginAction.ContinueButtonClick)
|
||||
assertEquals(
|
||||
TwoFactorLoginEvent.NavigateToDuo(mockkUri),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify {
|
||||
Uri.parse("bitwarden.com")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueButtonClick login returns CaptchaRequired should emit NavigateToCaptcha`() =
|
||||
runTest {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.util
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
@ -12,11 +13,14 @@ class TwoFactorAuthMethodExtensionTest {
|
|||
mapOf(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to R.string.authenticator_app_title.asText(),
|
||||
TwoFactorAuthMethod.EMAIL to R.string.email.asText(),
|
||||
TwoFactorAuthMethod.DUO to "".asText(),
|
||||
TwoFactorAuthMethod.DUO to "Duo".asText(), // TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.YUBI_KEY to R.string.yubi_key_title.asText(),
|
||||
TwoFactorAuthMethod.U2F to "".asText(),
|
||||
TwoFactorAuthMethod.REMEMBER to "".asText(),
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to "".asText(),
|
||||
// TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to "Duo (".asText()
|
||||
.concat(R.string.organization.asText())
|
||||
.concat(")".asText()),
|
||||
TwoFactorAuthMethod.FIDO_2_WEB_APP to "".asText(),
|
||||
TwoFactorAuthMethod.RECOVERY_CODE to R.string.recovery_code_title.asText(),
|
||||
)
|
||||
|
@ -35,11 +39,14 @@ class TwoFactorAuthMethodExtensionTest {
|
|||
R.string.enter_verification_code_app.asText(),
|
||||
TwoFactorAuthMethod.EMAIL to
|
||||
R.string.enter_verification_code_email.asText("ex***@email.com"),
|
||||
TwoFactorAuthMethod.DUO to "".asText(),
|
||||
// TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.DUO to "Follow the steps from Duo to finish logging in.".asText(),
|
||||
TwoFactorAuthMethod.YUBI_KEY to R.string.yubi_key_instruction.asText(),
|
||||
TwoFactorAuthMethod.U2F to "".asText(),
|
||||
TwoFactorAuthMethod.REMEMBER to "".asText(),
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to "".asText(),
|
||||
// TODO BIT-1927 replace with string resource
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to
|
||||
"Follow the steps from Duo to finish logging in.".asText(),
|
||||
TwoFactorAuthMethod.FIDO_2_WEB_APP to "".asText(),
|
||||
TwoFactorAuthMethod.RECOVERY_CODE to "".asText(),
|
||||
)
|
||||
|
@ -51,6 +58,24 @@ class TwoFactorAuthMethodExtensionTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isDuo returns the expected value`() {
|
||||
mapOf(
|
||||
TwoFactorAuthMethod.AUTHENTICATOR_APP to false,
|
||||
TwoFactorAuthMethod.EMAIL to false,
|
||||
TwoFactorAuthMethod.DUO to true,
|
||||
TwoFactorAuthMethod.YUBI_KEY to false,
|
||||
TwoFactorAuthMethod.U2F to false,
|
||||
TwoFactorAuthMethod.REMEMBER to false,
|
||||
TwoFactorAuthMethod.DUO_ORGANIZATION to true,
|
||||
TwoFactorAuthMethod.FIDO_2_WEB_APP to false,
|
||||
TwoFactorAuthMethod.RECOVERY_CODE to false,
|
||||
)
|
||||
.forEach { (type, isDuo) ->
|
||||
assertEquals(isDuo, type.isDuo)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldUseNfc returns the expected value`() {
|
||||
mapOf(
|
||||
|
|
Loading…
Add table
Reference in a new issue