Skip to content

Commit 56befef

Browse files
SanderKondratjevNortalaarmam
authored andcommitted
Implement initial Web eID mobile authentication flow (#242)
1 parent 35f8a17 commit 56befef

File tree

28 files changed

+1585
-129
lines changed

28 files changed

+1585
-129
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ dependencies {
214214
implementation(project(":utils-lib"))
215215
implementation(project(":commons-lib"))
216216
implementation(project(":id-card-lib"))
217+
implementation(project(":web-eid-lib"))
217218

218219
androidTestImplementation(project(":commons-lib:test-files"))
219220
}

app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/WebEidViewModelTest.kt

Lines changed: 126 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,156 @@
22

33
package ee.ria.DigiDoc.viewmodel
44

5+
import android.app.Activity
56
import android.net.Uri
6-
import kotlinx.coroutines.ExperimentalCoroutinesApi
7-
import kotlinx.coroutines.test.runTest
8-
import org.junit.Assert.assertEquals
9-
import org.junit.Assert.assertNull
7+
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
8+
import ee.ria.DigiDoc.webEid.WebEidAuthService
9+
import ee.ria.DigiDoc.webEid.domain.model.WebEidAuthRequest
10+
import kotlinx.coroutines.flow.MutableStateFlow
11+
import org.json.JSONObject
1012
import org.junit.Before
13+
import org.junit.Rule
1114
import org.junit.Test
12-
13-
@OptIn(ExperimentalCoroutinesApi::class)
15+
import org.junit.runner.RunWith
16+
import org.mockito.Mock
17+
import org.mockito.Mockito.never
18+
import org.mockito.Mockito.`when`
19+
import org.mockito.MockitoAnnotations
20+
import org.mockito.junit.MockitoJUnitRunner
21+
import org.mockito.kotlin.any
22+
import org.mockito.kotlin.verify
23+
24+
@RunWith(MockitoJUnitRunner::class)
1425
class WebEidViewModelTest {
26+
@get:Rule
27+
val instantExecutorRule = InstantTaskExecutorRule()
28+
29+
@Mock
30+
private lateinit var authService: WebEidAuthService
31+
32+
@Mock
33+
private lateinit var activity: Activity
1534

1635
private lateinit var viewModel: WebEidViewModel
1736

1837
@Before
19-
fun setUp() {
20-
viewModel = WebEidViewModel()
21-
}
38+
fun setup() {
39+
MockitoAnnotations.openMocks(this)
2240

23-
@Test
24-
fun handleAuth_validUri_setsAuthPayload() = runTest {
25-
val json = """
26-
{
27-
"challenge": "abc123",
28-
"login_uri": "https://example.com/auth/login",
29-
"get_signing_certificate": true
30-
}
31-
""".trimIndent()
32-
33-
val encoded = java.util.Base64.getEncoder().encodeToString(json.toByteArray())
34-
val uri = Uri.parse("web-eid-mobile://auth#$encoded")
41+
`when`(authService.authRequest).thenReturn(MutableStateFlow(null))
42+
`when`(authService.signRequest).thenReturn(MutableStateFlow(null))
43+
`when`(authService.errorState).thenReturn(MutableStateFlow(null))
44+
`when`(authService.redirectUri).thenReturn(MutableStateFlow(null))
3545

36-
viewModel.handleAuth(uri)
37-
38-
val result = viewModel.authPayload.value
39-
assertEquals("abc123", result?.challenge)
40-
assertEquals("https://example.com/auth/login", result?.loginUri)
41-
assertEquals(true, result?.getSigningCertificate)
46+
viewModel = WebEidViewModel(authService)
4247
}
4348

4449
@Test
45-
fun handleAuth_missingFragment_setsNullPayload() = runTest {
46-
val uri = Uri.parse("web-eid-mobile://auth")
47-
50+
fun handleAuth_callsParseAuthUri() {
51+
val uri = Uri.parse("web-eid-mobile://auth#dummyData")
4852
viewModel.handleAuth(uri)
49-
50-
assertNull(viewModel.authPayload.value)
53+
verify(authService).parseAuthUri(uri)
5154
}
5255

5356
@Test
54-
fun handleAuth_invalidBase64_setsNullPayload() = runTest {
55-
val uri = Uri.parse("web-eid-mobile://auth#invalid-base64!!")
57+
fun handleSign_callsParseSignUri() {
58+
val uri = Uri.parse("web-eid-mobile://sign#dummyData")
59+
viewModel.handleSign(uri)
60+
verify(authService).parseSignUri(uri)
61+
}
5662

57-
viewModel.handleAuth(uri)
63+
@Test
64+
fun reset_callsResetValues() {
65+
viewModel.reset()
66+
verify(authService).resetValues()
67+
}
5868

59-
assertNull(viewModel.authPayload.value)
69+
@Test
70+
fun redirectUri_isExposedFromAuthService() {
71+
val redirectFlow = MutableStateFlow("https://example.com#encodedPayload")
72+
`when`(authService.redirectUri).thenReturn(redirectFlow)
73+
val vm = WebEidViewModel(authService)
74+
assert(vm.redirectUri.value == "https://example.com#encodedPayload")
6075
}
6176

6277
@Test
63-
fun handleAuth_missingOptionalField_defaultsToFalse() = runTest {
64-
val json = """
65-
{
66-
"challenge": "xyz456",
67-
"login_uri": "https://rp.example.com/login"
68-
}
69-
""".trimIndent()
78+
fun redirectUri_updatesWhenServiceUpdates() {
79+
val redirectFlow = MutableStateFlow<String?>(null)
80+
`when`(authService.redirectUri).thenReturn(redirectFlow)
81+
val vm = WebEidViewModel(authService)
82+
redirectFlow.value = "https://example.com#updatedPayload"
83+
assert(vm.redirectUri.value == "https://example.com#updatedPayload")
84+
}
7085

71-
val encoded = java.util.Base64.getEncoder().encodeToString(json.toByteArray())
72-
val uri = Uri.parse("web-eid-mobile://auth#$encoded")
86+
@Test
87+
fun handleWebEidAuthResult_callsBuildAuthToken_whenPayloadValid() {
88+
val cert = byteArrayOf(1, 2, 3)
89+
val signature = byteArrayOf(4, 5, 6)
90+
val challenge = "test-challenge"
91+
val loginUri = "https://example.com/login"
92+
val getSigningCertificate = true
93+
val origin = "https://example.com"
94+
95+
val authRequest =
96+
WebEidAuthRequest(
97+
challenge = challenge,
98+
loginUri = loginUri,
99+
getSigningCertificate = getSigningCertificate,
100+
origin = origin,
101+
)
102+
`when`(authService.authRequest).thenReturn(MutableStateFlow(authRequest))
103+
104+
val token = JSONObject().put("mock", "token")
105+
`when`(authService.buildAuthToken(cert, signature, challenge)).thenReturn(token)
106+
107+
viewModel = WebEidViewModel(authService)
108+
109+
viewModel.handleWebEidAuthResult(cert, signature, activity)
110+
111+
verify(authService).buildAuthToken(cert, signature, challenge)
112+
verify(activity).startActivity(any())
113+
verify(activity).finish()
114+
}
73115

74-
viewModel.handleAuth(uri)
116+
@Test
117+
fun handleWebEidAuthResult_doesNothing_whenChallengeMissing() {
118+
val cert = byteArrayOf(1)
119+
val signature = byteArrayOf(2)
120+
121+
val authRequest =
122+
WebEidAuthRequest(
123+
challenge = "",
124+
loginUri = "https://example.com",
125+
getSigningCertificate = true,
126+
origin = "https://example.com",
127+
)
128+
`when`(authService.authRequest).thenReturn(MutableStateFlow(authRequest))
129+
130+
viewModel = WebEidViewModel(authService)
131+
viewModel.handleWebEidAuthResult(cert, signature, activity)
132+
133+
verify(authService, never()).buildAuthToken(any(), any(), any())
134+
verify(activity, never()).startActivity(any())
135+
}
75136

76-
val result = viewModel.authPayload.value
77-
assertEquals("xyz456", result?.challenge)
78-
assertEquals("https://rp.example.com/login", result?.loginUri)
79-
assertEquals(false, result?.getSigningCertificate)
137+
@Test
138+
fun handleWebEidAuthResult_doesNothing_whenLoginUriMissing() {
139+
val cert = byteArrayOf(1)
140+
val signature = byteArrayOf(2)
141+
142+
val authRequest =
143+
WebEidAuthRequest(
144+
challenge = "abc",
145+
loginUri = "",
146+
getSigningCertificate = true,
147+
origin = "https://example.com",
148+
)
149+
`when`(authService.authRequest).thenReturn(MutableStateFlow(authRequest))
150+
151+
viewModel = WebEidViewModel(authService)
152+
viewModel.handleWebEidAuthResult(cert, signature, activity)
153+
154+
verify(authService, never()).buildAuthToken(any(), any(), any())
155+
verify(activity, never()).startActivity(any())
80156
}
81157
}

app/src/main/kotlin/ee/ria/DigiDoc/RIADigiDocAppNavigation.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel
7676
import ee.ria.DigiDoc.viewmodel.shared.SharedSignatureViewModel
7777

7878
@Composable
79-
fun RIADigiDocAppScreen(externalFileUris: List<Uri>, webEidUri: Uri? = null) {
79+
fun RIADigiDocAppScreen(
80+
externalFileUris: List<Uri>,
81+
webEidUri: Uri? = null,
82+
) {
8083
val navController = rememberNavController()
8184
val sharedMenuViewModel: SharedMenuViewModel = hiltViewModel()
8285
val sharedContainerViewModel: SharedContainerViewModel = hiltViewModel()
@@ -88,11 +91,12 @@ fun RIADigiDocAppScreen(externalFileUris: List<Uri>, webEidUri: Uri? = null) {
8891

8992
sharedContainerViewModel.setExternalFileUris(externalFileUris)
9093

91-
val startDestination = when {
92-
webEidUri != null -> Route.WebEidScreen.route
93-
sharedSettingsViewModel.dataStore.getLocale() != null -> Route.Home.route
94-
else -> Route.Init.route
95-
}
94+
val startDestination =
95+
when {
96+
webEidUri != null -> Route.WebEidScreen.route
97+
sharedSettingsViewModel.dataStore.getLocale() != null -> Route.Home.route
98+
else -> Route.Init.route
99+
}
96100

97101
NavHost(
98102
navController = navController,
@@ -392,7 +396,9 @@ fun RIADigiDocAppScreen(externalFileUris: List<Uri>, webEidUri: Uri? = null) {
392396
@Composable
393397
fun RIADigiDocAppScreenPreview() {
394398
RIADigiDocTheme {
395-
RIADigiDocAppScreen(listOf(),
396-
webEidUri = null)
399+
RIADigiDocAppScreen(
400+
listOf(),
401+
webEidUri = null,
402+
)
397403
}
398404
}

app/src/main/kotlin/ee/ria/DigiDoc/fragment/WebEidFragment.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import androidx.navigation.NavHostController
2121
import androidx.navigation.compose.rememberNavController
2222
import ee.ria.DigiDoc.fragment.screen.WebEidScreen
2323
import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme
24+
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog
2425
import ee.ria.DigiDoc.viewmodel.WebEidViewModel
26+
import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel
27+
import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel
28+
import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel
2529

2630
@OptIn(ExperimentalComposeUiApi::class)
2731
@Composable
@@ -30,10 +34,18 @@ fun WebEidFragment(
3034
navController: NavHostController,
3135
webEidUri: Uri?,
3236
viewModel: WebEidViewModel = hiltViewModel(),
37+
sharedSettingsViewModel: SharedSettingsViewModel = hiltViewModel(),
38+
sharedContainerViewModel: SharedContainerViewModel = hiltViewModel(),
39+
sharedMenuViewModel: SharedMenuViewModel = hiltViewModel(),
3340
) {
3441
LaunchedEffect(webEidUri) {
35-
println("DEBUG: WebEidFragment got URI = $webEidUri")
36-
webEidUri?.let { viewModel.handleAuth(it) }
42+
webEidUri?.let {
43+
when (it.host) {
44+
"auth" -> viewModel.handleAuth(it)
45+
"sign" -> viewModel.handleSign(it)
46+
else -> debugLog("WebEidFragment", "Unknown Web eID URI host: ${it.host}")
47+
}
48+
}
3749
}
3850

3951
Surface(
@@ -49,6 +61,9 @@ fun WebEidFragment(
4961
modifier = modifier,
5062
navController = navController,
5163
viewModel = viewModel,
64+
sharedSettingsViewModel = sharedSettingsViewModel,
65+
sharedContainerViewModel = sharedContainerViewModel,
66+
sharedMenuViewModel = sharedMenuViewModel,
5267
)
5368
}
5469
}
@@ -60,7 +75,7 @@ fun WebEidFragmentPreview() {
6075
RIADigiDocTheme {
6176
WebEidFragment(
6277
navController = rememberNavController(),
63-
webEidUri = null
78+
webEidUri = null,
6479
)
6580
}
66-
}
81+
}

0 commit comments

Comments
 (0)