Skip to content

Commit

Permalink
Edured-77: Authentication flow implemented.
Browse files Browse the repository at this point in the history
Not available ATM:
1 - Multiple identities to select from
2 - Fallback when no network available: OTP via PIN
3 - Offer biometric upgrade when it wasn't asked for the current identity and biometric capabilities are available

Known issue: showing dialogs for authentication failure
  • Loading branch information
Iulia Stana committed Apr 6, 2023
1 parent ba69eff commit b0a7231
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 33 deletions.
78 changes: 61 additions & 17 deletions app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navDeepLink
import nl.eduid.screens.accountlinked.AccountLinkedScreen
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.biometric.EnableBiometricScreen
import nl.eduid.screens.biometric.EnableBiometricViewModel
import nl.eduid.screens.created.RequestEduIdCreatedScreen
Expand Down Expand Up @@ -67,11 +71,11 @@ fun MainGraph(
composable(Graph.HOME_PAGE) {
val viewModel = hiltViewModel<HomePageViewModel>(it)
HomePageScreen(viewModel = viewModel,
onScanForAuthorization = { /*QR authorization for 3rd party*/ },
onScanForAuthorization = { navController.navigate(Account.ScanQR.routeForAuth) },
onActivityClicked = { navController.navigate(Graph.DATA_AND_ACTIVITY) },
onPersonalInfoClicked = { navController.navigate(Graph.PERSONAL_INFO) },
onSecurityClicked = { navController.navigate(Graph.SECURITY) },
onEnrollWithQR = { navController.navigate(Account.ScanQR.route) },
onEnrollWithQR = { navController.navigate(Account.ScanQR.routeForEnrol) },
launchOAuth = { navController.navigate(Graph.OAUTH) }) {
navController.navigate(
Graph.REQUEST_EDU_ID_ACCOUNT
Expand All @@ -80,10 +84,13 @@ fun MainGraph(
}
//endregion
//region Scan
composable(Account.ScanQR.route) {
val viewModel = hiltViewModel<StatelessScanViewModel>(it)
composable(
route = Account.ScanQR.routeWithArgs, arguments = Account.ScanQR.arguments
) { entry ->
val viewModel = hiltViewModel<StatelessScanViewModel>(entry)
val isEnrolment = entry.arguments?.getBoolean(Account.ScanQR.isEnrolment, false) ?: false
ScanScreen(viewModel = viewModel,
isRegistration = true,
isEnrolment = isEnrolment,
goBack = { navController.popBackStack() },
goToNext = { challenge ->
val encodedChallenge = viewModel.encodeChallenge(challenge)
Expand All @@ -93,7 +100,7 @@ fun MainGraph(
)
} else {
navController.goToWithPopCurrent(
"${Account.Authorize.route}/$encodedChallenge"
"${Account.RequestAuthentication.route}/$encodedChallenge"
)
}
})
Expand Down Expand Up @@ -137,11 +144,43 @@ fun MainGraph(
promptAuth = { navController.navigate(Graph.OAUTH) })
}
//endregion
//region Authorize

//region Authentication
composable(
route = Account.RequestAuthentication.routeWithArgs,
arguments = Account.RequestAuthentication.arguments,
) { entry ->
val viewModel = hiltViewModel<EduIdAuthenticationViewModel>(entry)
RequestAuthenticationScreen(viewModel = viewModel, onLogin = { challenge ->
if (challenge != null) {
val encodedChallenge = viewModel.encodeChallenge(challenge)
navController.goToWithPopCurrent("${Account.AuthenticationCheckSecret.route}/$encodedChallenge")
}
}) { navController.popBackStack() }
}
composable(
route = Account.Authorize.routeWithArgs,
arguments = Account.Authorize.arguments,
route = Account.AuthenticationCheckSecret.routeWithArgs,
arguments = Account.AuthenticationCheckSecret.arguments,
) { entry ->
val viewModel = hiltViewModel<EduIdAuthenticationViewModel>(entry)
AuthenticationPinBiometricScreen(viewModel = viewModel,
goToAuthenticationComplete = { challenge, pin ->
if (challenge != null) {
val encodedChallenge = viewModel.encodeChallenge(challenge)
navController.goToWithPopCurrent(
Account.AuthenticationCompleted.buildRoute(
encodedChallenge = encodedChallenge, pin = pin
)
)
}
},
goHome = { navController.goToWithPopCurrent(Graph.HOME_PAGE) }) { navController.popBackStack() }
}
composable(
route = Account.AuthenticationCompleted.routeWithArgs,
arguments = Account.AuthenticationCompleted.arguments,
) { _ ->
AuthenticationCompletedScreen { navController.goToWithPopCurrent(Graph.HOME_PAGE) }
}
//endregion

Expand All @@ -162,7 +201,7 @@ fun MainGraph(
navController.goToWithPopCurrent("${Account.EnrollPinSetup.route}/$encodedChallenge")
} else {
navController.goToWithPopCurrent(
"${Account.Authorize.route}/$encodedChallenge"
"${Account.RequestAuthentication.route}/$encodedChallenge"
)
}
})
Expand Down Expand Up @@ -219,12 +258,11 @@ fun MainGraph(
)
}
composable(
route = RequestEduIdCreated.route, deepLinks = listOf(
navDeepLink {
uriPattern = RequestEduIdCreated.uriPatternHttps
}, navDeepLink {
uriPattern = RequestEduIdCreated.uriPatternCustomScheme
})
route = RequestEduIdCreated.route, deepLinks = listOf(navDeepLink {
uriPattern = RequestEduIdCreated.uriPatternHttps
}, navDeepLink {
uriPattern = RequestEduIdCreated.uriPatternCustomScheme
})
) { entry ->
val viewModel = hiltViewModel<HomePageViewModel>(entry)
RequestEduIdCreatedScreen(
Expand Down Expand Up @@ -330,7 +368,13 @@ fun MainGraph(
onEmailClicked = { navController.navigate(Graph.EDIT_EMAIL) },
onRoleClicked = { },
onInstitutionClicked = { },
onManageAccountClicked = { dateString -> navController.navigate(ManageAccountRoute.routeWithArgs(dateString)) },
onManageAccountClicked = { dateString ->
navController.navigate(
ManageAccountRoute.routeWithArgs(
dateString
)
)
},
goBack = { navController.popBackStack() },
)
}
Expand Down
56 changes: 49 additions & 7 deletions app/src/main/kotlin/nl/eduid/graphs/Routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package nl.eduid.graphs
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.navArgument
import nl.eduid.graphs.Graph.MANAGE_ACCOUNT
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
Expand All @@ -23,7 +22,6 @@ object Graph {
const val RESET_PASSWORD_CONFIRM = "reset_password_confirm"
const val EDIT_EMAIL = "edit_email"
const val TWO_FA_DETAIL = "2fa_detail"
const val MANAGE_ACCOUNT = "manage_account"
const val DELETE_ACCOUNT_FIRST_CONFIRM = "delete_account_first_confirm"
const val DELETE_ACCOUNT_SECOND_CONFIRM = "delete_account_second_confirm"
}
Expand Down Expand Up @@ -97,7 +95,19 @@ sealed class PhoneNumberRecovery(val route: String) {

sealed class Account(val route: String) {

object ScanQR : Account("scan")
object ScanQR : Account("scan") {
const val isEnrolment = "is_enrolment"
val routeWithArgs = "$route/{${isEnrolment}}"
val routeForEnrol = "$route/true"
val routeForAuth = "$route/false"
val arguments = listOf(navArgument(isEnrolment) {
type = NavType.BoolType
nullable = false
defaultValue = true
})

}

object EnrollPinSetup : Account("enroll_pin_setup") {
const val enrollChallenge = "enroll_challenge_arg"

Expand All @@ -109,7 +119,7 @@ sealed class Account(val route: String) {
})
}

object Authorize : Account("authorization") {
object RequestAuthentication : Account("authentication") {
const val challengeArg = "challenge_arg"

val routeWithArgs = "$route/{$challengeArg}"
Expand All @@ -120,6 +130,39 @@ sealed class Account(val route: String) {
})
}

object AuthenticationCheckSecret : Account("authentication_checksecret") {
private const val challengeArg = "challenge_arg"

val routeWithArgs = "$route/{$challengeArg}"
val arguments = listOf(navArgument(challengeArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
})
}

object AuthenticationCompleted : Account("authentication_completed") {
private const val challengeArg = "challenge_arg"
private const val pinArg = "pin_arg"

val routeWithArgs = "$route/{$challengeArg}?pin={$pinArg}"
val arguments = listOf(navArgument(challengeArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
}, navArgument(pinArg) {
type = NavType.StringType
nullable = true
})

fun buildRoute(encodedChallenge: String, pin: String?): String = if (pin.isNullOrEmpty()) {
"${route}/$encodedChallenge"
} else {
"${route}/$encodedChallenge?pin=$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 Expand Up @@ -161,11 +204,10 @@ sealed class WithChallenge(val route: String) {
}

object ManageAccountRoute {
private const val route = MANAGE_ACCOUNT
private const val route = "manage_account"
const val dateArg = "date_arg"
const val routeWithArgs = "$route/{$dateArg}"
val arguments = listOf(
navArgument(dateArg) {
val arguments = listOf(navArgument(dateArg) {
type = NavType.StringType
nullable = false
defaultValue = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package nl.eduid.screens.authorize

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import nl.eduid.R
import nl.eduid.ui.EduIdTopAppBar
import nl.eduid.ui.PrimaryButton
import nl.eduid.ui.theme.EduidAppAndroidTheme
import nl.eduid.ui.theme.TextGreen

@Composable
fun AuthenticationCompletedScreen(goHome: () -> Unit = {}) = EduIdTopAppBar(
withBackIcon = false
) {
Column(modifier = Modifier.fillMaxSize()) {
Text(
style = MaterialTheme.typography.titleLarge.copy(
textAlign = TextAlign.Start, color = TextGreen
), text = stringResource(R.string.authorize_title), modifier = Modifier.fillMaxWidth()
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_authorize_confirmed),
contentDescription = "",
modifier = Modifier.wrapContentSize(),
alignment = Alignment.Center
)

Text(
text = stringResource(R.string.authorize_confirmed_subtitle),
style = MaterialTheme.typography.titleLarge.copy(
color = TextGreen
),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.button_ok),
onClick = goHome,
)
Spacer(Modifier.height(24.dp))
}
}

@Preview
@Composable
private fun PreviewAuthorizeConfirmedScreen() = EduidAppAndroidTheme {
AuthenticationCompletedScreen()
}
Loading

0 comments on commit b0a7231

Please sign in to comment.