mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2025-08-30 07:10:59 +03:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d67c815aad | ||
|
c232a1f734 | ||
|
b95115af4f | ||
|
866c3c24a7 | ||
|
32336ffb2b | ||
|
b889b9cda7 | ||
|
e5931692b6 | ||
|
b66c2386f0 | ||
|
04933f2705 | ||
|
8b78ddf51a | ||
|
13a0a9430f | ||
|
d92656d982 | ||
|
0da3e17b28 | ||
|
2b660eeda0 | ||
|
5719712bef | ||
|
1e26f83db2 | ||
|
44430bfab2 | ||
|
7482c88a4e | ||
|
74d97827f8 | ||
|
4096201bef | ||
|
da2dbeb0fc | ||
|
7daea4b6c2 |
@@ -44,9 +44,7 @@ dependencies {
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
debugImplementation 'androidx.fragment:fragment-testing:1.4.0'
|
||||
|
||||
//To use activityViewModels
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
@@ -1,62 +0,0 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
//import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.IdlingPolicies
|
||||
import androidx.test.espresso.NoMatchingViewException
|
||||
import androidx.test.espresso.action.ViewActions.*
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
|
||||
import org.junit.*
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class UC4Test {
|
||||
@get:Rule
|
||||
var activityActivityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule(
|
||||
MainActivity::class.java
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
IdlingPolicies.setMasterPolicyTimeout(3, TimeUnit.SECONDS)
|
||||
IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.SECONDS)
|
||||
activityActivityTestRule.activity
|
||||
.supportFragmentManager.beginTransaction()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
}
|
||||
|
||||
fun navigateToCANView() {
|
||||
onView(withId(R.id.menu_settings_option)).perform(click())
|
||||
try {
|
||||
// Delete existing CAN
|
||||
onView(withText(R.string.can_delete)).perform(click())
|
||||
} catch (ignore: NoMatchingViewException) {}
|
||||
|
||||
onView(withId(R.id.can_menu_action)).perform(click())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validCAN() {
|
||||
navigateToCANView()
|
||||
onView(withText(R.string.can_helper_text)).check(matches(isDisplayed()))
|
||||
onView(supportsInputMethods()).perform(typeText("123456"))
|
||||
onView(withText(R.string.can_delete)).perform(closeSoftKeyboard())
|
||||
|
||||
onView(withText(R.string.can_status_saved)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidCAN() {
|
||||
navigateToCANView()
|
||||
onView(supportsInputMethods()).perform(typeText("12345"))
|
||||
onView(withText(R.string.can_helper_text)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
@@ -1,10 +1,14 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -14,14 +18,11 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.security.GeneralSecurityException
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
@@ -33,9 +34,10 @@ class AuthFragment : Fragment() {
|
||||
|
||||
private val viewModel: SmartCardViewModel by activityViewModels()
|
||||
|
||||
private val intentParameters: ParametersViewModel by activityViewModels()
|
||||
private val paramsModel: ParametersViewModel by activityViewModels()
|
||||
|
||||
private var binding: FragmentAuthBinding? = null
|
||||
private var _binding: FragmentAuthBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val args: CanFragmentArgs by navArgs()
|
||||
|
||||
@@ -48,8 +50,8 @@ class AuthFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentAuthBinding.inflate(inflater, container, false)
|
||||
return binding!!.root
|
||||
_binding = FragmentAuthBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -58,116 +60,101 @@ class AuthFragment : Fragment() {
|
||||
override fun onTick(p0: Long) {
|
||||
timeRemaining--
|
||||
if (timeRemaining == 0) {
|
||||
binding?.timeCounter?.text = getString(R.string.no_time)
|
||||
binding.timeCounter.text = getString(R.string.no_time)
|
||||
} else {
|
||||
binding?.timeCounter?.text = getString(R.string.time_left, timeRemaining)
|
||||
binding.timeCounter.text = getString(R.string.time_left, timeRemaining)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
Thread.sleep(750)
|
||||
goToTheStart()
|
||||
cancelAuth(408)
|
||||
}
|
||||
}.start()
|
||||
binding!!.nextButton.setOnClickListener { goToNextFragment() }
|
||||
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
||||
// The button exists in code for testing reasons, but not visible to the user anymore unless visibility is changed in the code.
|
||||
binding.nextButton.visibility = View.GONE
|
||||
binding.nextButton.setOnClickListener { goToNextFragment() }
|
||||
binding.cancelButton.setOnClickListener { cancelAuth(444) }
|
||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
if (adapter != null)
|
||||
getInfoFromIdCard(adapter)
|
||||
}
|
||||
|
||||
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
||||
if (args.reading) {
|
||||
adapter.enableReaderMode(activity, { tag ->
|
||||
timer.cancel()
|
||||
requireActivity().runOnUiThread {
|
||||
binding!!.timeCounter.text = getString(R.string.card_detected)
|
||||
}
|
||||
var msgCode = 0
|
||||
|
||||
val card = IsoDep.get(tag)
|
||||
card.timeout = 32768
|
||||
card.use {
|
||||
try {
|
||||
val comms = Comms(it, viewModel.userCan)
|
||||
val response = comms.readPersonalData(byteArrayOf(1, 2, 6, 3, 4, 8))
|
||||
viewModel.setUserFirstName(response[1])
|
||||
viewModel.setUserLastName(response[0])
|
||||
viewModel.setUserIdentificationNumber(response[2])
|
||||
viewModel.setGender(response[3])
|
||||
viewModel.setCitizenship(response[4])
|
||||
viewModel.setExpiration(response[5])
|
||||
requireActivity().runOnUiThread {
|
||||
binding!!.timeCounter.text = getString(R.string.data_read)
|
||||
}
|
||||
|
||||
} catch (e: android.nfc.TagLostException) {
|
||||
msgCode = R.string.tag_lost
|
||||
} catch (e: InvalidCANException) {
|
||||
msgCode = R.string.invalid_can
|
||||
// If the CAN is wrong we will also delete the saved CAN so that the user won't use it again.
|
||||
viewModel.deleteCan(requireContext())
|
||||
} catch (e: AuthAppException) {
|
||||
msgCode = when (e.code) {
|
||||
448 -> R.string.err_bad_data
|
||||
500 -> R.string.err_internal
|
||||
else -> R.string.err_unknown
|
||||
}
|
||||
} catch (e: GeneralSecurityException) {
|
||||
msgCode = R.string.err_internal
|
||||
} catch (e: IOException) {
|
||||
msgCode = R.string.err_reading_card
|
||||
} catch (e: Exception) {
|
||||
msgCode = R.string.err_unknown
|
||||
} finally {
|
||||
adapter.disableReaderMode(activity)
|
||||
}
|
||||
|
||||
if (msgCode != 0) {
|
||||
requireActivity().runOnUiThread {
|
||||
binding!!.timeCounter.text = getString(msgCode)
|
||||
}
|
||||
// Gives user some time to read the error message
|
||||
Thread.sleep(1000)
|
||||
goToTheStart()
|
||||
}
|
||||
}
|
||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||
} else { //We want to create a JWT instead of reading the info from the card.
|
||||
goToNextFragment()
|
||||
else { // If NFC adapter can not be detected then end the auth process as it is not possible to read an ID card
|
||||
cancelAuth(447) // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToNextFragment() {
|
||||
timer.cancel()
|
||||
if (args.auth) {
|
||||
val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile)
|
||||
findNavController().navigate(action)
|
||||
val action = AuthFragmentDirections.actionAuthFragmentToResultFragment(mobile = args.mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun cancelAuth(code: Int) {
|
||||
viewModel.clearUserInfo()
|
||||
timer.cancel()
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_authFragment_to_userFragment)
|
||||
(activity as MainActivity).returnError(code)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToTheStart() {
|
||||
viewModel.clearUserInfo()
|
||||
timer.cancel()
|
||||
if (args.reading) {
|
||||
findNavController().navigate(R.id.action_authFragment_to_homeFragment)
|
||||
} else {
|
||||
if (!args.mobile) {
|
||||
//Currently for some reason the activity is not killed entirely. Must be looked into further.
|
||||
requireActivity().finish()
|
||||
exitProcess(0)
|
||||
} else {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
private fun getInfoFromIdCard(adapter: NfcAdapter) {
|
||||
adapter.enableReaderMode(activity, { tag ->
|
||||
timer.cancel()
|
||||
requireActivity().runOnUiThread {
|
||||
binding.timeCounter.text = getString(R.string.card_detected)
|
||||
}
|
||||
}
|
||||
val card = IsoDep.get(tag)
|
||||
card.timeout = 32768
|
||||
card.use {
|
||||
try {
|
||||
val comms = Comms(it, viewModel.userCan)
|
||||
val jws = Authenticator(comms).authenticate(
|
||||
paramsModel.challenge,
|
||||
paramsModel.origin,
|
||||
viewModel.userPin
|
||||
)
|
||||
paramsModel.setToken(jws)
|
||||
requireActivity().runOnUiThread {
|
||||
goToNextFragment()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when(e) {
|
||||
is TagLostException -> requireActivity().runOnUiThread {
|
||||
binding!!.timeCounter.text = getString(R.string.id_card_removed_early)
|
||||
cancelAuth(444)
|
||||
}
|
||||
else -> {
|
||||
when ("invalid pin") {
|
||||
in e.message.toString().lowercase() -> requireActivity().runOnUiThread {
|
||||
val messagePieces = e.message.toString().split(" ")
|
||||
binding.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1])
|
||||
viewModel.deletePin(requireContext())
|
||||
cancelAuth(449)
|
||||
}
|
||||
else -> requireActivity().runOnUiThread {
|
||||
binding.timeCounter.text = getString(R.string.wrong_can_text)
|
||||
viewModel.deleteCan(requireContext())
|
||||
cancelAuth(449)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Give user some time to read the error message
|
||||
Thread.sleep(2000)
|
||||
} finally {
|
||||
adapter.disableReaderMode(activity)
|
||||
}
|
||||
}
|
||||
}, NfcAdapter.FLAG_READER_NFC_A, null)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
binding = null
|
||||
_binding = null
|
||||
}
|
||||
}
|
@@ -90,6 +90,7 @@ class CanFragment : Fragment() {
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
(activity as MainActivity).returnError(444)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
|
@@ -4,7 +4,6 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.TagLostException
|
||||
import android.nfc.tech.IsoDep
|
||||
@@ -14,17 +13,20 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.NFC.Comms
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import org.json.JSONObject
|
||||
import java.lang.Exception
|
||||
import java.lang.RuntimeException
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* HomeFragment is only shown to the user when then the user launches the application. When the application
|
||||
@@ -88,7 +90,8 @@ class HomeFragment : Fragment() {
|
||||
*/
|
||||
private fun goToTheNextFragment(mobile: Boolean = false) {
|
||||
(activity as MainActivity).menuAvailable = false
|
||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
|
||||
val action =
|
||||
HomeFragmentDirections.actionHomeFragmentToCanFragment(auth = true, mobile = mobile)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
@@ -101,18 +104,75 @@ class HomeFragment : Fragment() {
|
||||
try {
|
||||
if (mobile) {
|
||||
// We use !! to get extras because we want an exception to be thrown when something is missing.
|
||||
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
||||
//intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
||||
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
||||
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
|
||||
val challengeUrl = requireActivity().intent.getStringExtra("challenge")!!
|
||||
val headers = requireActivity().intent.getStringExtra("headers")!!
|
||||
val map: HashMap<String, String> = HashMap()
|
||||
map.put("sessionId", headers)
|
||||
intentParams.setHeaders(map)
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
Ion.with(activity)
|
||||
.load(challengeUrl)
|
||||
.setHeader("sessionId", headers)
|
||||
.asJsonObject()
|
||||
.setCallback { _, result ->
|
||||
try {
|
||||
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||
intentParams.setChallenge(challenge)
|
||||
goToTheNextFragment(mobile)
|
||||
} catch (e: Exception) {
|
||||
Log.i("GETrequest", e.toString())
|
||||
}
|
||||
}
|
||||
} else { //Website
|
||||
/*
|
||||
var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
|
||||
// TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
|
||||
challenge = challenge.replace(" ", "+")
|
||||
intentParams.setChallenge(challenge)
|
||||
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
|
||||
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
|
||||
*/
|
||||
var getAuthChallengeUrl =
|
||||
requireActivity().intent.data!!.getQueryParameter("getAuthChallengeUrl")!!
|
||||
getAuthChallengeUrl =
|
||||
getAuthChallengeUrl.substring(1, getAuthChallengeUrl.length - 1)
|
||||
var postAuthTokenUrl =
|
||||
requireActivity().intent.data!!.getQueryParameter("postAuthTokenUrl")!!
|
||||
postAuthTokenUrl = postAuthTokenUrl.substring(1, postAuthTokenUrl.length - 1)
|
||||
val headers =
|
||||
getHeaders(requireActivity().intent.data!!.getQueryParameter("headers")!!)
|
||||
intentParams.setAuthUrl(postAuthTokenUrl)
|
||||
val address = "https://" + URL(getAuthChallengeUrl).host
|
||||
intentParams.setOrigin(address)
|
||||
intentParams.setHeaders(headers)
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(activity)
|
||||
.load(getAuthChallengeUrl)
|
||||
|
||||
// Set headers.
|
||||
for ((header, value) in intentParams.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.asJsonObject()
|
||||
.setCallback { _, result ->
|
||||
try {
|
||||
// Get data from the result and call launchAuth method
|
||||
val challenge =
|
||||
result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||
intentParams.setChallenge(challenge)
|
||||
goToTheNextFragment(mobile)
|
||||
} catch (e: Exception) {
|
||||
Log.i("GETrequest", "was unsuccessful" + e.message)
|
||||
throw RuntimeException()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
goToTheNextFragment(mobile)
|
||||
} catch (e: Exception) {
|
||||
// There was a problem with parameters, which means that authentication is not possible.
|
||||
// In that case we will cancel the authentication immediately as it would be waste of the user's time to carry on
|
||||
@@ -128,7 +188,7 @@ class HomeFragment : Fragment() {
|
||||
} else {
|
||||
message.setMessage(getString(R.string.problem_other))
|
||||
}
|
||||
message.setPositiveButton(getString(R.string.continue_button)) {_, _ ->
|
||||
message.setPositiveButton(getString(R.string.continue_button)) { _, _ ->
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
@@ -165,6 +225,17 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHeaders(headersString: String): Map<String, String> {
|
||||
val headers = HashMap<String, String>()
|
||||
val headersStringFormatted = headersString.substring(1, headersString.length - 1)
|
||||
val headersJsonObject = JSONObject(headersStringFormatted)
|
||||
|
||||
for (name in headersJsonObject.keys()) {
|
||||
headers[name] = headersJsonObject[name].toString()
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays texts that inform the user whether the CAN and PIN 1 are saved on the device or not.
|
||||
* This might help the user to save some time as checking menu is not necessary unless the user
|
||||
@@ -191,7 +262,7 @@ class HomeFragment : Fragment() {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.return_text){_, _ -> }
|
||||
.setPositiveButton(R.string.return_text) { _, _ -> }
|
||||
.show()
|
||||
val title = dialog.findViewById<TextView>(R.id.alertTitle)
|
||||
title?.textSize = 24F
|
||||
@@ -211,11 +282,17 @@ class HomeFragment : Fragment() {
|
||||
binding.detectionActionText.text = getString(R.string.action_detect_unavailable)
|
||||
binding.homeActionButton.text = getString(R.string.add_can_text)
|
||||
binding.homeActionButton.setOnClickListener {
|
||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(saving = true, fromhome = true)
|
||||
val action = HomeFragmentDirections.actionHomeFragmentToCanFragment(
|
||||
saving = true,
|
||||
fromhome = true
|
||||
)
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
binding.homeHelpButton.setOnClickListener {
|
||||
displayMessage(getString(R.string.can_question), getString(R.string.can_explanation))
|
||||
displayMessage(
|
||||
getString(R.string.can_question),
|
||||
getString(R.string.can_explanation)
|
||||
)
|
||||
}
|
||||
binding.homeActionButton.visibility = View.VISIBLE
|
||||
binding.homeHelpButton.visibility = View.VISIBLE
|
||||
@@ -262,13 +339,15 @@ class HomeFragment : Fragment() {
|
||||
findNavController().navigate(action)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
when(e) {
|
||||
when (e) {
|
||||
is TagLostException -> requireActivity().runOnUiThread {
|
||||
binding.detectionActionText.text = getString(R.string.id_card_removed_early)
|
||||
binding.detectionActionText.text =
|
||||
getString(R.string.id_card_removed_early)
|
||||
reset()
|
||||
}
|
||||
else -> requireActivity().runOnUiThread {
|
||||
binding.detectionActionText.text = getString(R.string.nfc_reading_error)
|
||||
binding.detectionActionText.text =
|
||||
getString(R.string.nfc_reading_error)
|
||||
viewModel.deleteCan(requireContext())
|
||||
canState()
|
||||
reset()
|
||||
|
@@ -5,9 +5,17 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.gson.JsonObject
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
|
||||
|
||||
/**
|
||||
@@ -16,6 +24,8 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var navigationController: NavController
|
||||
private val paramsModel: ParametersViewModel by viewModels()
|
||||
|
||||
|
||||
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
|
||||
var menuAvailable: Boolean = true
|
||||
@@ -54,4 +64,24 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun returnError(errorCode: Int) {
|
||||
val json = JsonObject()
|
||||
json.addProperty("auth-token", "")
|
||||
json.addProperty("error", errorCode)
|
||||
|
||||
Ion.getDefault(this).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(this)
|
||||
.load(paramsModel.authUrl)
|
||||
for ((header, value) in paramsModel.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,9 +3,6 @@ package com.tarkvaraprojekt.mobileauthapp.NFC;
|
||||
import android.nfc.tech.IsoDep;
|
||||
import android.util.Log;
|
||||
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.AuthAppException;
|
||||
import com.tarkvaraprojekt.mobileauthapp.auth.InvalidCANException;
|
||||
|
||||
import org.bouncycastle.crypto.BlockCipher;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.macs.CMac;
|
||||
@@ -24,6 +21,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -33,47 +31,43 @@ import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class Comms {
|
||||
private static final byte[] master = { // select Main AID
|
||||
0, -92, 4, 12, 16, -96, 0, 0, 0, 119, 1, 8, 0, 7, 0, 0, -2, 0, 0, 1, 0
|
||||
};
|
||||
|
||||
private static final byte[] MSESetAT = { // manage security environment: set authentication template
|
||||
0, 34, -63, -92, 15, -128, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -125, 1, 2, 0
|
||||
};
|
||||
private static final byte[] selectMaster = Hex.decode("00a4040c10a000000077010800070000fe00000100");
|
||||
|
||||
private static final byte[] GAGetNonce = { // general authenticate: get nonce
|
||||
16, -122, 0, 0, 2, 124, 0, 0
|
||||
};
|
||||
private static final byte[] MSESetAT = Hex.decode("0022c1a40f800a04007f0007020204020483010200");
|
||||
|
||||
private static final byte[] GAMapNonceIncomplete = {
|
||||
16, -122, 0, 0, 69, 124, 67, -127, 65
|
||||
};
|
||||
private static final byte[] GAGetNonce = Hex.decode("10860000027c0000");
|
||||
|
||||
private static final byte[] GAKeyAgreementIncomplete = {
|
||||
16, -122, 0, 0, 69, 124, 67, -125, 65
|
||||
};
|
||||
private static final byte[] GAMapNonceIncomplete = Hex.decode("10860000457c438141");
|
||||
|
||||
private static final byte[] GAMutualAuthenticationIncomplete = {
|
||||
0, -122, 0, 0, 12, 124, 10, -123, 8
|
||||
};
|
||||
private static final byte[] GAKeyAgreementIncomplete = Hex.decode("10860000457c438341");
|
||||
|
||||
private static final byte[] dataForMACIncomplete = {
|
||||
127, 73, 79, 6, 10, 4, 0, 127, 0, 7, 2, 2, 4, 2, 4, -122, 65
|
||||
};
|
||||
private static final byte[] GAMutualAuthenticationIncomplete = Hex.decode("008600000c7c0a8508");
|
||||
|
||||
private static final byte[] masterSec = {
|
||||
12, -92, 4, 12, 45, -121, 33, 1
|
||||
};
|
||||
private static final byte[] dataForMACIncomplete = Hex.decode("7f494f060a04007f000702020402048641");
|
||||
|
||||
private static final byte[] personal = { // select personal data DF
|
||||
12, -92, 1, 12, 29, -121, 17, 1
|
||||
};
|
||||
private static final byte[] selectFile = Hex.decode("0ca4010c1d871101");
|
||||
|
||||
private static final byte[] read = { // read binary
|
||||
12, -80, 0, 0, 13, -105, 1, 0
|
||||
};
|
||||
private static final byte[] readFile = Hex.decode("0cb000000d970100");
|
||||
|
||||
private IsoDep idCard;
|
||||
private static final byte[] verifyPIN1 = Hex.decode("0c2000011d871101");
|
||||
|
||||
private static final byte[] verifyPIN2 = Hex.decode("0c2000851d871101");
|
||||
|
||||
private static final byte[] MSESetEnv = Hex.decode("0c2241A41d871101");
|
||||
|
||||
private static final byte[] Env = Hex.decode("8004FF200800840181");
|
||||
|
||||
private static final byte[] InternalAuthenticate = Hex.decode("0c8800001d871101");
|
||||
|
||||
private static final byte[] IASECCFID = {0x3f, 0x00};
|
||||
private static final byte[] personalDF = {0x50, 0x00};
|
||||
private static final byte[] AWP = {(byte) 0xad, (byte) 0xf1};
|
||||
private static final byte[] QSCD = {(byte) 0xad, (byte) 0xf2};
|
||||
private static final byte[] authCert = {0x34, 0x01};
|
||||
private static final byte[] signCert = {0x34, 0x1f};
|
||||
|
||||
private final IsoDep idCard;
|
||||
private final byte[] keyEnc;
|
||||
private final byte[] keyMAC;
|
||||
private byte ssc; // Send sequence counter.
|
||||
@@ -87,21 +81,12 @@ public class Comms {
|
||||
public Comms(IsoDep idCard, String CAN) throws IOException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
|
||||
idCard.connect();
|
||||
|
||||
this.idCard = idCard;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
byte[][] keys = PACE(CAN);
|
||||
Log.i("Pace duration", String.valueOf(System.currentTimeMillis() - start));
|
||||
|
||||
byte[][] keys = PACE(CAN.getBytes(StandardCharsets.UTF_8));
|
||||
keyEnc = keys[0];
|
||||
keyMAC = keys[1];
|
||||
}
|
||||
|
||||
public byte[] getAuthenticationCertificate() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the message authentication code
|
||||
*
|
||||
@@ -154,40 +139,51 @@ public class Comms {
|
||||
* @param CAN the card access number provided by the user
|
||||
* @return the decrypted nonce
|
||||
*/
|
||||
private byte[] decryptNonce(byte[] encryptedNonce, String CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
byte[] decryptionKey = createKey(CAN.getBytes(StandardCharsets.UTF_8), (byte) 3);
|
||||
private byte[] decryptNonce(byte[] encryptedNonce, byte[] CAN) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
|
||||
byte[] decryptionKey = createKey(CAN, (byte) 3);
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptionKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
return cipher.doFinal(encryptedNonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Communicates with the card and logs the response
|
||||
*
|
||||
* @param APDU The command
|
||||
* @param log Information for logging
|
||||
* @return The response
|
||||
*/
|
||||
private byte[] getResponse(byte[] APDU, String log) throws IOException {
|
||||
byte[] response = idCard.transceive(APDU);
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException(String.format("%s failed.", log));
|
||||
}
|
||||
Log.i(log, Hex.toHexString(response));
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to use the PACE protocol to create a secure channel with an Estonian ID-card
|
||||
*
|
||||
* @param CAN the card access number
|
||||
*/
|
||||
private byte[][] PACE(String CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||
private byte[][] PACE(byte[] CAN) throws IOException, NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
|
||||
|
||||
// select the ECC applet on the chip
|
||||
byte[] response = idCard.transceive(master);
|
||||
Log.i("Select applet", Hex.toHexString(response));
|
||||
// select the IAS-ECC application on the chip
|
||||
getResponse(selectMaster, "Select the master application");
|
||||
|
||||
// initiate PACE
|
||||
response = idCard.transceive(MSESetAT);
|
||||
Log.i("Authentication template", Hex.toHexString(response));
|
||||
getResponse(MSESetAT, "Set authentication template");
|
||||
|
||||
// get nonce
|
||||
response = idCard.transceive(GAGetNonce);
|
||||
Log.i("Get nonce", Hex.toHexString(response));
|
||||
byte[] response = getResponse(GAGetNonce, "Get nonce");
|
||||
byte[] decryptedNonce = decryptNonce(Arrays.copyOfRange(response, 4, response.length - 2), CAN);
|
||||
|
||||
// generate an EC keypair and exchange public keys with the chip
|
||||
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
|
||||
BigInteger privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE); // should be in [1, spec.getN()-1], but this is good enough for this application
|
||||
ECPoint publicKey = spec.getG().multiply(privateKey).normalize();
|
||||
byte[] APDU = createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i("Map nonce", Hex.toHexString(response));
|
||||
response = getResponse(createAPDU(GAMapNonceIncomplete, publicKey.getEncoded(false), 66), "Map nonce");
|
||||
ECPoint cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||
|
||||
// calculate the new base point, use it to generate a new keypair, and exchange public keys
|
||||
@@ -195,35 +191,41 @@ public class Comms {
|
||||
ECPoint mappedECBasePoint = spec.getG().multiply(new BigInteger(1, decryptedNonce)).add(sharedSecret).normalize();
|
||||
privateKey = new BigInteger(255, new SecureRandom()).add(BigInteger.ONE);
|
||||
publicKey = mappedECBasePoint.multiply(privateKey).normalize();
|
||||
APDU = createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i("Key agreement", Hex.toHexString(response));
|
||||
response = getResponse(createAPDU(GAKeyAgreementIncomplete, publicKey.getEncoded(false), 66), "Key agreement");
|
||||
cardPublicKey = spec.getCurve().decodePoint(Arrays.copyOfRange(response, 4, 69));
|
||||
|
||||
// generate the session keys and exchange MACs to verify them
|
||||
sharedSecret = cardPublicKey.multiply(privateKey).normalize();
|
||||
byte[] encodedSecret = sharedSecret.getAffineXCoord().getEncoded();
|
||||
byte[] keyEnc = createKey(encodedSecret, (byte) 1);
|
||||
byte[] keyMAC = createKey(encodedSecret, (byte) 2);
|
||||
APDU = createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65);
|
||||
byte[] MAC = getMAC(APDU, keyMAC);
|
||||
APDU = createAPDU(GAMutualAuthenticationIncomplete, MAC, 9);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i("Mutual authentication", Hex.toHexString(response));
|
||||
byte[] secret = cardPublicKey.multiply(privateKey).normalize().getAffineXCoord().getEncoded();
|
||||
byte[] keyEnc = createKey(secret, (byte) 1);
|
||||
byte[] keyMAC = createKey(secret, (byte) 2);
|
||||
byte[] MAC = getMAC(createAPDU(dataForMACIncomplete, cardPublicKey.getEncoded(false), 65), keyMAC);
|
||||
response = getResponse(createAPDU(GAMutualAuthenticationIncomplete, MAC, 9), "Mutual authentication");
|
||||
|
||||
// if the chip-side verification fails, crash and burn
|
||||
if (response.length == 2) throw new InvalidCANException();
|
||||
|
||||
// otherwise verify chip's MAC and return session keys
|
||||
APDU = createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65);
|
||||
MAC = getMAC(APDU, keyMAC);
|
||||
// verify chip's MAC and return session keys
|
||||
MAC = getMAC(createAPDU(dataForMACIncomplete, publicKey.getEncoded(false), 65), keyMAC);
|
||||
if (!Hex.toHexString(response, 4, 8).equals(Hex.toHexString(MAC))) {
|
||||
throw new AuthAppException("Could not verify chip's MAC.", 448); // Should never happen.
|
||||
throw new RuntimeException("Could not verify chip's MAC."); // *Should* never happen.
|
||||
}
|
||||
return new byte[][]{keyEnc, keyMAC};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a file and reads its contents
|
||||
*
|
||||
* @param FID file identifier of the required file
|
||||
* @param info string for logging
|
||||
* @return decrypted file contents
|
||||
*/
|
||||
private byte[] readFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
selectFile(FID, info);
|
||||
byte[] response = getResponse(new byte[0], readFile, "Read binary");
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException(String.format("Could not read %s", info));
|
||||
}
|
||||
return encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts or decrypts the APDU data
|
||||
*
|
||||
@@ -258,74 +260,182 @@ public class Comms {
|
||||
byte[] macData = new byte[data.length > 0 ? 48 + length : 48];
|
||||
macData[15] = ssc; // first block contains the ssc
|
||||
System.arraycopy(incomplete, 0, macData, 16, 4); // second block has the command
|
||||
macData[20] = -128; // elements are terminated by 0x80 and zero-padded to the next block
|
||||
macData[20] = (byte) 0x80; // elements are terminated by 0x80 and zero-padded to the next block
|
||||
System.arraycopy(incomplete, 5, macData, 32, 3); // third block contains appropriately encapsulated data/Le
|
||||
if (data.length > 0) { // if the APDU has data, add padding and encrypt it
|
||||
byte[] paddedData = Arrays.copyOf(data, length);
|
||||
paddedData[data.length] = -128;
|
||||
paddedData[data.length] = (byte) 0x80;
|
||||
encryptedData = encryptDecryptData(paddedData, Cipher.ENCRYPT_MODE);
|
||||
System.arraycopy(encryptedData, 0, macData, 35, encryptedData.length);
|
||||
}
|
||||
macData[35 + encryptedData.length] = -128;
|
||||
macData[35 + encryptedData.length] = (byte) 0x80;
|
||||
byte[] MAC = getMAC(macData, keyMAC);
|
||||
|
||||
// construct the APDU using the encrypted data and the MAC
|
||||
byte[] APDU = new byte[incomplete.length + encryptedData.length + MAC.length + 3];
|
||||
System.arraycopy(incomplete, 0, APDU, 0, incomplete.length);
|
||||
byte[] APDU = Arrays.copyOf(incomplete, incomplete.length + encryptedData.length + MAC.length + 3);
|
||||
if (encryptedData.length > 0) {
|
||||
System.arraycopy(encryptedData, 0, APDU, incomplete.length, encryptedData.length);
|
||||
}
|
||||
System.arraycopy(new byte[]{-114, 8}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
||||
System.arraycopy(new byte[]{(byte) 0x8E, 0x08}, 0, APDU, incomplete.length + encryptedData.length, 2); // MAC is encapsulated using the tag 0x8E
|
||||
System.arraycopy(MAC, 0, APDU, incomplete.length + encryptedData.length + 2, MAC.length);
|
||||
|
||||
ssc++;
|
||||
return APDU;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the personal data dedicated file
|
||||
*
|
||||
* @param FID the last bytes of file identifiers being requested
|
||||
* @return array containing the data strings
|
||||
* Selects a FILE by its identifier
|
||||
*
|
||||
*/
|
||||
public String[] readPersonalData(byte[] FID) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
private void selectFile(byte[] FID, String info) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
byte[] response = getResponse(FID, selectFile, String.format("Select %s", info));
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException(String.format("Could not select %s", info));
|
||||
}
|
||||
}
|
||||
|
||||
String[] personalData = new String[FID.length];
|
||||
byte[] data;
|
||||
byte[] APDU;
|
||||
byte[] response;
|
||||
/**
|
||||
* Gets the contents of the personal data dedicated file
|
||||
*
|
||||
* @param lastBytes the last bytes of the personal data file identifiers (0 < x < 16)
|
||||
* @return array containing the corresponding data strings
|
||||
*/
|
||||
public String[] readPersonalData(byte[] lastBytes) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
|
||||
String[] personalData = new String[lastBytes.length];
|
||||
int stringIndex = 0;
|
||||
|
||||
// select the master application
|
||||
selectFile(IASECCFID, "the master application");
|
||||
|
||||
// select the personal data dedicated file
|
||||
data = new byte[]{80, 0}; // personal data DF FID
|
||||
APDU = createSecureAPDU(data, personal);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i("Select personal data DF", Hex.toHexString(response));
|
||||
selectFile(personalDF, "the personal data DF");
|
||||
|
||||
// select and read the first 8 elementary files in the DF
|
||||
for (int i = 0; i < FID.length; i++) {
|
||||
byte[] FID = Arrays.copyOf(personalDF, personalDF.length);
|
||||
// select and read the personal data elementary files
|
||||
for (byte index : lastBytes) {
|
||||
|
||||
byte index = FID[i];
|
||||
if (index > 15 || index < 1) throw new AuthAppException("Invalid personal data FID.", 500);
|
||||
|
||||
data[1] = index;
|
||||
APDU = createSecureAPDU(data, personal);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i(String.format("Select EF 500%d", index), Hex.toHexString(response));
|
||||
|
||||
APDU = createSecureAPDU(new byte[0], read);
|
||||
response = idCard.transceive(APDU);
|
||||
Log.i(String.format("Read binary EF 500%d", index), Hex.toHexString(response));
|
||||
if (index > 15 || index < 1) throw new RuntimeException("Invalid personal data FID.");
|
||||
FID[1] = index;
|
||||
|
||||
// store the decrypted datum
|
||||
byte[] raw = encryptDecryptData(Arrays.copyOfRange(response, 3, 19), Cipher.DECRYPT_MODE);
|
||||
int indexOfTerminator = Hex.toHexString(raw).lastIndexOf("80") / 2;
|
||||
personalData[i] = new String(Arrays.copyOfRange(raw, 0, indexOfTerminator));
|
||||
byte[] response = readFile(FID, "a personal data EF");
|
||||
int indexOfTerminator = Hex.toHexString(response).lastIndexOf("80") / 2;
|
||||
personalData[stringIndex++] = new String(Arrays.copyOfRange(response, 0, indexOfTerminator));
|
||||
|
||||
}
|
||||
|
||||
return personalData;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to verify the selected PIN
|
||||
*
|
||||
* @param PIN user-provided PIN
|
||||
* @param oneOrTwo true for PIN1, false for PIN2
|
||||
*/
|
||||
private void verifyPIN(byte[] PIN, boolean oneOrTwo) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
|
||||
|
||||
selectFile(IASECCFID, "the master application");
|
||||
if (!oneOrTwo) {
|
||||
selectFile(QSCD, "the application");
|
||||
}
|
||||
|
||||
// pad the PIN and use the chip for verification
|
||||
byte[] paddedPIN = Hex.decode("ffffffffffffffffffffffff");
|
||||
System.arraycopy(PIN, 0, paddedPIN, 0, PIN.length);
|
||||
byte[] response = getResponse(paddedPIN, oneOrTwo ? verifyPIN1 : verifyPIN2, "PIN verification");
|
||||
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
if (response[response.length - 2] == 0x69 && response[response.length - 1] == (byte) 0x83) {
|
||||
throw new RuntimeException("Invalid PIN. Authentication method blocked.");
|
||||
} else {
|
||||
throw new RuntimeException(String.format("Invalid PIN. Attempts left: %d.", response[response.length - 1] + 64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the authentication or signature certificate from the chip
|
||||
*
|
||||
* @param authOrSign true for auth, false for sign cert
|
||||
* @return the requested certificate
|
||||
*/
|
||||
public byte[] getCertificate(boolean authOrSign) throws NoSuchPaddingException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, IOException {
|
||||
|
||||
selectFile(IASECCFID, "the master application");
|
||||
|
||||
selectFile(authOrSign ? AWP : QSCD, "the application");
|
||||
|
||||
selectFile(authOrSign ? authCert : signCert, "the certificate");
|
||||
|
||||
byte[] certificate = new byte[0];
|
||||
byte[] readCert = Arrays.copyOf(readFile, readFile.length);
|
||||
// Construct the certificate byte array n=indexOfTerminator bytes at a time
|
||||
for (int i = 0; i < 16; i++) {
|
||||
|
||||
// Set the P1/P2 values to incrementally read the certificate
|
||||
readCert[2] = (byte) (certificate.length / 256);
|
||||
readCert[3] = (byte) (certificate.length % 256);
|
||||
byte[] response = getResponse(new byte[0], readCert, "Read the certificate");
|
||||
if (response[response.length - 2] == 0x6b && response[response.length - 1] == 0x00) {
|
||||
throw new RuntimeException("Wrong read parameters.");
|
||||
}
|
||||
|
||||
// Set the range containing a portion of the certificate and decrypt it
|
||||
int start = response[2] == 1 ? 3 : 4;
|
||||
int end = start + (response[start - 2] + 256) % 256 - 1;
|
||||
byte[] decrypted = encryptDecryptData(Arrays.copyOfRange(response, start, end), Cipher.DECRYPT_MODE);
|
||||
int indexOfTerminator = Hex.toHexString(decrypted).lastIndexOf("80") / 2;
|
||||
certificate = Arrays.copyOf(certificate, certificate.length + indexOfTerminator);
|
||||
System.arraycopy(decrypted, 0, certificate, certificate.length - indexOfTerminator, indexOfTerminator);
|
||||
|
||||
if (response[response.length - 2] == (byte) 0x90 && response[response.length - 1] == 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return certificate;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the authentication token hash
|
||||
*
|
||||
* @param PIN1 PIN1
|
||||
* @param token the token hash to be signed
|
||||
* @return authentication token hash signature
|
||||
*/
|
||||
public byte[] authenticate(String PIN1, byte[] token) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
|
||||
verifyPIN(PIN1.getBytes(StandardCharsets.UTF_8), true);
|
||||
|
||||
selectFile(AWP, "the AWP application");
|
||||
|
||||
byte[] response = getResponse(Env, MSESetEnv, "Set environment");
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException("Setting the environment failed.");
|
||||
}
|
||||
|
||||
InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16));
|
||||
InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16));
|
||||
response = getResponse(token, InternalAuthenticate, "Internal Authenticate");
|
||||
|
||||
if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) {
|
||||
throw new RuntimeException("Signing the token failed.");
|
||||
}
|
||||
|
||||
byte[] signature = encryptDecryptData(Arrays.copyOfRange(response, 3, 115), Cipher.DECRYPT_MODE);
|
||||
int indexOfTerminator = Hex.toHexString(signature).lastIndexOf("80") / 2;
|
||||
|
||||
return Arrays.copyOf(signature, indexOfTerminator);
|
||||
}
|
||||
|
||||
|
||||
private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException {
|
||||
byte[] response = idCard.transceive(createSecureAPDU(data, command));
|
||||
Log.i(log, Hex.toHexString(response));
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -96,6 +96,7 @@ class PinFragment : Fragment() {
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
(activity as MainActivity).returnError(444)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
|
@@ -11,9 +11,11 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* ResultFragment is used to create a JWT and to send response to the website/application
|
||||
@@ -45,12 +47,20 @@ class ResultFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* Only used when the MobileAuthApp was launched by an app. Not for website use.
|
||||
* Not really the safest way of doing things, but sufficient for POC purposes.
|
||||
*/
|
||||
private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") {
|
||||
val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||
private fun createResponse(
|
||||
success: Boolean = true,
|
||||
idCode: String = "noCode",
|
||||
name: String = "noName",
|
||||
authority: String = "noAuthority"
|
||||
) {
|
||||
val responseCode =
|
||||
if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("result", result)
|
||||
resultIntent.putExtra("token", token)
|
||||
resultIntent.putExtra("idCode", idCode)
|
||||
resultIntent.putExtra("name", name)
|
||||
resultIntent.putExtra("authority", authority)
|
||||
requireActivity().setResult(responseCode, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
@@ -60,29 +70,39 @@ class ResultFragment : Fragment() {
|
||||
*/
|
||||
fun postToken() {
|
||||
val json = JsonObject()
|
||||
json.addProperty("token", paramsModel.token)
|
||||
json.addProperty("challenge", paramsModel.challenge)
|
||||
json.addProperty("auth-token", paramsModel.token)
|
||||
json.addProperty("error", 200)
|
||||
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
Ion.with(activity)
|
||||
.load(paramsModel.origin + paramsModel.authUrl)
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { e, result ->
|
||||
if (result == null) {
|
||||
if (args.mobile) {
|
||||
createResponse(false)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
val ion = Ion.with(activity)
|
||||
.load(paramsModel.authUrl)
|
||||
for ((header, value) in paramsModel.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { e, result ->
|
||||
Log.i("resultTag", result.toString())
|
||||
if (result == null) {
|
||||
if (args.mobile) {
|
||||
createResponse(false)
|
||||
} else {
|
||||
if (args.mobile) {
|
||||
createResponse(true, result.toString(), paramsModel.token)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
if (args.mobile) {
|
||||
val userData = result.asJsonObject["userData"]
|
||||
val idCode = userData.asJsonObject["idCode"].asString
|
||||
val name = userData.asJsonObject["name"].asString
|
||||
val authority = result.asJsonObject["roles"].asJsonArray[0].asJsonObject["authority"].asString
|
||||
createResponse(true, idCode, name, authority)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp.auth
|
||||
|
||||
/**
|
||||
* A specialised RuntimeException class for exceptions related to the mobile authentication app.
|
||||
* Possible error codes can be found at
|
||||
* https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Error-codes
|
||||
* @param message Error message
|
||||
* @param code An error code defined in the project wiki
|
||||
*/
|
||||
open class AuthAppException(message: String, var code: Int) : RuntimeException(message)
|
@@ -1,7 +0,0 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp.auth
|
||||
|
||||
/**
|
||||
* An AuthAppException for when the user entered CAN does not match the one read from the ID-card
|
||||
* @see AuthAppException
|
||||
*/
|
||||
class InvalidCANException : AuthAppException("Invalid CAN", 400)
|
@@ -1,5 +1,7 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Log.WARN
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ParametersViewModel: ViewModel() {
|
||||
@@ -16,6 +18,9 @@ class ParametersViewModel: ViewModel() {
|
||||
private var _origin: String = ""
|
||||
val origin get() = _origin
|
||||
|
||||
private var _headers: Map<String, String> = HashMap<String, String>()
|
||||
val headers get() =_headers
|
||||
|
||||
fun setChallenge(newChallenge: String) {
|
||||
_challenge = newChallenge
|
||||
}
|
||||
@@ -31,4 +36,9 @@ class ParametersViewModel: ViewModel() {
|
||||
fun setOrigin(newOrigin: String) {
|
||||
_origin = newOrigin
|
||||
}
|
||||
|
||||
fun setHeaders(newHeaders: Map<String, String>) {
|
||||
Log.i("HEADERS", newHeaders.toList().toString())
|
||||
_headers = newHeaders
|
||||
}
|
||||
}
|
@@ -1,29 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Must translate to English, but should work now -->
|
||||
<string name="app_name">NFC authentication</string>
|
||||
<string name="home_fragment">Work in progress</string>
|
||||
<string name="app_name">NFC authenticator</string>
|
||||
|
||||
<!-- BUTTONS -->
|
||||
<string name="cancel_text">CANCEL</string>
|
||||
<string name="return_text">BACK</string>
|
||||
<string name="add_can_text">ADD CAN</string>
|
||||
<string name="try_again_text">TRY AGAIN</string>
|
||||
<string name="continue_button">CONTINUE</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="wrong_can_text">Wrong CAN</string>
|
||||
<string name="action_detect">Put the ID card against the phone to detect it</string>
|
||||
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
|
||||
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
|
||||
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
|
||||
<string name="id_card_removed_early">ID card was removed too early</string>
|
||||
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 saved</string>
|
||||
<string name="pin_status_negative">PIN 1 not saved</string>
|
||||
<string name="can_status_saved">CAN saved</string>
|
||||
<string name="can_status_negative">CAN not saved</string>
|
||||
|
||||
<string name="begin_text">READ ID CARD</string>
|
||||
<string name="next_text">NEXT</string>
|
||||
<string name="cancel_text">CANCEL</string>
|
||||
<string name="save_text">SAVE</string>
|
||||
<string name="deny_text">NO</string>
|
||||
<string name="return_text">BACK</string>
|
||||
|
||||
<string name="help_text">HELP</string>
|
||||
<string name="can_question">What is CAN?</string>
|
||||
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Problem with parameters</string>
|
||||
<string name="problem_challenge">Challenge is missing</string>
|
||||
<string name="problem_authurl">AuthUrl is missing</string>
|
||||
<string name="problem_originurl">OriginUrl is missing</string>
|
||||
<string name="problem_other">Unspecified problem with parameters</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_fragment">Please enter PIN 1</string>
|
||||
<string name="enter_pin">PIN 1</string>
|
||||
<string name="example_pin">Example. 1234</string>
|
||||
<string name="length_pin">Allowed length for PIN 1 is 4..12</string>
|
||||
<string name="pin_save_request">PIN 1 is currently not saved. Do you wish to save the entered PIN 1? Saved PIN 1 will be entered automatically in the future. Saved PIN 1 can be changed and deleted in the settings menu.</string>
|
||||
<string name="save_pin_title">Save PIN 1</string>
|
||||
<string name="pin_view">Please enter PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 must be 4–12 digits long</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Please enter PIN 2</string>
|
||||
@@ -32,21 +49,14 @@
|
||||
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="example_can">Example. 123456</string>
|
||||
<string name="text_can">CAN</string>
|
||||
<string name="enter_can">Enter ID card\'s CAN (Card Access Number)</string>
|
||||
<string name="length_can">Length of the CAN is wrong</string>
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="save_can_title">Save CAN</string>
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
||||
<string name="auth_instruction_text">Put the ID card against the phone</string>
|
||||
<string name="time_left">Time left %d sek</string>
|
||||
<string name="no_time">No time left</string>
|
||||
<string name="err_unknown">Unknown error</string>
|
||||
<string name="invalid_can">Wrong CAN</string>
|
||||
<string name="tag_lost">Connection between device and ID-card lost</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NAME</string>
|
||||
@@ -55,18 +65,14 @@
|
||||
<string name="expiration_label">DATE OF EXPIRY</string>
|
||||
<string name="citizenship_label">CITIZENSHIP</string>
|
||||
<string name="gender_label">SEX</string>
|
||||
<string name="clear_button">FORGET</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">Wait for the app to close</string>
|
||||
<string name="result_text">Checking the created token</string>
|
||||
<string name="result_info">The app will close automatically</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
<string name="menu_language_title">Language</string>
|
||||
<string name="menu_action_unavailable">Currently unavailable</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_add">Add CAN</string>
|
||||
<string name="can_delete">Delete CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Add PIN1</string>
|
||||
@@ -75,9 +81,7 @@
|
||||
<string name="show">SHOW</string>
|
||||
<string name="hide">HIDE</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="unavailable">Settings currently unavailabe</string>
|
||||
<string name="can_save_request">CAN is currently not saved. Do you wish to save the CAN? Saved CAN will be entered automatically in the future. Saved CAN can be changed and deleted in the settings menu.</string>
|
||||
<string name="err_reading_card">Failed to read data from the ID-card</string>
|
||||
<string name="err_internal">Internal error</string>
|
||||
<string name="err_bad_data">Read bad data from the ID-card, try using the card again</string>
|
||||
<string name="menu_unavailable_message">Settings are currently unavailable</string>
|
||||
<string name="can_deleted">CAN deleted</string>
|
||||
<string name="pin_deleted">PIN 1 deleted</string>
|
||||
</resources>
|
@@ -1,28 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">NFC authentication</string>
|
||||
<string name="home_fragment">Work in progress</string>
|
||||
<string name="app_name">NFC autentija</string>
|
||||
|
||||
<string name="begin_text">LOE ID KAARTI</string>
|
||||
<string name="next_text">EDASI</string>
|
||||
<!-- Buttons -->
|
||||
<string name="cancel_text">KATKESTA</string>
|
||||
<string name="save_text">SALVESTA</string>
|
||||
<string name="deny_text">EI</string>
|
||||
<string name="return_text">TAGASI</string>
|
||||
<string name="add_can_text">LISA CAN</string>
|
||||
<string name="try_again_text">ÜRITA UUESTI</string>
|
||||
<string name="continue_button">JÄTKA</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Kaart tuvastatud. Hoia kaarti vastu telefoni.</string>
|
||||
<string name="data_read">Andmed loetud, võid jätkata.</string>
|
||||
<string name="wrong_can_text">Vale CAN</string>
|
||||
<string name="action_detect">ID kaardi tuvastamiseks pane kaart vastu telefoni</string>
|
||||
<string name="action_detect_unavailable">ID kaardi tuvastamiseks peab olema CAN lisatud</string>
|
||||
<string name="nfc_not_available">NFC ei ole sisse lülitatud või puudub telefonil NFC võimekus</string>
|
||||
<string name="nfc_reading_error">Sisestatud CAN ei ole vastavuses ID kaardiga</string>
|
||||
<string name="id_card_removed_early">ID kaart eemaldati liiga vara</string>
|
||||
<string name="wrong_pin">Vale PIN 1. ID kaardil PIN 1 sisetamise kordi alles: %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 on salvestatud</string>
|
||||
<string name="pin_status_negative">PIN 1 ei ole salvestatud</string>
|
||||
<string name="can_status_saved">CAN on salvestatud</string>
|
||||
<string name="can_status_negative">CAN ei ole salvestatud</string>
|
||||
|
||||
<string name="help_text">INFO</string>
|
||||
<string name="can_question">Mis on CAN?</string>
|
||||
<string name="can_explanation">CAN on 6 kohaline numbritest koosnev kood, mida on vaja ID kaardiga suhtlemiseks. CAN-i leiab ID kaardilt omaniku pildi alt pealkirjaga KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Probleem parameetritega</string>
|
||||
<string name="problem_challenge">Puudub challenge parameeter</string>
|
||||
<string name="problem_authurl">Puudub AuthUrl parameeter</string>
|
||||
<string name="problem_originurl">Puudub OriginUrl parameeter</string>
|
||||
<string name="problem_other">Täpsustamata probleem parameetritega</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_fragment">Palun sisesta PIN 1</string>
|
||||
<string name="enter_pin">PIN 1</string>
|
||||
<string name="example_pin">Näide. 1234</string>
|
||||
<string name="length_pin">PIN 1 lubatud pikkus on 4..12</string>
|
||||
<string name="pin_save_request">Praegu ei ole rakenduses PIN 1 salvestatud. Kas sa soovid sisestatud PIN 1-te salvestada? Sellisel juhul sisestatakse see järgmisel korral automaatselt. Salvestatud PIN 1-te saab alati menüüs muuta ja kustutada.</string>
|
||||
<string name="save_pin_title">Salvesta PIN 1</string>
|
||||
<string name="pin_view">Palun sisesta PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 lubatud pikkus on 4..12</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Palun sisesta PIN 2</string>
|
||||
@@ -31,27 +48,19 @@
|
||||
<string name="length_pin2">PIN 2 lubatud pikkus on 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="example_can">Näide. 123456</string>
|
||||
<string name="text_can">CAN</string>
|
||||
<string name="enter_can">Sisesta ID kaardi CAN (Card Access Number)</string>
|
||||
<string name="length_can">CANi pikkus on vale</string>
|
||||
<string name="card_detected">Kaart on tuvastatud. Hoia kaarti vastu telefoni.</string>
|
||||
<string name="data_read">Andmed loetud. Võid edasi minna.</string>
|
||||
<string name="can_save_request">Praegu ei ole rakenduses CAN salvestatud. Kas sa soovid sisestatud CANi salvestada? Sellisel juhul sisestatakse see järgmisel korral automaatselt. Salvestatud CANi saab alati menüüs muuta ja kustutada.</string> <string name="save_can_title">Salvesta CAN</string>
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">ID kaardiga ühenduse loomiseks pane kaart vastu telefoni</string>
|
||||
<string name="auth_instruction_text">Pane ID kaart vastu telefoni</string>
|
||||
<string name="time_left">Aega on jäänud %d sek</string>
|
||||
<string name="no_time">Aeg on otsas</string>
|
||||
<string name="err_unknown">Tundmatu viga</string>
|
||||
<string name="invalid_can">Vale CAN</string>
|
||||
<string name="tag_lost">Ühendus seadme ja kaardi vahel katkes</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NIMI</string>
|
||||
<string name="user_name">%1$s %2$s</string>
|
||||
<string name="identification_number_label">ISIKUKOOD</string>
|
||||
<string name="clear_button">UNUSTA</string>
|
||||
<string name="expiration_label">KEHTIV KUNI</string>
|
||||
<string name="citizenship_label">KODAKONDSUS</string>
|
||||
<string name="gender_label">SUGU</string>
|
||||
@@ -62,10 +71,7 @@
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Seaded</string>
|
||||
<string name="menu_language_title">Keel</string>
|
||||
<string name="menu_action_unavailable">Toiming pole hetkel saadaval</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_add">Lisa CAN</string>
|
||||
<string name="can_delete">Kustuta CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Lisa PIN1</string>
|
||||
@@ -74,8 +80,7 @@
|
||||
<string name="show">NÄITA</string>
|
||||
<string name="hide">PEIDA</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="unavailable">Seaded pole hetkel saadaval</string>
|
||||
<string name="err_reading_card">Ei saanud ID-kaardilt andmeid lugeda</string>
|
||||
<string name="err_internal">Rakendusesisene viga</string>
|
||||
<string name="err_bad_data">ID-kaardilt loeti vigased andmed, proovi uuesti kaarti kasutada</string>
|
||||
<string name="menu_unavailable_message">Seaded pole hetkel saadaval</string>
|
||||
<string name="can_deleted">CAN kustatud</string>
|
||||
<string name="pin_deleted">PIN 1 kustatud</string>
|
||||
</resources>
|
@@ -1,27 +1,44 @@
|
||||
<resources>
|
||||
<string name="app_name">NFC authentication</string>
|
||||
<string name="home_fragment">Work in progress</string>
|
||||
<string name="app_name">NFC authenticator</string>
|
||||
|
||||
<string name="begin_text">READ ID CARD</string>
|
||||
<string name="next_text">NEXT</string>
|
||||
<!-- BUTTONS -->
|
||||
<string name="cancel_text">CANCEL</string>
|
||||
<string name="save_text">SAVE</string>
|
||||
<string name="deny_text">NO</string>
|
||||
<string name="return_text">BACK</string>
|
||||
<string name="add_can_text">ADD CAN</string>
|
||||
<string name="try_again_text">TRY AGAIN</string>
|
||||
<string name="continue_button">CONTINUE</string>
|
||||
|
||||
<!-- Card Detection related -->
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="wrong_can_text">Wrong CAN</string>
|
||||
<string name="action_detect">Put the ID card against the phone to detect it</string>
|
||||
<string name="action_detect_unavailable">CAN must be added before ID card can be detected</string>
|
||||
<string name="nfc_not_available">NFC is not turned on or is not supported by the phone</string>
|
||||
<string name="nfc_reading_error">The provided CAN does not match the ID card</string>
|
||||
<string name="id_card_removed_early">ID card was removed too early</string>
|
||||
<string name="wrong_pin">Wrong PIN 1. Tries on the card left %s</string>
|
||||
|
||||
<!-- string resources for HomeFragment -->
|
||||
<string name="pin_status_saved">PIN 1 saved</string>
|
||||
<string name="pin_status_negative">PIN 1 not saved</string>
|
||||
<string name="can_status_saved">CAN saved</string>
|
||||
<string name="can_status_negative">CAN not saved</string>
|
||||
|
||||
<string name="help_text">HELP</string>
|
||||
<string name="can_question">What is CAN?</string>
|
||||
<string name="can_explanation">CAN is a 6 digit code that is needed to communicate with an ID card. It can be found on the ID card under the card holder\'s picture with a title KASUTAJA ALLKIRI/HOLDER\'S SIGNATURE.</string>
|
||||
<string name="problem_parameters">Problem with parameters</string>
|
||||
<string name="problem_challenge">Challenge is missing</string>
|
||||
<string name="problem_authurl">AuthUrl is missing</string>
|
||||
<string name="problem_originurl">OriginUrl is missing</string>
|
||||
<string name="problem_other">Unspecified problem with parameters</string>
|
||||
<!-- string resources for PinFragment -->
|
||||
<string name="pin_fragment">Please enter PIN 1</string>
|
||||
<string name="enter_pin">PIN 1</string>
|
||||
<string name="example_pin">Example. 1234</string>
|
||||
<string name="length_pin">Allowed length for PIN 1 is 4..12</string>
|
||||
<string name="pin_save_request">PIN 1 is currently not saved. Do you wish to save the entered PIN 1? Saved PIN 1 will be entered automatically in the future. Saved PIN 1 can be changed and deleted in the settings menu.</string>
|
||||
<string name="save_pin_title">Save PIN 1</string>
|
||||
<string name="pin_view">Please enter PIN 1</string>
|
||||
<string name="hint_pin">PIN 1</string>
|
||||
<string name="pin_helper_text">PIN 1 must be 4–12 digits long</string>
|
||||
<string name="save_pin">Save PIN 1</string>
|
||||
<string name="pin_save_on">On</string>
|
||||
<string name="pin_save_off">Off</string>
|
||||
|
||||
<!-- string resources for Pin2Fragment -->
|
||||
<string name="pin2_fragment">Please enter PIN 2</string>
|
||||
@@ -30,22 +47,14 @@
|
||||
<string name="length_pin2">Allowed length for PIN 2 is 5..12</string>
|
||||
|
||||
<!-- string resources for CanFragment -->
|
||||
<string name="example_can">Example. 123456</string>
|
||||
<string name="text_can">CAN</string>
|
||||
<string name="enter_can">Enter ID card\'s CAN (Card Access Number)</string>
|
||||
<string name="length_can">Length of the CAN is wrong</string>
|
||||
<string name="card_detected">Card detected. Hold it against the phone.</string>
|
||||
<string name="data_read">Data read. You can continue.</string>
|
||||
<string name="can_save_request">CAN is currently not saved. Do you wish to save the CAN? Saved CAN will be entered automatically in the future. Saved CAN can be changed and deleted in the settings menu.</string>
|
||||
<string name="save_can_title">Save CAN</string>
|
||||
<string name="can_view">Please enter CAN</string>
|
||||
<string name="can_text">CAN</string>
|
||||
<string name="can_helper_text">CAN must be 6 digits long</string>
|
||||
|
||||
<!-- string resources for AuthFragment layout -->
|
||||
<string name="auth_instruction_text">Put the ID card against the phone to establish connection</string>
|
||||
<string name="auth_instruction_text">Put the ID card against the phone</string>
|
||||
<string name="time_left">Time left %d sek</string>
|
||||
<string name="no_time">No time left</string>
|
||||
<string name="err_unknown">Unknown error</string>
|
||||
<string name="invalid_can">Wrong CAN</string>
|
||||
<string name="tag_lost">Connection between device and ID-card lost</string>
|
||||
|
||||
<!-- string resources for UserFragment layout -->
|
||||
<string name="user_name_label">NAME</string>
|
||||
@@ -54,18 +63,14 @@
|
||||
<string name="expiration_label">DATE OF EXPIRY</string>
|
||||
<string name="citizenship_label">CITIZENSHIP</string>
|
||||
<string name="gender_label">SEX</string>
|
||||
<string name="clear_button">FORGET</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">Wait for the app to close</string>
|
||||
<string name="result_info">The app will close automatically</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
<string name="menu_language_title">Language</string>
|
||||
<string name="menu_action_unavailable">Currently unavailable</string>
|
||||
<string name="saved_can">CAN: %s</string>
|
||||
<string name="can_add">Add CAN</string>
|
||||
<string name="can_delete">Delete CAN</string>
|
||||
<string name="saved_pin">PIN1: %s</string>
|
||||
<string name="pin1_add">Add PIN 1</string>
|
||||
@@ -74,8 +79,7 @@
|
||||
<string name="show">SHOW</string>
|
||||
<string name="hide">HIDE</string>
|
||||
<string name="hidden_pin">****</string>
|
||||
<string name="unavailable">Settings currently unavailable</string>
|
||||
<string name="err_reading_card">Failed to read data from the ID-card</string>
|
||||
<string name="err_internal">Internal error</string>
|
||||
<string name="err_bad_data">Read bad data from the ID-card, try using the card again</string>
|
||||
<string name="menu_unavailable_message">Settings are currently unavailable</string>
|
||||
<string name="can_deleted">CAN deleted</string>
|
||||
<string name="pin_deleted">PIN 1 deleted</string>
|
||||
</resources>
|
@@ -43,4 +43,5 @@ dependencies {
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.testmobileapp">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
@@ -9,17 +9,19 @@ import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.example.testmobileapp.databinding.ActivityMainBinding
|
||||
import com.koushikdutta.ion.Ion
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Base url where the requests should be made. Add yours here. It must use https.
|
||||
*/
|
||||
private const val BASE_URL = "https-base-url-here"
|
||||
private const val BASE_URL = "https://a0fe-2001-7d0-88ab-b880-7571-cba0-5db2-11b7.ngrok.io"
|
||||
private const val AUTH_URL = "$BASE_URL/auth/login"
|
||||
private const val CHALLENGE_URL = "$BASE_URL/auth/challenge"
|
||||
|
||||
/**
|
||||
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
|
||||
* Test mobile app to demonstrate how other applications could potentially use MobileAuthApp.
|
||||
* Single purpose app that launches the MobileAuthApp and gets the response back (JWT).
|
||||
* Only for demo purposes.
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -31,19 +33,18 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
|
||||
if (response.resultCode == Activity.RESULT_OK) {
|
||||
binding.loginTextView.text = getString(R.string.auth_success)
|
||||
// Logs are used to show what information can be retrieved from the mobileauthapp.
|
||||
Log.i("getResult", response.data?.getStringExtra("token").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("result").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("idCode").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("name").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("authority").toString())
|
||||
var user = ""
|
||||
try {
|
||||
val resultObject = JSONObject(response.data?.getStringExtra("result").toString())
|
||||
user = resultObject.getString("principal")
|
||||
user = response.data?.getStringExtra("name").toString()
|
||||
} catch (e: Exception) {
|
||||
Log.i("getResult", "unable to retrieve name from principal")
|
||||
Log.i("getResult", "unable to retrieve name")
|
||||
}
|
||||
showResult(user)
|
||||
}
|
||||
@@ -54,47 +55,27 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
showLogin()
|
||||
|
||||
binding.loginOptionNfcButton.setOnClickListener { getData() }
|
||||
binding.loginOptionNfcButton.setOnClickListener {
|
||||
launchAuth()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates an intent to launch the MobileAuthApp
|
||||
*/
|
||||
private fun launchAuth(challenge: String = "challenge", originUrl: String = "baseUrl", authUrl: String = "authUrl") {
|
||||
private fun launchAuth() {
|
||||
val launchIntent = Intent()
|
||||
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
|
||||
launchIntent.putExtra("action", "auth")
|
||||
launchIntent.putExtra("challenge", challenge)
|
||||
launchIntent.putExtra("originUrl", originUrl)
|
||||
launchIntent.putExtra("authUrl", authUrl)
|
||||
launchIntent.putExtra("challenge", CHALLENGE_URL)
|
||||
launchIntent.putExtra("originUrl", BASE_URL)
|
||||
launchIntent.putExtra("authUrl", AUTH_URL)
|
||||
launchIntent.putExtra("headers","${(0..100000).random()}")
|
||||
launchIntent.putExtra("mobile", true)
|
||||
authLauncher.launch(launchIntent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for retrieving data from an endpoint.
|
||||
* Ion library is used as it is very convenient for making simple GET requests.
|
||||
*/
|
||||
private fun getData() {
|
||||
// Enter the server endpoint address to here
|
||||
val url = "$BASE_URL/auth/challenge"
|
||||
Ion.getDefault(this).conscryptMiddleware.enable(false)
|
||||
Ion.with(applicationContext)
|
||||
.load(url)
|
||||
.asJsonObject()
|
||||
.setCallback { _, result ->
|
||||
try {
|
||||
// Get data from the result and call launchAuth method
|
||||
val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
|
||||
Log.v("Challenge", challenge)
|
||||
launchAuth(challenge, BASE_URL, "/auth/authentication")
|
||||
} catch (e: Exception) {
|
||||
Log.i("GETrequest", "was unsuccessful")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLogin() {
|
||||
binding.loginOptions.visibility = View.VISIBLE
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
@@ -7,7 +8,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.0.2"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
|
||||
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
3
demoBackend/.gitignore
vendored
3
demoBackend/.gitignore
vendored
@@ -31,3 +31,6 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### web-eid.js ###
|
||||
src/demo-website/src/web-eid.js
|
@@ -32,7 +32,7 @@ Copy the second forwarding link (the one with https) and put it in ```com.tarkva
|
||||
### 3. Run the project
|
||||
Use your favourite IDE or just run it via commandline with ```./mvnw spring-boot:run```
|
||||
|
||||
On your Android device browser navigate to the url you copied earlier and you should see the website landing page. If you have the mobile authentication app installed, you should be able to log into the website with your Estonian ID-card.
|
||||
On your browser (Android to test out from Android device or desktop to try out ID-card reader or QR-code capability) navigate to the url you copied earlier and you should see the website landing page. If you have the mobile authentication app installed, you should be able to log into the website with your Estonian ID-card.
|
||||
|
||||
|
||||
## Credits...
|
||||
|
@@ -13,11 +13,14 @@
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>demoBackend</name>
|
||||
<description>demoBackend</description>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<java.version>11</java.version>
|
||||
<kotlin.version>1.5.31</kotlin.version>
|
||||
<caffeine.version>2.8.5</caffeine.version>
|
||||
<javaxcache.version>1.1.1</javaxcache.version>
|
||||
<node.version>v16.13.0</node.version>
|
||||
<npm.version>8.1.4</npm.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@@ -91,6 +94,7 @@
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<finalName>demo</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -115,6 +119,91 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<!-- Plugin to install node and npm and then build the vue project -->
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.12.0</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<id>Install node and npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<nodeVersion>${node.version}</nodeVersion>
|
||||
<npmVersion>${npm.version}</npmVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>generate-resources</phase>
|
||||
<configuration>
|
||||
<arguments>install</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>npm build</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>process-resources</phase>
|
||||
<configuration>
|
||||
<arguments>run build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<nodeVersion>${node.version}</nodeVersion>
|
||||
<workingDirectory>src/demo-website</workingDirectory>
|
||||
<!-- <installDirectory>src/demo-website/dist</installDirectory>-->
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Plugin to copy built vue project from src/frontend/dist to target/classes/static -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>Copy web-eid.js file to Vue root folder.</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>src/demo-website/src</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/demo-website/node_modules/@web-eid/web-eid-library/dist/es</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>Copy Vue frontend into Spring Boot target static folder</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>target/classes/static</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/demo-website/dist</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
23
demoBackend/src/demo-website/.gitignore
vendored
Normal file
23
demoBackend/src/demo-website/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
BIN
demoBackend/src/demo-website/.npmrc
Normal file
BIN
demoBackend/src/demo-website/.npmrc
Normal file
Binary file not shown.
24
demoBackend/src/demo-website/README.md
Normal file
24
demoBackend/src/demo-website/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# demo-website
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
demoBackend/src/demo-website/babel.config.js
Normal file
5
demoBackend/src/demo-website/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
BIN
demoBackend/src/demo-website/node/node.exe
Normal file
BIN
demoBackend/src/demo-website/node/node.exe
Normal file
Binary file not shown.
44
demoBackend/src/demo-website/node/npm
Normal file
44
demoBackend/src/demo-website/node/npm
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix
|
||||
|
||||
basedir=`dirname "$0"`
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
NODE_EXE="$basedir/node.exe"
|
||||
if ! [ -x "$NODE_EXE" ]; then
|
||||
NODE_EXE="$basedir/node"
|
||||
fi
|
||||
if ! [ -x "$NODE_EXE" ]; then
|
||||
NODE_EXE=node
|
||||
fi
|
||||
|
||||
# this path is passed to node.exe, so it needs to match whatever
|
||||
# kind of paths Node.js thinks it's using, typically win32 paths.
|
||||
CLI_BASEDIR="$("$NODE_EXE" -p 'require("path").dirname(process.execPath)')"
|
||||
NPM_CLI_JS="$CLI_BASEDIR/node_modules/npm/bin/npm-cli.js"
|
||||
|
||||
NPM_PREFIX=`"$NODE_EXE" "$NPM_CLI_JS" prefix -g`
|
||||
if [ $? -ne 0 ]; then
|
||||
# if this didn't work, then everything else below will fail
|
||||
echo "Could not determine Node.js install directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js"
|
||||
|
||||
# a path that will fail -f test on any posix bash
|
||||
NPM_WSL_PATH="/.."
|
||||
|
||||
# WSL can run Windows binaries, so we have to give it the win32 path
|
||||
# however, WSL bash tests against posix paths, so we need to construct that
|
||||
# to know if npm is installed globally.
|
||||
if [ `uname` = 'Linux' ] && type wslpath &>/dev/null ; then
|
||||
NPM_WSL_PATH=`wslpath "$NPM_PREFIX_NPM_CLI_JS"`
|
||||
fi
|
||||
if [ -f "$NPM_PREFIX_NPM_CLI_JS" ] || [ -f "$NPM_WSL_PATH" ]; then
|
||||
NPM_CLI_JS="$NPM_PREFIX_NPM_CLI_JS"
|
||||
fi
|
||||
|
||||
"$NODE_EXE" "$NPM_CLI_JS" "$@"
|
19
demoBackend/src/demo-website/node/npm.cmd
Normal file
19
demoBackend/src/demo-website/node/npm.cmd
Normal file
@@ -0,0 +1,19 @@
|
||||
:: Created by npm, please don't edit manually.
|
||||
@ECHO OFF
|
||||
|
||||
SETLOCAL
|
||||
|
||||
SET "NODE_EXE=%~dp0\node.exe"
|
||||
IF NOT EXIST "%NODE_EXE%" (
|
||||
SET "NODE_EXE=node"
|
||||
)
|
||||
|
||||
SET "NPM_CLI_JS=%~dp0\node_modules\npm\bin\npm-cli.js"
|
||||
FOR /F "delims=" %%F IN ('CALL "%NODE_EXE%" "%NPM_CLI_JS%" prefix -g') DO (
|
||||
SET "NPM_PREFIX_NPM_CLI_JS=%%F\node_modules\npm\bin\npm-cli.js"
|
||||
)
|
||||
IF EXIST "%NPM_PREFIX_NPM_CLI_JS%" (
|
||||
SET "NPM_CLI_JS=%NPM_PREFIX_NPM_CLI_JS%"
|
||||
)
|
||||
|
||||
"%NODE_EXE%" "%NPM_CLI_JS%" %*
|
27448
demoBackend/src/demo-website/package-lock.json
generated
Normal file
27448
demoBackend/src/demo-website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
demoBackend/src/demo-website/package.json
Normal file
51
demoBackend/src/demo-website/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "demo-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
"@web-eid/web-eid-library": "../../../../web-eid.js/",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^3.0.0",
|
||||
"vue-cookie-next": "^1.3.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vue3-cookies": "^1.0.6",
|
||||
"vuex": "^4.0.2",
|
||||
"vuex-persistedstate": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap-vue-3": "^0.0.5",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
BIN
demoBackend/src/demo-website/public/favicon.ico
Normal file
BIN
demoBackend/src/demo-website/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
17
demoBackend/src/demo-website/public/index.html
Normal file
17
demoBackend/src/demo-website/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
26
demoBackend/src/demo-website/src/App.vue
Normal file
26
demoBackend/src/demo-website/src/App.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#nav a {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#nav a.router-link-exact-active {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
BIN
demoBackend/src/demo-website/src/assets/logo.png
Normal file
BIN
demoBackend/src/demo-website/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
134
demoBackend/src/demo-website/src/components/Login.vue
Normal file
134
demoBackend/src/demo-website/src/components/Login.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="container container-md d-flex flex-column">
|
||||
<div>
|
||||
<h3 class="text-center">Welcome to Estonian ID card mobile authentication demo website.</h3>
|
||||
<p>This website to demonstrates the viability of using your NFC-enabled ID-card and your smartphone to authenticate yourself.
|
||||
This is a proof of concept solution, so currently only authentication is supported. This solution was created for <a href="https://courses.cs.ut.ee/2021/tvp/">Software Project (Tarkvaraprojekt)</a> course in the University of Tartu
|
||||
in cooperation with <a href="https://github.com/martinpaljak/">Martin Paljak</a>.</p>
|
||||
<p>This solution is meant to be web-eid.js compatible, so this example website uses a <a href="https://github.com/TanelOrumaa/web-eid.js">fork of web-eid.js</a> which supports the Android authentication app.</p>
|
||||
<h2>Usage</h2>
|
||||
<p>To get started, download and install the authentication Android app from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/releases">GitHub</a> (Android 8.0+ required).
|
||||
You can then click "Login" to authenticate yourself on this demo website with the app or if you are using a non-Android device, you can use both the app or the default web-eid.js option to login using the smartcard reader.
|
||||
</p>
|
||||
|
||||
<p class="text-center">Read more from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p>
|
||||
</div>
|
||||
<div class="justify-content-center d-flex">
|
||||
<div id="canvas"></div>
|
||||
</div>
|
||||
<div class="justify-content-center d-flex">
|
||||
<button type="button" class="btn loginButton btn-dark" v-on:click="authenticate">
|
||||
<div v-if="loading" class="d-flex justify-content-center">
|
||||
<div class="spinner-border text-light spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>Authenticate</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="btn-group-sm d-flex justify-content-center" v-if="!isAndroidDevice" role="group" aria-label="Basic radio toggle button group">
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnCardReader" autocomplete="off" v-on:click="useCardReader">
|
||||
<label class="btn btn-outline-secondary" for="btnCardReader">using ID-card reader</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="btnradio" id="btnApp" autocomplete="off" checked v-on:click="useApp">
|
||||
<label class="btn btn-outline-secondary" for="btnApp">using Android App</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import * as webeid from '../web-eid.js';
|
||||
import router from "@/router";
|
||||
|
||||
export default {
|
||||
name: 'LoginComponent',
|
||||
data() {
|
||||
return {
|
||||
useAndroidApp: true,
|
||||
loading: false,
|
||||
challenge: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
useApp: function() {
|
||||
this.useAndroidApp = true;
|
||||
},
|
||||
|
||||
useCardReader: function() {
|
||||
this.useAndroidApp = false;
|
||||
},
|
||||
|
||||
|
||||
|
||||
authenticate: async function () {
|
||||
this.loading = true;
|
||||
|
||||
const options = {
|
||||
getAuthChallengeUrl: window.location.origin + "/auth/challenge",
|
||||
postAuthTokenUrl: window.location.origin + "/auth/login",
|
||||
getAuthSuccessUrl: window.location.origin + "/auth/login",
|
||||
useAuthApp: this.useAndroidApp,
|
||||
headers: {
|
||||
"sessionId": this.$store.getters.getSessionId
|
||||
},
|
||||
};
|
||||
|
||||
console.log(options);
|
||||
|
||||
try {
|
||||
const response = await webeid.authenticate(options);
|
||||
console.log("Authentication successful! Response:", response);
|
||||
this.loading = false;
|
||||
this.$store.dispatch("setLoggedIn", true);
|
||||
await router.push("welcome");
|
||||
|
||||
} catch (error) {
|
||||
console.log("Authentication failed! Error:", error);
|
||||
alert(error.message);
|
||||
this.loading = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn() {
|
||||
return this.$store.getAuthenticated;
|
||||
},
|
||||
loading() {
|
||||
return this.loading;
|
||||
},
|
||||
isAndroidDevice() {
|
||||
return this.$store.getters.getIsAndroid
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const isAndroid = webeid.isAndroidDevice();
|
||||
this.$store.dispatch("setIsAndroid", isAndroid);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container > div {
|
||||
margin-top: 2vh;
|
||||
}
|
||||
.loginButton {
|
||||
height: 4vh;
|
||||
width: 20vh;
|
||||
line-height: 3vh;
|
||||
}
|
||||
|
||||
.loginButton > p {
|
||||
font-size: 3vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
height: 30vh;
|
||||
width: 30vh;
|
||||
}
|
||||
</style>
|
50
demoBackend/src/demo-website/src/components/Navbar.vue
Normal file
50
demoBackend/src/demo-website/src/components/Navbar.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<!-- As a heading -->
|
||||
<nav class="navbar navbar-dark bg-dark container-fluid flex-row">
|
||||
<div class="">
|
||||
<span class="navbar-brand mb-0 h1">Mobile authentication demo</span>
|
||||
</div>
|
||||
<div v-if="isLoggedIn" class="nav-item">
|
||||
<button type="button" class="btn btn-light" v-on:click="logOut">Log out</button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from "@/router";
|
||||
|
||||
export default {
|
||||
name: "Navbar",
|
||||
computed: {
|
||||
isLoggedIn() {
|
||||
return this.$store.getters.getAuthenticated;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logOut: function () {
|
||||
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: {"sessionId": this.$store.getters.getSessionId}
|
||||
};
|
||||
fetch("/auth/logout", requestOptions)
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
this.$store.dispatch("setLoggedIn", false);
|
||||
router.push("/");
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const sessionId = this.$cookie.getCookie("JSESSIONID");
|
||||
this.$store.dispatch("fetchSessionId", sessionId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
height: 7vh;
|
||||
}
|
||||
</style>
|
65
demoBackend/src/demo-website/src/components/Welcome.vue
Normal file
65
demoBackend/src/demo-website/src/components/Welcome.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="container container-md d-flex flex-column">
|
||||
<div>
|
||||
<h3 class="text-center">Welcome {{ userName }}!</h3>
|
||||
<h4 class="text-center">{{ userIdCode }}</h4>
|
||||
<p class="text-center">You've successfully logged into this site using your ID card.</p>
|
||||
<p class="text-center">Read more from <a
|
||||
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WelcomeComponent',
|
||||
props: {},
|
||||
methods: {
|
||||
getUserData: async function () {
|
||||
const requestOptions = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"sessionid": this.$store.getters.getSessionId
|
||||
}
|
||||
};
|
||||
fetch("/auth/userData", requestOptions)
|
||||
.then((response) => {
|
||||
let data = response.body;
|
||||
data.getReader().read().then((body) => {
|
||||
let authObject = JSON.parse(new TextDecoder().decode(body.value));
|
||||
this.$store.dispatch("setUserName", authObject.userData.name);
|
||||
let idCode = authObject.userData.idCode.substring(6)
|
||||
console.log(idCode)
|
||||
this.$store.dispatch("setUserIdCode", idCode);
|
||||
});
|
||||
console.log(data);
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn() {
|
||||
return this.$store.getters.getAuthenticated;
|
||||
},
|
||||
userName() {
|
||||
return this.$store.getters.getUserName;
|
||||
},
|
||||
userIdCode() {
|
||||
return this.$store.getters.getUserIdCode;
|
||||
}
|
||||
}
|
||||
,
|
||||
mounted() {
|
||||
// Get user data.
|
||||
this.getUserData();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
margin-top: 2vh;
|
||||
}
|
||||
</style>
|
99
demoBackend/src/demo-website/src/main.js
Normal file
99
demoBackend/src/demo-website/src/main.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import {createApp} from 'vue';
|
||||
import App from './App.vue';
|
||||
import {createStore} from 'vuex';
|
||||
import BootstrapVue3 from 'bootstrap-vue-3';
|
||||
import createPersistedState from "vuex-persistedstate";
|
||||
import { VueCookieNext } from 'vue-cookie-next'
|
||||
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue-3/dist/bootstrap-vue-3.css'
|
||||
import router from "./router/index";
|
||||
|
||||
// Create a new store instance.
|
||||
const store = createStore({
|
||||
state() {
|
||||
return {
|
||||
authenticated: false,
|
||||
jSessionId: null,
|
||||
isAndroid: false,
|
||||
userName: null,
|
||||
userIdCode: null,
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setLoggedIn(state, isLoggedIn) {
|
||||
state.authenticated = isLoggedIn;
|
||||
},
|
||||
setSessionId(state, sessionId) {
|
||||
state.jSessionId = sessionId;
|
||||
},
|
||||
setIsAndroid(state, isAndroid) {
|
||||
state.isAndroid = isAndroid;
|
||||
},
|
||||
setUserName(state, userName) {
|
||||
state.userName = userName;
|
||||
},
|
||||
setIdCode(state, idCode) {
|
||||
state.userIdCode = idCode;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
fetchSessionId(context, sessionId) {
|
||||
context.commit("setSessionId", sessionId);
|
||||
},
|
||||
setLoggedIn(context, isLoggedIn) {
|
||||
context.commit("setLoggedIn", isLoggedIn);
|
||||
},
|
||||
setIsAndroid(context, isAndroid) {
|
||||
context.commit("setIsAndroid", isAndroid);
|
||||
},
|
||||
setUserName(context, userName) {
|
||||
context.commit("setUserName", userName);
|
||||
},
|
||||
setUserIdCode(context, userIdCode) {
|
||||
context.commit("setIdCode", userIdCode);
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getAuthenticated: state => {
|
||||
return state.authenticated;
|
||||
},
|
||||
getSessionId: state => {
|
||||
return state.jSessionId;
|
||||
},
|
||||
getIsAndroid: state => {
|
||||
return state.isAndroid;
|
||||
},
|
||||
getUserName: state => {
|
||||
return state.userName;
|
||||
},
|
||||
getUserIdCode: state => {
|
||||
return state.userIdCode;
|
||||
},
|
||||
},
|
||||
plugins: [createPersistedState()],
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
// this route requires auth, check if logged in
|
||||
// if not, redirect to login page.
|
||||
if (!store.state.authenticated) {
|
||||
next({name: 'Login'})
|
||||
} else {
|
||||
next() // go to wherever I'm going
|
||||
}
|
||||
} else {
|
||||
next() // does not require auth, make sure to always call next()!
|
||||
}
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(BootstrapVue3)
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(VueCookieNext);
|
||||
app.mount('#app')
|
||||
|
||||
VueCookieNext.config({ expire: '7d' })
|
31
demoBackend/src/demo-website/src/router/index.js
Normal file
31
demoBackend/src/demo-website/src/router/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Login from '@/views/Login.vue'
|
||||
import Welcome from "@/views/Welcome";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: {
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/welcome',
|
||||
name: 'Welcome',
|
||||
component: Welcome,
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
|
||||
export default router
|
22
demoBackend/src/demo-website/src/views/Login.vue
Normal file
22
demoBackend/src/demo-website/src/views/Login.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<Navbar/>
|
||||
<LoginComponent/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoginComponent from '@/components/Login'
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
components: {
|
||||
LoginComponent,
|
||||
Navbar
|
||||
},
|
||||
methods: {
|
||||
csrf_token: function () {
|
||||
return "csrf-token";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
22
demoBackend/src/demo-website/src/views/Welcome.vue
Normal file
22
demoBackend/src/demo-website/src/views/Welcome.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<Navbar/>
|
||||
<WelcomeComponent/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelcomeComponent from '@/components/Welcome'
|
||||
import Navbar from "@/components/Navbar";
|
||||
|
||||
export default {
|
||||
name: 'Welcome',
|
||||
components: {
|
||||
WelcomeComponent,
|
||||
Navbar
|
||||
},
|
||||
methods: {
|
||||
csrf_token: function () {
|
||||
return "csrf-token";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
20
demoBackend/src/demo-website/vue.config.js
Normal file
20
demoBackend/src/demo-website/vue.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// vue.config.js
|
||||
module.exports = {
|
||||
chainWebpack: config => {
|
||||
config.module.rule('vue').uses.delete('cache-loader');
|
||||
config.module.rule('js').uses.delete('cache-loader');
|
||||
config.module.rule('ts').uses.delete('cache-loader');
|
||||
config.module.rule('tsx').uses.delete('cache-loader');
|
||||
},
|
||||
// https.//cli.vuejs.org/config/#devserver-proxy
|
||||
devServer: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/auth": {
|
||||
target: "http://localhost:8080",
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
@@ -3,14 +3,13 @@ package com.tarkvaratehnika.demobackend.config
|
||||
class ApplicationConfiguration {
|
||||
|
||||
companion object {
|
||||
// URL for intent, do not edit.
|
||||
val AUTH_APP_LAUNCH_INTENT = "authapp://start/"
|
||||
// Endpoint for challenge.
|
||||
val CHALLENGE_ENDPOINT_URL = "/auth/challenge"
|
||||
// Endpoint for authentication
|
||||
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
||||
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
||||
val WEBSITE_ORIGIN_URL = "https://5d0c-85-253-195-195.ngrok.io"
|
||||
val WEBSITE_ORIGIN_URL = "https://6fa5-145-14-34-146.ngrok.io"
|
||||
|
||||
// Authentication request timeout in seconds.
|
||||
val AUTH_REQUEST_TIMEOUT_MS = 120000
|
||||
|
||||
val USER_ROLE = "USER"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.tarkvaratehnika.demobackend.config
|
||||
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
|
||||
@EnableWebSecurity
|
||||
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
|
||||
|
||||
override fun configure(auth: AuthenticationManagerBuilder?) {
|
||||
auth?.inMemoryAuthentication()?.withUser("justSomeUser")?.password("someBackdoorPasswordThisDoesntMatterItsADemo")
|
||||
?.roles("USER")
|
||||
}
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http.authorizeRequests()
|
||||
?.antMatchers("/welcome")?.hasRole("USER")
|
||||
?.and()
|
||||
?.sessionManagement()?.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
|
||||
?.and()?.csrf()?.disable()
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
package com.tarkvaratehnika.demobackend.config
|
||||
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails
|
||||
|
||||
@Configuration
|
||||
class SessionManager {
|
||||
|
||||
companion object {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(SessionManager::class.java)
|
||||
|
||||
private val sessionRegistry = HashMap<String, AuthDto>()
|
||||
|
||||
fun registerSession(sessionId: String) {
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
LOG.debug("Session already exists.")
|
||||
} else {
|
||||
sessionRegistry[sessionId] = AuthDto(arrayListOf(), hashMapOf(), 200)
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoleToSession(sessionId: String, role: GrantedAuthority): AuthDto {
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
val session = sessionRegistry[sessionId]
|
||||
session!!.roles.add(role)
|
||||
return session
|
||||
} else {
|
||||
throw Exception("Session with sessionId: $sessionId does not exist.")
|
||||
}
|
||||
}
|
||||
|
||||
fun addErrorToSession(sessionId: String?, authDto: AuthDto) {
|
||||
// Errors are only sent by authentication app, so we can ignore sessionId being null.
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
sessionRegistry[sessionId]!!.errorCode = authDto.errorCode
|
||||
}
|
||||
}
|
||||
|
||||
fun getError(sessionId: String) : Int {
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
if (sessionRegistry[sessionId]!!.errorCode != 200) {
|
||||
return sessionRegistry[sessionId]!!.errorCode
|
||||
}
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
/**
|
||||
* Function adds role and userdata specified in authDto to the current session.
|
||||
*/
|
||||
fun addRoleToCurrentSession(authDto: AuthDto) {
|
||||
val securityContext = SecurityContextHolder.getContext()
|
||||
var sessionId = getSessionId()
|
||||
if (sessionId == null) {
|
||||
// No sessionId attached to the session, get one from credentials.
|
||||
sessionId = securityContext.authentication.credentials.toString()
|
||||
}
|
||||
val authentication = UsernamePasswordAuthenticationToken(authDto.userData, sessionId, authDto.roles)
|
||||
securityContext.authentication = authentication
|
||||
}
|
||||
|
||||
fun removeRoleFromCurrentSession(headers: Map<String, String>) {
|
||||
val securityContext = SecurityContextHolder.getContext()
|
||||
var sessionId = securityContext.authentication.credentials
|
||||
if (sessionId == null || sessionId == "") {
|
||||
// Fallback to when for some reason session object doesn't have sessionId attached.
|
||||
sessionId = getSessionId(headers)
|
||||
}
|
||||
sessionRegistry[sessionId]?.roles = arrayListOf()
|
||||
val authentication = UsernamePasswordAuthenticationToken(null, sessionId, listOf())
|
||||
securityContext.authentication = authentication
|
||||
}
|
||||
|
||||
fun addUserDataToSession(sessionId: String, name: String, idCode: String): AuthDto {
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
val session = sessionRegistry[sessionId]
|
||||
session!!.userData["name"] = name
|
||||
session.userData["idCode"] = idCode
|
||||
return session
|
||||
} else {
|
||||
throw Exception("Session with sessionId: $sessionId does not exist.")
|
||||
}
|
||||
}
|
||||
|
||||
fun getSessionHasRole(sessionId: String, role: String): Boolean {
|
||||
if (sessionRegistry.containsKey(sessionId)) {
|
||||
if (sessionRegistry[sessionId]!!.roles.contains(SimpleGrantedAuthority(role))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getSessionAuth(sessionId: String?): AuthDto? {
|
||||
if (sessionId == null) {
|
||||
return null
|
||||
}
|
||||
return sessionRegistry[sessionId]
|
||||
}
|
||||
|
||||
fun getSessionId(headers: Map<String, String>): String? {
|
||||
return headers["sessionid"]
|
||||
}
|
||||
|
||||
fun getSessionId(): String? {
|
||||
val context = SecurityContextHolder.getContext()
|
||||
if (context.authentication != null && context.authentication.details != null) {
|
||||
return (context.authentication.details as WebAuthenticationDetails).sessionId
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -3,9 +3,12 @@ package com.tarkvaratehnika.demobackend.config
|
||||
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||
import org.springframework.security.core.session.SessionRegistry
|
||||
import org.springframework.security.core.session.SessionRegistryImpl
|
||||
import org.webeid.security.exceptions.JceException
|
||||
import org.webeid.security.nonce.NonceGenerator
|
||||
import org.webeid.security.nonce.NonceGeneratorBuilder
|
||||
@@ -25,12 +28,11 @@ import javax.cache.Cache
|
||||
import javax.cache.CacheManager
|
||||
import javax.cache.Caching
|
||||
import javax.cache.configuration.CompleteConfiguration
|
||||
import javax.cache.configuration.FactoryBuilder
|
||||
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||
import javax.cache.configuration.MutableConfiguration
|
||||
import javax.cache.expiry.CreatedExpiryPolicy
|
||||
import javax.cache.expiry.Duration
|
||||
|
||||
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||
|
||||
@Configuration
|
||||
class ValidationConfiguration {
|
||||
@@ -39,12 +41,11 @@ class ValidationConfiguration {
|
||||
|
||||
private val NONCE_TTL_MINUTES: Long = 5
|
||||
private val CACHE_NAME = "nonceCache"
|
||||
private val CERTS_RESOURCE_PATH = "/certs/"
|
||||
private val CERTS_RESOURCE_PATH = "/certs"
|
||||
private val TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
|
||||
private val TRUSTSTORE_PASSWORD = "changeit"
|
||||
companion object {
|
||||
const val ROLE_USER : String = "ROLE_USER"
|
||||
}
|
||||
|
||||
|
||||
|
||||
init {
|
||||
LOG.warn("Creating new ValidationConfiguration.")
|
||||
@@ -55,6 +56,8 @@ class ValidationConfiguration {
|
||||
return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
fun nonceCache(): Cache<String, ZonedDateTime>? {
|
||||
val cacheManager: CacheManager = cacheManager()
|
||||
@@ -65,6 +68,7 @@ class ValidationConfiguration {
|
||||
LOG.warn("Creating new cache.")
|
||||
cache = createNonceCache(cacheManager)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,5 @@
|
||||
package com.tarkvaratehnika.demobackend.dto
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
data class AuthDto(var roles: ArrayList<GrantedAuthority>, var userData: HashMap<String, String>, var errorCode: Int)
|
@@ -0,0 +1,5 @@
|
||||
package com.tarkvaratehnika.demobackend.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class AuthTokenDTO (@JsonProperty("auth-token") val token : String, val error : Int?)
|
@@ -1,6 +0,0 @@
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class AuthTokenDTO (val token : String, val challenge : String) {
|
||||
}
|
@@ -23,14 +23,12 @@
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||
import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.authentication.AuthenticationServiceException
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import org.springframework.stereotype.Component
|
||||
import org.webeid.security.exceptions.TokenValidationException
|
||||
@@ -44,23 +42,15 @@ object AuthTokenDTOAuthenticationProvider {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java)
|
||||
|
||||
|
||||
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||
|
||||
|
||||
val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator()
|
||||
|
||||
@Throws(AuthenticationException::class)
|
||||
fun authenticate(auth : Authentication) : Authentication {
|
||||
fun authenticate(auth : Authentication, sessionId: String?) : AuthDto {
|
||||
val authentication = auth as PreAuthenticatedAuthenticationToken
|
||||
val token = (authentication.credentials as AuthTokenDTO).token
|
||||
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
||||
val authorities = arrayListOf<GrantedAuthority>()
|
||||
authorities.add(USER_ROLE)
|
||||
|
||||
try {
|
||||
val userCertificate: X509Certificate = tokenValidator.validate(token)
|
||||
return WebEidAuthentication.fromCertificate(userCertificate, authorities, challenge)
|
||||
return WebEidAuthentication.fromCertificate(userCertificate, sessionId)
|
||||
} catch (e : TokenValidationException) {
|
||||
// Validation failed.
|
||||
throw AuthenticationServiceException("Token validation failed. " + e.message)
|
||||
|
@@ -22,17 +22,29 @@
|
||||
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
|
||||
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration.Companion.USER_ROLE
|
||||
import com.tarkvaratehnika.demobackend.config.SessionManager
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.webeid.security.certificate.CertificateData
|
||||
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.context.SecurityContext
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.io.Serializable
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.log
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
class WebEidAuthentication(
|
||||
private val principalName: String,
|
||||
@@ -40,55 +52,74 @@ class WebEidAuthentication(
|
||||
private val authorities: ArrayList<GrantedAuthority>
|
||||
) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication {
|
||||
|
||||
|
||||
// Companion object is for static functions.
|
||||
companion object {
|
||||
|
||||
private val loggedInUsers = HashMap<String, Authentication>()
|
||||
private val LOG = LoggerFactory.getLogger(WebEidAuthentication::class.java)
|
||||
|
||||
fun fromCertificate(
|
||||
userCertificate: X509Certificate,
|
||||
authorities: ArrayList<GrantedAuthority>,
|
||||
challenge: String
|
||||
): Authentication {
|
||||
val principalName = getPrincipalNameFromCertificate(userCertificate)
|
||||
sessionId: String?,
|
||||
): AuthDto {
|
||||
// Get user data.
|
||||
val name = getPrincipalNameFromCertificate(userCertificate)
|
||||
val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate))
|
||||
val authentication = WebEidAuthentication(principalName, idCode, authorities)
|
||||
loggedInUsers[challenge] = authentication
|
||||
return authentication
|
||||
|
||||
// Fetch valid sessionId.
|
||||
var methodIndependentSessionId = sessionId
|
||||
if (methodIndependentSessionId == null) {
|
||||
methodIndependentSessionId = SessionManager.getSessionId()
|
||||
if (methodIndependentSessionId == null) {
|
||||
throw Exception("No session")
|
||||
}
|
||||
}
|
||||
|
||||
// Add role and user data to the AuthDto and return it.
|
||||
SessionManager.addRoleToSession(methodIndependentSessionId, SimpleGrantedAuthority(USER_ROLE))
|
||||
return SessionManager.addUserDataToSession(methodIndependentSessionId, name, idCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for getting a Spring authentication object by supplying a challenge.
|
||||
* TODO: Figure out a more secure solution in the future.
|
||||
* Function for getting a Spring authentication object for this session.
|
||||
*/
|
||||
fun fromChallenge(challenge: String): Authentication? {
|
||||
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
|
||||
// return null
|
||||
// }
|
||||
val auth = loggedInUsers[challenge]
|
||||
if (auth != null) {
|
||||
// If challenge is valid, delete the authentication object from the map (so this can only be fetched once).
|
||||
loggedInUsers.remove(challenge)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return auth
|
||||
}
|
||||
fun fromSession(headers: HashMap<String, String>): ResponseEntity<String> {
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
// // TODO: DELETE
|
||||
//
|
||||
// const val ROLE_USER: String = "ROLE_USER"
|
||||
// private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||
//
|
||||
// fun addAuth(challenge: String) {
|
||||
// val authorities = arrayListOf<GrantedAuthority>()
|
||||
// authorities.add(USER_ROLE)
|
||||
// val auth = WebEidAuthentication("Somename", "11111111111", authorities)
|
||||
// loggedInUsers[challenge] = auth
|
||||
// }
|
||||
//
|
||||
//
|
||||
// // TODO: DELETE UNTIL
|
||||
val currentTime = Date()
|
||||
|
||||
// Get sessionId for current session.
|
||||
var sessionId = SessionManager.getSessionId()
|
||||
|
||||
if (sessionId == null) {
|
||||
sessionId = SessionManager.getSessionId(headers)
|
||||
if (sessionId == null) {
|
||||
return ResponseEntity.status(400).body(mapper.writeValueAsString(400))
|
||||
}
|
||||
}
|
||||
|
||||
while (currentTime.time + ApplicationConfiguration.AUTH_REQUEST_TIMEOUT_MS > Date().time) {
|
||||
Thread.sleep(1000)
|
||||
|
||||
// Check if an error has been submitted for this session.
|
||||
val error = SessionManager.getError(sessionId)
|
||||
if (error != 200) {
|
||||
return ResponseEntity.status(error).body(mapper.writeValueAsString(error))
|
||||
}
|
||||
|
||||
// Check if this session has received a role.
|
||||
if (SessionManager.getSessionHasRole(sessionId, USER_ROLE)) {
|
||||
// Get AuthDto
|
||||
val auth = SessionManager.getSessionAuth(sessionId)
|
||||
|
||||
// Set role and user data to current session.
|
||||
SessionManager.addRoleToCurrentSession(auth!!)
|
||||
return ResponseEntity.status(200).body(mapper.writeValueAsString(auth))
|
||||
}
|
||||
}
|
||||
|
||||
// In case of timeout return 408.
|
||||
return ResponseEntity.status(408).body(mapper.writeValueAsString(408))
|
||||
}
|
||||
|
||||
private fun getPrincipalNameFromCertificate(userCertificate: X509Certificate): String {
|
||||
return Objects.requireNonNull(CertificateData.getSubjectGivenName(userCertificate)) + " " +
|
||||
|
@@ -1,20 +0,0 @@
|
||||
package com.tarkvaratehnika.demobackend.web
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
@Controller
|
||||
class LoginController {
|
||||
|
||||
@GetMapping
|
||||
fun login(model : Model) : String {
|
||||
model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
|
||||
model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
|
||||
model.addAttribute("originUrl", ApplicationConfiguration.WEBSITE_ORIGIN_URL)
|
||||
model.addAttribute("loggedInUrl", "/signature")
|
||||
model.addAttribute("authenticationRequestUrl", ApplicationConfiguration.AUTHENTICATION_ENDPOINT_URL)
|
||||
return "index"
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
package com.tarkvaratehnika.demobackend.web
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
@Controller
|
||||
class SignatureController {
|
||||
|
||||
|
||||
@PreAuthorize("hasAuthority('$ROLE_USER')")
|
||||
@GetMapping("signature")
|
||||
fun signature(model : Model) : String {
|
||||
// model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
|
||||
// model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
|
||||
return "signature"
|
||||
}
|
||||
}
|
@@ -1,15 +1,17 @@
|
||||
package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTO
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.tarkvaratehnika.demobackend.config.SessionManager
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider
|
||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("auth")
|
||||
@@ -18,23 +20,39 @@ class AuthenticationController {
|
||||
private val LOG = LoggerFactory.getLogger(AuthenticationController::class.java)
|
||||
|
||||
|
||||
@PostMapping("authentication", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun authenticate(@RequestBody body : String): Authentication {
|
||||
val parts = body.split("\"")
|
||||
val authToken = AuthTokenDTO(parts[3], parts[7])
|
||||
@PostMapping("login", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun authenticate(@RequestHeader headers: Map<String, String>, @RequestBody authTokenDTO: AuthTokenDTO): AuthDto {
|
||||
val sessionId = SessionManager.getSessionId(headers)
|
||||
|
||||
// Check if an error occurred in the auth app.
|
||||
if (authTokenDTO.error != null && authTokenDTO.error != 200) {
|
||||
val auth = AuthDto(arrayListOf(), hashMapOf(), authTokenDTO.error)
|
||||
SessionManager.addErrorToSession(sessionId, auth)
|
||||
return auth
|
||||
}
|
||||
|
||||
// Create Spring Security Authentication object with supplied token as credentials.
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, authTokenDTO)
|
||||
|
||||
// Return authentication object if success.
|
||||
return AuthTokenDTOAuthenticationProvider.authenticate(auth)
|
||||
return AuthTokenDTOAuthenticationProvider.authenticate(auth, sessionId)
|
||||
}
|
||||
|
||||
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getAuthenticated(@RequestParam challenge: String) : Authentication? {
|
||||
val auth = WebEidAuthentication.fromChallenge(challenge)
|
||||
if (auth == null) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Not allowed.")
|
||||
}
|
||||
return auth
|
||||
|
||||
@GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : ResponseEntity<String> {
|
||||
return WebEidAuthentication.fromSession(headers)
|
||||
}
|
||||
|
||||
@GetMapping("userData", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getUserData(@RequestHeader headers: Map<String, String>) : AuthDto? {
|
||||
return SessionManager.getSessionAuth(SessionManager.getSessionId(headers))
|
||||
}
|
||||
|
||||
@PostMapping("logout")
|
||||
fun logOut(@RequestHeader headers: Map<String, String>) : HttpStatus? {
|
||||
SessionManager.removeRoleFromCurrentSession(headers)
|
||||
return HttpStatus.ACCEPTED
|
||||
|
||||
}
|
||||
}
|
@@ -22,14 +22,24 @@
|
||||
|
||||
package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.SessionManager
|
||||
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import org.webeid.security.nonce.NonceGenerator
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("auth")
|
||||
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||
@@ -37,10 +47,30 @@ class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
|
||||
|
||||
@GetMapping("challenge")
|
||||
fun challenge(): ChallengeDto {
|
||||
fun challenge(@RequestHeader headers: Map<String, String>): ChallengeDto {
|
||||
|
||||
val sessionId = SessionManager.getSessionId(headers)
|
||||
|
||||
if (sessionId == null) {
|
||||
LOG.warn("SESSION ID MISSING FOR CHALLENGE")
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "SessionId missing.")
|
||||
}
|
||||
|
||||
SessionManager.registerSession(sessionId)
|
||||
|
||||
// val context = SecurityContextHolder.getContext()
|
||||
// val authorities = arrayListOf<GrantedAuthority>()
|
||||
// authorities.add(SimpleGrantedAuthority("USER"))
|
||||
// authorities.add(SimpleGrantedAuthority("ROLE_USER"))
|
||||
// val auth = context.authentication
|
||||
//
|
||||
// val newAuth: Authentication =
|
||||
// UsernamePasswordAuthenticationToken(auth.principal, auth.credentials, authorities)
|
||||
// SecurityContextHolder.getContext().authentication = newAuth;
|
||||
|
||||
// SessionManager.createSession(SessionManager.getSessionId(headers))
|
||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||
LOG.warn(challengeDto.nonce)
|
||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||
return challengeDto
|
||||
}
|
||||
|
||||
|
@@ -1 +1,2 @@
|
||||
server.servlet.session.cookie.http-only=false
|
||||
|
||||
|
@@ -1,29 +0,0 @@
|
||||
html {
|
||||
font-size: 2vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.cont {
|
||||
display: grid;
|
||||
width: 80%;
|
||||
padding-top: 10%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 10%;
|
||||
}
|
||||
|
||||
#loginButton {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.cont > * {
|
||||
margin: 1rem;
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
window.onload = () => {
|
||||
// Add event listener for login button.
|
||||
let loginButton = document.getElementById("loginButton");
|
||||
|
||||
if (loginButton != null) {
|
||||
loginButton.addEventListener("click", () => {
|
||||
let action = loginButton.getAttribute("data-action");
|
||||
loginButton.setAttribute("disabled", "true");
|
||||
loginButton.textContent = "Logging in";
|
||||
launchAuthApp(action);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -1,71 +0,0 @@
|
||||
const POLLING_INTERVAL = 1000;
|
||||
const POLLING_RETRIES = 120;
|
||||
|
||||
function launchAuthApp(action) {
|
||||
if (!isAndroid()) {
|
||||
alert("Functionality only available for Android devices.")
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch challenge.
|
||||
httpGetAsync(originUrl + challengeUrl, (body) => {
|
||||
let data = JSON.parse(body);
|
||||
let challenge = data.nonce;
|
||||
let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling.
|
||||
console.log(intent);
|
||||
window.location.href = intent;
|
||||
pollForAuth(POLLING_INTERVAL, challenge);
|
||||
})
|
||||
}
|
||||
|
||||
function pollForAuth(timeout, challenge) {
|
||||
console.log("Polling for auth");
|
||||
let encodedChallenge = encodeURIComponent(challenge);
|
||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + encodedChallenge;
|
||||
let counter = 0;
|
||||
let timer = setInterval(() => {
|
||||
// Fetch authentication object.
|
||||
httpGetAsync(requestUrl, (body) => {
|
||||
console.log(body);
|
||||
// If this is a successful request, stop the polling.
|
||||
clearInterval(timer);
|
||||
window.location.href = originUrl + loggedInUrl;
|
||||
});
|
||||
counter++;
|
||||
if (counter > POLLING_RETRIES) {
|
||||
clearInterval(timer); // Stop polling after some time.
|
||||
let loginErrorAlert = document.getElementById("loginErrorAlert");
|
||||
loginErrorAlert.classList.remove("d-none")
|
||||
}
|
||||
}, timeout)
|
||||
|
||||
}
|
||||
|
||||
function createParametrizedIntentUrl(challenge, action) {
|
||||
if (action == null) {
|
||||
console.error("There has to be an action for intent.")
|
||||
}
|
||||
else if (challenge == null) {
|
||||
console.error("Challenge missing, can't authenticate without it.")
|
||||
} else {
|
||||
return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function isAndroid() {
|
||||
// Check if using Android device.
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return ua.indexOf("android") > -1;
|
||||
}
|
||||
|
||||
function httpGetAsync(theUrl, callback) {
|
||||
console.log("Sending a request.")
|
||||
const xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
|
||||
callback(xmlHttp.responseText);
|
||||
}
|
||||
}
|
||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||
xmlHttp.send(null);
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link th:href="@{/css/main.css}" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" th:src="@{/js/index.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
||||
<script th:inline="javascript">const originUrl = [[${originUrl}]];
|
||||
const intentUrl = [[${intentUrl}]];
|
||||
const challengeUrl = [[${challengeUrl}]];
|
||||
const loggedInUrl = [[${loggedInUrl}]];
|
||||
const authenticationRequestUrl = [[${authenticationRequestUrl}]]</script> <!-- Pass some values to JS -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Auth demo webapp</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="cont">
|
||||
<h4>Welcome to Estonian ID card mobile authentication demo website. When using a mobile phone, you can log in to the
|
||||
website using your ID card by using the button below.</h4>
|
||||
<h5>Make sure you've installed the authentication app from: <a
|
||||
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
||||
<button type="button" class="btn btn-lg btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
||||
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
||||
Login failed. Refresh the page to try again.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link th:href="@{/css/main.css}" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" th:src="@{/js/signature.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="btn btn-danger">Log out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="cont">
|
||||
<h4>Congratulations! You have just authenticated yourself using your mobile phone and your ID-card. You can try to
|
||||
give a signature to a file now.</h4>
|
||||
<h5>This page is still WIP, signing a document feature will be implemented later.</h5>
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" id="customFile">
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user