26 Commits

Author SHA1 Message Date
TanelOrumaa
d6e4b61ff6 Configured URL-s to heaid.ee 2022-01-18 01:09:06 +02:00
Henrik Lepson
b95115af4f added comment 2022-01-17 17:16:56 +02:00
Henrik Lepson
866c3c24a7 deleted unused code 2022-01-17 17:15:45 +02:00
Henrik Lepson
32336ffb2b small changes to the return data to the test mobile app 2022-01-17 17:13:40 +02:00
Henrik Lepson
b889b9cda7 fixed testapp compatibility issue 2021-12-14 22:46:35 +02:00
TanelOrumaa
e5931692b6 MOB-55 Fixed an issue with session invalidation. 2021-12-14 19:50:11 +02:00
TanelOrumaa
b66c2386f0 MOB-55 Small addition to readme 2021-12-12 20:25:24 +02:00
TanelOrumaa
04933f2705 web-eid.js is now fetched from git, fixed a small problem related to authentication 2021-12-12 20:22:27 +02:00
TanelOrumaa
8b78ddf51a MOB-55 Fixed some issues with session management. 2021-12-12 20:06:08 +02:00
TanelOrumaa
13a0a9430f MOB-40 Changed header type from string to map and now will be used in requests 2021-12-12 20:05:18 +02:00
TanelOrumaa
d92656d982 MOB-55 Fixed bug with mobile app 2021-12-07 00:01:20 +02:00
TanelOrumaa
0da3e17b28 MOB-55 Fixed a bug with url 2021-12-07 00:00:51 +02:00
TanelOrumaa
2b660eeda0 Merge branch 'compatibility' into MOB-55 2021-12-06 23:39:35 +02:00
TanelOrumaa
5719712bef MOB-55 Disabled CSRF 2021-12-06 23:39:13 +02:00
Henrik Lepson
1e26f83db2 tweaked url 2021-12-06 23:27:43 +02:00
TanelOrumaa
44430bfab2 Merge branch 'compatibility' into MOB-55 2021-12-06 23:03:56 +02:00
TanelOrumaa
7482c88a4e MOB-55 Added session cookies reading 2021-12-06 23:01:10 +02:00
Henrik Lepson
74d97827f8 testing new approach 2021-12-06 22:08:04 +02:00
TanelOrumaa
4096201bef Merge branch 'main' into MOB-55 2021-12-06 21:09:07 +02:00
TanelOrumaa
da2dbeb0fc MOB-42 Redid the whole frontend part in Vue 2021-12-06 21:08:15 +02:00
Henrik Lepson
60207319b7 Merge pull request #16 from TanelOrumaa/newapproach
Improved code
2021-12-06 20:27:00 +02:00
TanelOrumaa
7daea4b6c2 MOB-55 Added vue frontend 2021-12-06 18:14:01 +02:00
Henrik Lepson
e5300dfa5e got rid of git status syntax 2021-12-04 17:21:07 +02:00
Henrik Lepson
d4c2a11521 added more error messages 2021-12-04 17:08:58 +02:00
Henrik Lepson
09c4fa6be3 fixed small issue in testmobileapp 2021-12-04 12:46:40 +02:00
Henrik Lepson
63bc89b0e4 Merge pull request #14 from TanelOrumaa/iter4UI
UI/UX improvements for iteration 4
2021-12-03 16:24:00 +02:00
60 changed files with 37828 additions and 495 deletions

View File

