Skip to content

Commit

Permalink
TIQR-443: One time password screen
Browse files Browse the repository at this point in the history
When there's no internet connection, we show a screen with a one-time password, which can be used to log in on the website
  • Loading branch information
dzolnai committed May 30, 2024
1 parent 7ecc89c commit 4d7d9a7
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 17 deletions.
23 changes: 22 additions & 1 deletion app/src/main/kotlin/nl/eduid/graphs/AuthGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import nl.eduid.screens.authorize.AuthenticationCompletedScreen
import nl.eduid.screens.authorize.AuthenticationPinBiometricScreen
import nl.eduid.screens.authorize.EduIdAuthenticationViewModel
import nl.eduid.screens.authorize.RequestAuthenticationScreen
import nl.eduid.screens.onetimepassword.OneTimePasswordScreen
import org.tiqr.data.viewmodel.AuthenticationViewModel

fun NavGraphBuilder.authenticationFlow(navController: NavHostController) {
navigation(
Expand Down Expand Up @@ -42,13 +44,32 @@ fun NavGraphBuilder.authenticationFlow(navController: NavHostController) {
)
)
}
}) { navController.popBackStack() }
},
goToOneTimePassword = { challenge, pin ->
if (challenge != null) {
val encodedChallenge = viewModel.encodeChallenge(challenge)
navController.goToWithPopCurrent(
Account.OneTimePassword.buildRoute(
encodedChallenge = encodedChallenge,
pin = pin
)
)
}
}
) { navController.popBackStack() }
}
composable(
route = Account.AuthenticationCompleted.routeWithArgs,
arguments = Account.AuthenticationCompleted.arguments,
) { _ ->
AuthenticationCompletedScreen { navController.popBackStack() }
}
composable(
route = Account.OneTimePassword.routeWithArgs,
arguments = Account.OneTimePassword.arguments,
) { entry ->
val viewModel = hiltViewModel<EduIdAuthenticationViewModel>(entry)
OneTimePasswordScreen(viewModel = viewModel) { navController.popBackStack() }
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import nl.eduid.graphs.RequestEduIdLinkSent.LOGIN_REASON
import nl.eduid.graphs.RequestEduIdLinkSent.reasonArg
import nl.eduid.screens.accountlinked.AccountLinkedScreen
import nl.eduid.screens.accountlinked.ResultAccountLinked
import nl.eduid.screens.authorize.EduIdAuthenticationViewModel
import nl.eduid.screens.biometric.EnableBiometricScreen
import nl.eduid.screens.biometric.EnableBiometricViewModel
import nl.eduid.screens.contbrowser.ContinueInBrowserScreen
Expand Down Expand Up @@ -62,6 +63,7 @@ import nl.eduid.screens.twofactorkey.TwoFactorKeyViewModel
import nl.eduid.screens.twofactorkeydelete.TwoFactorKeyDeleteScreen
import nl.eduid.screens.twofactorkeydelete.TwoFactorKeyDeleteViewModel
import org.tiqr.data.model.EnrollmentChallenge
import org.tiqr.data.viewmodel.AuthenticationViewModel

@OptIn(ExperimentalAnimationApi::class)
@Composable
Expand All @@ -72,6 +74,7 @@ fun MainGraph(
) {
composable(Graph.HOME_PAGE) {//region Home
val viewModel = hiltViewModel<HomePageViewModel>(it)

HomePageScreen(viewModel = viewModel,
onScanForAuthorization = { navController.navigate(Account.ScanQR.routeForAuth) },
onActivityClicked = { navController.navigate(Graph.DATA_AND_ACTIVITY) },
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/kotlin/nl/eduid/graphs/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ sealed class Account(val route: String) {

}

object OneTimePassword : Account("one_time_password") {
private const val challengeArg = "challenge_arg"
const val pinArg = "pin_arg"

val routeWithArgs = "$route/{$challengeArg}/{$pinArg}"
val arguments = listOf(navArgument(challengeArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
}, navArgument(pinArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
})
fun buildRoute(encodedChallenge: String, pin: String): String = "${route}/$encodedChallenge/$pin"
}


//https://eduid.nl/tiqrenroll/?metadata=https%3A%2F%2Flogin.test2.eduid.nl%2Ftiqr%2Fmetadata%3Fenrollment_key%3Dd47fa31400084edc043f8c547c5ed3f6b18d69f5a71f422519911f034b865f96153c8fc1507d81bc05aba95d095489a8d0400909f8aab348e2ac1786b28db572
object DeepLink : Account("deeplinks") {
const val enrollPattern = "https://eduid.nl/tiqrenroll/?metadata="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import timber.log.Timber
fun AuthenticationPinBiometricScreen(
viewModel: EduIdAuthenticationViewModel,
goToAuthenticationComplete: (AuthenticationChallenge?, String) -> Unit,
goToOneTimePassword: (AuthenticationChallenge?, String) -> Unit,
onCancel: () -> Unit,
) = EduIdTopAppBar(
withBackIcon = false
Expand All @@ -74,6 +75,9 @@ fun AuthenticationPinBiometricScreen(
goToAuthenticationComplete(authChallenge, pin)
},
clearCompleteChallenge = viewModel::clearCompleteChallenge,
goToOneTimePassword = { pin ->
goToOneTimePassword(authChallenge, pin)
},
goHomeOnFail = onCancel,
)
}
Expand All @@ -84,12 +88,13 @@ private fun AuthenticationPinBiometricContent(
challengeComplete: ChallengeCompleteResult<ChallengeCompleteFailure>? = null,
isPinInvalid: Boolean = false,
padding: PaddingValues = PaddingValues(),
onBiometricResult: (BiometricSignIn) -> Unit = {},
submitPin: (String) -> Unit = {},
onCancel: () -> Unit = {},
goToAuthenticationComplete: (String) -> Unit = {},
clearCompleteChallenge: () -> Unit = {},
goHomeOnFail: () -> Unit = {},
onBiometricResult: (BiometricSignIn) -> Unit,
submitPin: (String) -> Unit,
onCancel: () -> Unit,
goToAuthenticationComplete: (String) -> Unit ,
goToOneTimePassword: (String) -> Unit,
clearCompleteChallenge: () -> Unit,
goHomeOnFail: () -> Unit,
) {
var isCheckingSecret by rememberSaveable { mutableStateOf(false) }
var pinValue by rememberSaveable { mutableStateOf("") }
Expand All @@ -108,8 +113,7 @@ private fun AuthenticationPinBiometricContent(
AuthenticationCompleteFailure.Reason.UNKNOWN,
AuthenticationCompleteFailure.Reason.CONNECTION,
-> {
Timber.e("This should be a fallback to OTP")
TODO()
goToOneTimePassword(pinValue)
}

AuthenticationCompleteFailure.Reason.INVALID_RESPONSE -> {
Expand Down Expand Up @@ -250,5 +254,12 @@ private fun PreviewAuthorizePinBiometricScreen() = EduidAppAndroidTheme {
AuthenticationPinBiometricContent(
shouldPromptBiometric = false,
isPinInvalid = false,
goToAuthenticationComplete = {},
submitPin = {},
clearCompleteChallenge = {},
onBiometricResult = {},
goHomeOnFail = {},
goToOneTimePassword = {},
onCancel = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package nl.eduid.screens.authorize
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import com.squareup.moshi.Moshi
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -15,6 +18,7 @@ import org.tiqr.data.model.AuthenticationCompleteRequest
import org.tiqr.data.model.ChallengeCompleteFailure
import org.tiqr.data.model.ChallengeCompleteResult
import org.tiqr.data.model.SecretCredential
import org.tiqr.data.model.SecretType
import org.tiqr.data.repository.AuthenticationRepository
import timber.log.Timber
import javax.inject.Inject
Expand All @@ -30,6 +34,19 @@ class EduIdAuthenticationViewModel @Inject constructor(
val challengeComplete =
MutableLiveData<ChallengeCompleteResult<ChallengeCompleteFailure>?>(null)

private val _otpGenerate = MutableLiveData<SecretCredential>()
val otp = _otpGenerate.switchMap { credential ->
liveData {
challenge.value?.let { challenge ->
challenge.identity?.let {
emit(repository.completeOtp(credential, it, challenge))
}
}
}
}

val userId = challenge.map { it?.identity?.identifier }

init {
val authorizeChallenge =
savedStateHandle.get<String>(Account.RequestAuthentication.challengeArg) ?: ""
Expand All @@ -47,7 +64,10 @@ class EduIdAuthenticationViewModel @Inject constructor(
// )
null
}

val pin = savedStateHandle.get<String>(Account.OneTimePassword.pinArg)
if (pin != null) {
generateOTP(pin)
}
}

fun clearCompleteChallenge() {
Expand All @@ -68,4 +88,13 @@ class EduIdAuthenticationViewModel @Inject constructor(
challengeComplete.postValue(challengeResult)
}

/**
* Perform OTP generation
*/
private fun generateOTP(password: String) {
val type =
if (password == SecretType.BIOMETRIC.key) SecretType.BIOMETRIC else SecretType.PIN
_otpGenerate.value = SecretCredential(password = password, type = type)
}

}
Loading

0 comments on commit 4d7d9a7

Please sign in to comment.