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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ internal class LoginViewModel @Inject constructor(
code = "",
)
}
// nothing to type: an account whose only second factor is a passkey confirms
// with it right away
if (outcome.methods.isPasskeyOnly()) passkeyLogin()
}

is LoginOutcome.SignedIn -> {
Expand Down Expand Up @@ -330,5 +333,10 @@ internal class LoginViewModel @Inject constructor(

companion object {
const val TOTP_LENGTH = 6

/** whether the passkey is the only second factor the account has */
internal fun Set<SecondFactorMethod>.isPasskeyOnly() =
SecondFactorMethod.Passkey in this &&
SecondFactorMethod.Totp !in this && SecondFactorMethod.RecoveryCode !in this
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import de.markusressel.mkdocseditor.feature.auth.common.ui.compose.PasswordField
import de.markusressel.mkdocseditor.feature.auth.common.ui.compose.RecoveryCodeField
import de.markusressel.mkdocseditor.feature.auth.common.ui.compose.UsernameField
import de.markusressel.mkdocseditor.feature.auth.login.ui.LoginViewModel
import de.markusressel.mkdocseditor.feature.auth.login.ui.LoginViewModel.Companion.isPasskeyOnly
import de.markusressel.mkdocseditor.feature.auth.login.ui.LoginViewModel.UiEvent
import de.markusressel.mkdocseditor.feature.common.ui.compose.TabContentScaffold
import de.markusressel.mkdocseditor.feature.common.ui.compose.topbar.MkDocsEditorTopAppBar
Expand Down Expand Up @@ -179,6 +180,21 @@ private fun SecondFactorStep(
style = MaterialTheme.typography.titleMedium,
)

if (uiState.methods.isPasskeyOnly()) {
Text(stringResource(R.string.auth_passkey_second_factor_hint))
Button(
modifier = Modifier.fillMaxWidth(),
enabled = enabled,
onClick = { onUiEvent(UiEvent.PasskeyClicked) },
) {
Text(stringResource(R.string.auth_use_passkey))
}
TextButton(onClick = { onUiEvent(UiEvent.BackToPasswordClicked) }) {
Text(stringResource(R.string.auth_start_over))
}
return
}

if (uiState.useRecoveryCode) {
Text(stringResource(R.string.auth_recovery_code_hint))
RecoveryCodeField(
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings_auth.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<string name="auth_use_recovery_code">Use a recovery code</string>
<string name="auth_use_authenticator_code">Use the authenticator app</string>
<string name="auth_use_passkey">Use a passkey</string>
<string name="auth_passkey_second_factor_hint">Confirm the sign-in with your passkey.</string>
<string name="auth_disabled_hint">This backend runs without authentication.</string>
<string name="auth_setup_required_hint">This backend has no accounts yet. Open the setup link from the backend log (or run \"mkdocsrest setup-link\") and paste it below.</string>
<string name="auth_second_factor_setup_hint">This backend requires a second factor. Set up an authenticator app or a passkey in your account before you continue.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,19 @@ class LoginViewModelTest {
assertEquals(LoginEvent.PickCredential(api.ceremony.publicKey, includePasswords = false), events.last())
}

@Test
fun `passkey only accounts open the passkey sheet right after the password`() = runTest {
api.onLogin = { _, _ -> secondFactorRequired("passkey") }
start()
viewModel.onUiEvent(UiEvent.PasswordChanged("pw"))
viewModel.onUiEvent(UiEvent.LoginClicked)

assertEquals(LoginViewModel.Step.SecondFactor, viewModel.uiState.value.step)
assertTrue("beginPasskeyLogin:mdc_1" in api.calls)
assertEquals(LoginEvent.PickCredential(api.ceremony.publicKey, includePasswords = false), events.last())
assertTrue(viewModel.uiState.value.loading)
}

@Test
fun `a saved password from the provider signs in without a save prompt`() = runTest {
var usedPassword: String? = null
Expand Down
Loading