@@ -36,7 +36,8 @@ class AuthFragment : Fragment() {
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()
@@ -49,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?) {
@@ -59,9 +60,9 @@ 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)
}
}
@@ -71,9 +72,9 @@ class AuthFragment : Fragment() {
}
}.start()
// 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() }
binding.nextButton.visibility = View.GONE
binding.nextButton.setOnClickListener { goToNextFragment() }
binding.cancelButton.setOnClickListener { cancelAuth() }
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter != null)
getInfoFromIdCard(adapter)
@@ -104,7 +105,7 @@ class AuthFragment : Fragment() {
adapter.enableReaderMode(activity, { tag ->
timer.cancel()
requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.card_detected)
binding.timeCounter.text = getString(R.string.card_detected)
}
val card = IsoDep.get(tag)
card.timeout = 32768
@@ -127,11 +128,11 @@ class AuthFragment : Fragment() {
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])
binding.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1])
viewModel.deletePin(requireContext())
}
else -> requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.wrong_can_text)
binding.timeCounter.text = getString(R.string.wrong_can_text)
viewModel.deleteCan(requireContext())
}
}
@@ -149,6 +150,6 @@ class AuthFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -29,7 +29,8 @@ class CanFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentCanBinding? = null
private var _binding: FragmentCanBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that we are navigating here from the settings menu and must return to the settings menu.
@@ -40,17 +41,17 @@ class CanFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCanBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentCanBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
checkIfSkip()
binding!!.canTextField.editText?.addTextChangedListener {
binding.canTextField.editText?.addTextChangedListener {
checkEnteredCan()
}
binding!!.buttonCancel.setOnClickListener { goToTheStart() }
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
/**
@@ -112,7 +113,7 @@ class CanFragment : Fragment() {
* allowed to modify the entered can.
*/
private fun checkEnteredCan() {
val enteredCan = binding!!.canTextField.editText?.text.toString()
val enteredCan = binding.canTextField.editText?.text.toString()
if (enteredCan.length == 6) {
viewModel.setUserCan(enteredCan)
viewModel.storeCan(requireContext()) //Maybe storeCan should always automatically call setUserCan method as well because these methods usually are used together
@@ -127,6 +128,6 @@ class CanFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -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
@@ -39,7 +41,8 @@ class HomeFragment : Fragment() {
private val intentParams: ParametersViewModel by activityViewModels()
private var binding: FragmentHomeBinding? = null
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
// The ID card reader mode is enabled on the home fragment when can is saved.
private var canSaved: Boolean = false
@@ -54,10 +57,10 @@ class HomeFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
// Making settings menu active again
(activity as MainActivity).menuAvailable = true
return binding!!.root
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -87,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)
}
@@ -100,26 +104,97 @@ 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()
}
}
}
} 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
// before getting an inevitable error.
val message = MaterialAlertDialogBuilder(requireContext())
message.setTitle(getString(R.string.problem_parameters))
if (intentParams.challenge == "") {
message.setMessage(getString(R.string.problem_challenge))
} else if (intentParams.authUrl == "") {
message.setMessage(getString(R.string.problem_authurl))
} else if (intentParams.origin == "") {
message.setMessage(getString(R.string.problem_originurl))
} else {
message.setMessage(getString(R.string.problem_other))
}
message.setPositiveButton(getString(R.string.continue_button)) { _, _ ->
val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
}
goToTheNextFragment(mobile)
message.show()
}
}
/**
@@ -127,12 +202,12 @@ class HomeFragment : Fragment() {
*/
private fun canState() {
if (viewModel.userCan.length == 6) {
binding!!.canStatusText.text = getString(R.string.can_status_saved)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
binding.canStatusText.text = getString(R.string.can_status_saved)
binding.canStatusLogo.setImageResource(R.drawable.ic_check_logo)
canSaved = true
} else {
binding!!.canStatusText.text = getString(R.string.can_status_negative)
binding!!.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
binding.canStatusText.text = getString(R.string.can_status_negative)
binding.canStatusLogo.setImageResource(R.drawable.ic_info_logo)
canSaved = false
}
}
@@ -142,14 +217,25 @@ class HomeFragment : Fragment() {
*/
private fun pinState() {
if (viewModel.userPin.length in 4..12) {
binding!!.pinStatusText.text = getString(R.string.pin_status_saved)
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
binding.pinStatusText.text = getString(R.string.pin_status_saved)
binding.pinStatusLogo.setImageResource(R.drawable.ic_check_logo)
} else {
binding!!.pinStatusText.text = getString(R.string.pin_status_negative)
binding!!.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
binding.pinStatusText.text = getString(R.string.pin_status_negative)
binding.pinStatusLogo.setImageResource(R.drawable.ic_info_logo)
}
}
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
@@ -172,10 +258,10 @@ class HomeFragment : Fragment() {
/**
* Displays a help message to the user explaining what the CAN is
*/
private fun displayMessage() {
private fun displayMessage(title: String, message: String) {
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.can_question))
.setMessage(getString(R.string.can_explanation))
.setTitle(title)
.setMessage(message)
.setPositiveButton(R.string.return_text) { _, _ -> }
.show()
val title = dialog.findViewById<TextView>(R.id.alertTitle)
@@ -188,22 +274,28 @@ class HomeFragment : Fragment() {
*/
private fun updateAction(canIsSaved: Boolean) {
if (canIsSaved) {
binding!!.detectionActionText.text = getString(R.string.action_detect)
binding.detectionActionText.text = getString(R.string.action_detect)
enableReaderMode()
binding!!.homeActionButton.visibility = View.GONE
binding!!.homeHelpButton.visibility = View.GONE
binding.homeActionButton.visibility = View.GONE
binding.homeHelpButton.visibility = View.GONE
} else {
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)
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
)
findNavController().navigate(action)
}
binding!!.homeHelpButton.setOnClickListener {
displayMessage()
binding.homeHelpButton.setOnClickListener {
displayMessage(
getString(R.string.can_question),
getString(R.string.can_explanation)
)
}
binding!!.homeActionButton.visibility = View.VISIBLE
binding!!.homeHelpButton.visibility = View.VISIBLE
binding.homeActionButton.visibility = View.VISIBLE
binding.homeHelpButton.visibility = View.VISIBLE
}
}
@@ -211,11 +303,11 @@ class HomeFragment : Fragment() {
* Resets the error message and allows the user to try again
*/
private fun reset() {
binding!!.homeActionButton.text = getString(R.string.try_again_text)
binding!!.homeActionButton.setOnClickListener {
binding.homeActionButton.text = getString(R.string.try_again_text)
binding.homeActionButton.setOnClickListener {
updateAction(canSaved)
}
binding!!.homeActionButton.visibility = View.VISIBLE
binding.homeActionButton.visibility = View.VISIBLE
}
/**
@@ -224,11 +316,11 @@ class HomeFragment : Fragment() {
private fun enableReaderMode() {
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter == null || !adapter.isEnabled) {
binding!!.detectionActionText.text = getString(R.string.nfc_not_available)
binding.detectionActionText.text = getString(R.string.nfc_not_available)
} else {
adapter.enableReaderMode(activity, { tag ->
requireActivity().runOnUiThread {
binding!!.detectionActionText.text = getString(R.string.card_detected)
binding.detectionActionText.text = getString(R.string.card_detected)
}
val card = IsoDep.get(tag)
card.timeout = 32768
@@ -249,11 +341,13 @@ class HomeFragment : Fragment() {
} catch (e: Exception) {
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()
@@ -269,7 +363,9 @@ class HomeFragment : Fragment() {
override fun onDestroyView() {
super.onDestroyView()
if (receiver != null) {
requireActivity().unregisterReceiver(receiver)
binding = null
}
_binding = null
}
}

View File

@@ -19,23 +19,24 @@ class Pin2Fragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentPin2Binding? = null
private var _binding: FragmentPin2Binding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPin2Binding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentPin2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.nextButton.setOnClickListener {
binding.nextButton.setOnClickListener {
checkPin2Length()
}
binding!!.cancelButton.setOnClickListener {
binding.cancelButton.setOnClickListener {
cancel()
}
}
@@ -45,7 +46,7 @@ class Pin2Fragment : Fragment() {
* then it is saved to the viewModel.
*/
private fun checkPin2Length() {
val enteredPin2 = binding!!.pin2EditText.editText?.text.toString()
val enteredPin2 = binding.pin2EditText.editText?.text.toString()
if (enteredPin2.length in 5..12) {
viewModel.setUserPin2(enteredPin2)
} else {
@@ -66,7 +67,7 @@ class Pin2Fragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -29,7 +29,8 @@ class PinFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentPinBinding? = null
private var _binding: FragmentPinBinding? = null
private val binding get() = _binding!!
// Navigation arguments:
// saving = true means that the user must be returned to the settings menu
@@ -42,8 +43,8 @@ class PinFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPinBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentPinBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -51,28 +52,27 @@ class PinFragment : Fragment() {
checkIfSkip()
// Switch should be not visible when user is in savings mode
if (args.saving) {
binding!!.savePinQuestion.visibility = View.GONE
binding!!.saveLayout.visibility = View.GONE
binding.savePinQuestion.visibility = View.GONE
binding.saveLayout.visibility = View.GONE
} else {
saveToggle =
activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true) == true //Android Studio recommendation to get rid of Boolean?.
Log.i("myLogging", activity?.getPreferences(Context.MODE_PRIVATE)?.getBoolean("saveToggle", true).toString())
if (!saveToggle) {
binding!!.saveSwitch.isChecked = false
binding.saveSwitch.isChecked = false
}
binding!!.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
binding.saveSwitch.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding!!.saveStatus.text = getString(R.string.pin_save_on)
binding.saveStatus.text = getString(R.string.pin_save_on)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", true)?.apply()
} else {
binding!!.saveStatus.text = getString(R.string.pin_save_off)
binding.saveStatus.text = getString(R.string.pin_save_off)
activity?.getPreferences(Context.MODE_PRIVATE)?.edit()?.putBoolean("saveToggle", false)?.apply()
}
saveToggle = !saveToggle
}
}
binding!!.buttonContinue.setOnClickListener { checkEnteredPin() }
binding!!.buttonCancel.setOnClickListener { goToTheStart() }
binding.buttonContinue.setOnClickListener { checkEnteredPin() }
binding.buttonCancel.setOnClickListener { goToTheStart() }
}
/**
@@ -130,7 +130,7 @@ class PinFragment : Fragment() {
* allowed to modify the entered PIN 1.
*/
private fun checkEnteredPin() {
val enteredPin = binding!!.pinTextField.editText?.text.toString()
val enteredPin = binding.pinTextField.editText?.text.toString()
if (enteredPin.length in 4..12) {
viewModel.setUserPin(enteredPin)
if (args.saving) {
@@ -152,6 +152,6 @@ class PinFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -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
@@ -24,7 +26,8 @@ class ResultFragment : Fragment() {
private val paramsModel: ParametersViewModel by activityViewModels()
private var binding: FragmentResultBinding? = null
private var _binding: FragmentResultBinding? = null
private val binding get() = _binding!!
private val args: ResultFragmentArgs by navArgs()
@@ -33,8 +36,8 @@ class ResultFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentResultBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentResultBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -44,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()
}
@@ -59,15 +70,22 @@ class ResultFragment : Fragment() {
*/
fun postToken() {
val json = JsonObject()
json.addProperty("token", paramsModel.token)
json.addProperty("challenge", paramsModel.challenge)
json.addProperty("auth-token", paramsModel.token)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
Ion.with(activity)
.load(paramsModel.origin + paramsModel.authUrl)
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)
@@ -76,7 +94,11 @@ class ResultFragment : Fragment() {
}
} else {
if (args.mobile) {
createResponse(true, result.toString(), paramsModel.token)
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()
}
@@ -86,7 +108,7 @@ class ResultFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -20,33 +20,34 @@ class UserFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentUserBinding? = null
private var _binding: FragmentUserBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentUserBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentUserBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
displayInformation()
binding!!.clearButton.setOnClickListener { goToTheStart() }
binding.clearButton.setOnClickListener { goToTheStart() }
}
/**
* Assigns text values to the fields in order to display user information.
*/
private fun displayInformation() {
binding!!.userName.text =
binding.userName.text =
getString(R.string.user_name, viewModel.userFirstName, viewModel.userLastName)
binding!!.identificationNumber.text = viewModel.userIdentificationNumber
binding!!.gender.text = viewModel.gender
binding!!.expiration.text = viewModel.expiration.replace(" ", "/")
binding!!.citizenship.text = viewModel.citizenship
binding.identificationNumber.text = viewModel.userIdentificationNumber
binding.gender.text = viewModel.gender
binding.expiration.text = viewModel.expiration.replace(" ", "/")
binding.citizenship.text = viewModel.citizenship
}
/**
@@ -59,6 +60,6 @@ class UserFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -25,7 +25,8 @@ class SettingsFragment : Fragment() {
private val viewModel: SmartCardViewModel by activityViewModels()
private var binding: FragmentSettingsBinding? = null
private var _binding: FragmentSettingsBinding? = null
private val binding get() = _binding!!
private var showPin: Boolean = false
@@ -34,8 +35,8 @@ class SettingsFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding!!.root
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -43,10 +44,10 @@ class SettingsFragment : Fragment() {
showCanField()
showPinField()
togglePinButton()
binding!!.canMenuAction.setOnClickListener { canAction() }
binding!!.pinMenuAction.setOnClickListener { pinAction() }
binding!!.pinMenuShow.setOnClickListener { togglePin() }
binding!!.returnButton.setOnClickListener { backToHome() }
binding.canMenuAction.setOnClickListener { canAction() }
binding.pinMenuAction.setOnClickListener { pinAction() }
binding.pinMenuShow.setOnClickListener { togglePin() }
binding.returnButton.setOnClickListener { backToHome() }
}
/**
@@ -64,11 +65,11 @@ class SettingsFragment : Fragment() {
*/
private fun showCanField() {
if (viewModel.userCan.length == 6) {
binding!!.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
binding!!.canMenuAction.text = getString(R.string.can_delete)
binding.canSaved.text = getString(R.string.saved_can, viewModel.userCan)
binding.canMenuAction.text = getString(R.string.can_delete)
} else {
binding!!.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
binding!!.canMenuAction.text = getString(R.string.add_can_text)
binding.canSaved.text = getString(R.string.saved_can, getString(R.string.missing))
binding.canMenuAction.text = getString(R.string.add_can_text)
}
}
@@ -95,16 +96,16 @@ class SettingsFragment : Fragment() {
*/
private fun showPinField() {
if (viewModel.userPin.length in 4..12) {
binding!!.pinMenuShow.visibility = Button.VISIBLE
binding.pinMenuShow.visibility = Button.VISIBLE
if (showPin)
binding!!.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
binding.pinSaved.text = getString(R.string.saved_pin, viewModel.userPin)
else
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
binding!!.pinMenuAction.text = getString(R.string.pin1_delete)
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.hidden_pin))
binding.pinMenuAction.text = getString(R.string.pin1_delete)
} else {
binding!!.pinMenuShow.visibility = Button.GONE
binding!!.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
binding!!.pinMenuAction.text = getString(R.string.pin1_add)
binding.pinMenuShow.visibility = Button.GONE
binding.pinSaved.text = getString(R.string.saved_pin, getString(R.string.missing))
binding.pinMenuAction.text = getString(R.string.pin1_add)
}
}
@@ -138,9 +139,9 @@ class SettingsFragment : Fragment() {
*/
private fun togglePinButton() {
if (showPin) {
binding!!.pinMenuShow.text = getString(R.string.hide)
binding.pinMenuShow.text = getString(R.string.hide)
} else {
binding!!.pinMenuShow.text = getString(R.string.show)
binding.pinMenuShow.text = getString(R.string.show)
}
}
@@ -154,7 +155,7 @@ class SettingsFragment : Fragment() {
override fun onDestroy() {
super.onDestroy()
binding = null
_binding = null
}
}

View File

@@ -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
}
}

View File

@@ -29,7 +29,11 @@
<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_view">Please enter PIN 1</string>
<string name="hint_pin">PIN 1</string>
@@ -63,7 +67,7 @@
<string name="gender_label">SEX</string>
<!-- string resources for ResultFragment layout-->
<string name="result_text">Controlling the created token</string>
<string name="result_text">Checking the created token</string>
<string name="result_info">The app will close automatically</string>
<!-- menu -->

View File

@@ -28,7 +28,11 @@
<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_view">Palun sisesta PIN 1</string>
<string name="hint_pin">PIN 1</string>

View File

@@ -27,7 +27,11 @@
<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_view">Please enter PIN 1</string>
<string name="hint_pin">PIN 1</string>

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -9,18 +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.google.gson.JsonObject
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://heaid.ee"
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() {
@@ -32,30 +33,20 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
if (response.resultCode == Activity.RESULT_OK) {
// Currently we are not actually checking whether we get a valid token.
// For testing purposes only, to make sure that we are able to get a response at all.
binding.loginTextView.text = getString(R.string.auth_success)
Log.i("getResult", response.data?.getStringExtra("token").toString())
Log.i("getResult", response.data?.getStringExtra("result").toString())
// Logs are used to show what information can be retrieved from the mobileauthapp.
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)
/*
binding.loginOptionNfcButton.text = "Log Out"
binding.loginOptionNfcButton.setOnClickListener {
binding.loginOptionNfcButton.text = "NFC auth"
binding.loginOptionNfcButton.setOnClickListener { getData() }
}
*/
}
if (response.resultCode == Activity.RESULT_CANCELED) {
binding.loginTextView.text = getString(R.string.auth_failure)
@@ -64,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
}
@@ -114,6 +85,7 @@ class MainActivity : AppCompatActivity() {
binding.resultLayout.visibility = View.VISIBLE
binding.resultObject.text = getString(R.string.hello, user)
binding.buttonForget.setOnClickListener {
binding.loginTextView.text = getString(R.string.login_text)
binding.resultObject.text = ""
binding.resultLayout.visibility = View.GONE
binding.loginOptions.visibility = View.VISIBLE

View File

@@ -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
}

View File

@@ -31,3 +31,6 @@ build/
### VS Code ###
.vscode/
### web-eid.js ###
!**src/demo-website/src/web-eid.js

View File

@@ -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...

View File

@@ -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>
@@ -115,6 +118,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
View 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?

Binary file not shown.

View 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/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

Binary file not shown.

View 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" "$@"

View 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%" %*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
{
"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": "github:TanelOrumaa/web-eid.js#main",
"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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,128 @@
<template>
<div class="container container-md d-flex flex-column">
<div>
<h3 class="text-center">Welcome to Estonian ID card mobile authentication demo website. When using an Android mobile phone, you can
log in to the
website using your ID card by using the button below.</h3>
<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>

View 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>

View 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>

View 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' })

View 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

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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
}
}
},
}

View File

@@ -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://heaid.ee"
// Authentication request timeout in seconds.
val AUTH_REQUEST_TIMEOUT_MS = 120000
val USER_ROLE = "USER"
}
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,107 @@
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) {
LOG.warn("REGISTERING SESSION $sessionId")
if (sessionRegistry.containsKey(sessionId)) {
LOG.debug("Session already exists.")
} else {
sessionRegistry[sessionId] = AuthDto(arrayListOf(), hashMapOf())
}
}
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.")
}
}
/**
* 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
}
}
}

View File

@@ -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
}

View File

@@ -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>)

View File

@@ -0,0 +1,5 @@
package com.tarkvaratehnika.demobackend.dto
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (@JsonProperty("auth-token") val token : String)

View File

@@ -1,6 +0,0 @@
package com.tarkvaratehnika.demobackend.security
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (val token : String, val challenge : String) {
}

View File

@@ -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)

View File

@@ -22,17 +22,27 @@
package com.tarkvaratehnika.demobackend.security
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.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,39 +50,71 @@ 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.
*/
fun fromChallenge(challenge: String): Authentication? {
fun fromSession(headers: HashMap<String, String>): AuthDto {
val currentTime = Date()
// Get sessionId for current session.
var sessionId = SessionManager.getSessionId()
if (sessionId == null) {
LOG.warn("SESSION IS NULL")
sessionId = SessionManager.getSessionId(headers)
if (sessionId == null) {
LOG.warn("SESSION IS STILL NULL")
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Session ID not found.")
}
LOG.warn("SESSION IS NOW: " + sessionId)
}
while (currentTime.time + ApplicationConfiguration.AUTH_REQUEST_TIMEOUT_MS > Date().time) {
Thread.sleep(1000)
if (SessionManager.getSessionHasRole(sessionId, USER_ROLE)) {
// Get AuthDto
val auth = SessionManager.getSessionAuth(sessionId)
// Set role and user data to current session.
SessionManager.addRoleToCurrentSession(auth!!)
LOG.warn("ROLE ADDED AND LOGGING IN.")
return auth
}
}
// 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
throw ResponseStatusException(HttpStatus.REQUEST_TIMEOUT, "Token not received in time.")
}
// // TODO: DELETE

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -1,15 +1,15 @@
package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.security.AuthTokenDTO
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.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
@RestController
@RequestMapping("auth")
@@ -18,23 +18,33 @@ 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 body : AuthTokenDTO): AuthDto {
val sessionId = SessionManager.getSessionId(headers)
// Create Spring Security Authentication object with supplied token as credentials.
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
val auth = PreAuthenticatedAuthenticationToken(null, body)
// 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.")
@GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : AuthDto {
return WebEidAuthentication.fromSession(headers)
}
return auth
@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
}
}

View File

@@ -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
}

View File

@@ -1 +1,2 @@
server.servlet.session.cookie.http-only=false

View File

@@ -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;
}

View File

@@ -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);
})
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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>