mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2025-08-30 07:10:59 +03:00
Compare commits
3 Commits
testappcha
...
release-0.
Author | SHA1 | Date | |
---|---|---|---|
|
c28fc2be48 | ||
|
7edd8189a4 | ||
|
b565f6846d |
@@ -4,6 +4,7 @@ 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
|
||||
@@ -13,20 +14,17 @@ 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
|
||||
@@ -90,8 +88,7 @@ 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)
|
||||
}
|
||||
|
||||
@@ -104,75 +101,18 @@ 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
|
||||
@@ -188,7 +128,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()
|
||||
@@ -225,17 +165,6 @@ 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
|
||||
@@ -262,7 +191,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
|
||||
@@ -282,17 +211,11 @@ 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
|
||||
@@ -339,15 +262,13 @@ 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()
|
||||
|
@@ -11,11 +11,9 @@ 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
|
||||
@@ -47,20 +45,12 @@ 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,
|
||||
idCode: String = "noCode",
|
||||
name: String = "noName",
|
||||
authority: String = "noAuthority"
|
||||
) {
|
||||
val responseCode =
|
||||
if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||
private fun createResponse(success: Boolean = true, result: String = "noResult", token: String = "noToken") {
|
||||
val responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
|
||||
val resultIntent = Intent()
|
||||
resultIntent.putExtra("idCode", idCode)
|
||||
resultIntent.putExtra("name", name)
|
||||
resultIntent.putExtra("authority", authority)
|
||||
resultIntent.putExtra("result", result)
|
||||
resultIntent.putExtra("token", token)
|
||||
requireActivity().setResult(responseCode, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
@@ -70,22 +60,15 @@ class ResultFragment : Fragment() {
|
||||
*/
|
||||
fun postToken() {
|
||||
val json = JsonObject()
|
||||
json.addProperty("auth-token", paramsModel.token)
|
||||
|
||||
|
||||
json.addProperty("token", paramsModel.token)
|
||||
json.addProperty("challenge", paramsModel.challenge)
|
||||
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
val ion = Ion.with(activity)
|
||||
.load(paramsModel.authUrl)
|
||||
for ((header, value) in paramsModel.headers) {
|
||||
ion.setHeader(header, value)
|
||||
}
|
||||
|
||||
ion
|
||||
Ion.with(activity)
|
||||
.load(paramsModel.origin + paramsModel.authUrl)
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { e, result ->
|
||||
Log.i("resultTag", result.toString())
|
||||
if (result == null) {
|
||||
if (args.mobile) {
|
||||
createResponse(false)
|
||||
@@ -94,11 +77,7 @@ class ResultFragment : Fragment() {
|
||||
}
|
||||
} 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)
|
||||
createResponse(true, result.toString(), paramsModel.token)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
package com.tarkvaraprojekt.mobileauthapp.model
|
||||
|
||||
import android.util.Log
|
||||
import android.util.Log.WARN
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ParametersViewModel: ViewModel() {
|
||||
@@ -18,9 +16,6 @@ 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
|
||||
}
|
||||
@@ -36,9 +31,4 @@ class ParametersViewModel: ViewModel() {
|
||||
fun setOrigin(newOrigin: String) {
|
||||
_origin = newOrigin
|
||||
}
|
||||
|
||||
fun setHeaders(newHeaders: Map<String, String>) {
|
||||
Log.i("HEADERS", newHeaders.toList().toString())
|
||||
_headers = newHeaders
|
||||
}
|
||||
}
|
@@ -67,7 +67,7 @@
|
||||
<string name="gender_label">SEX</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">Checking the created token</string>
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">The app will close automatically</string>
|
||||
|
||||
<!-- menu -->
|
||||
|
@@ -22,11 +22,4 @@ More info about installing third party applications on the Android phones: https
|
||||
The project comes with a test mobile application and a test web application that can be used to try the MobileAuthApp authentication feature even if you don't have any web applications or mobile applications that require user authentication. Both projects come with a README file that help with a setup.
|
||||
The mobile authentication application, when launched by the user not a website or some other application, can also read card holder's information, which can be used to verify whether the application reads the information from the ID card correctly.
|
||||
|
||||
### Wiki pages relevant for the "Software project" subject
|
||||
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision)
|
||||
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes)
|
||||
* [Project tasks](https://tvp-mobile-authentication.atlassian.net/jira/software/projects/MOB/boards/1/backlog) (Ask Tanel for JIRA permissions if needed).
|
||||
* [Project plan](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-plan)
|
||||
* [Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases)
|
||||
* [User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories)
|
||||
* [Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests)
|
||||
### See the [Wiki](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki) for pages relevant for the "Software project" subject
|
||||
|
@@ -43,5 +43,4 @@ 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,19 +9,17 @@ 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://heaid.ee"
|
||||
private const val AUTH_URL = "$BASE_URL/auth/login"
|
||||
private const val CHALLENGE_URL = "$BASE_URL/auth/challenge"
|
||||
private const val BASE_URL = "https-base-url-here"
|
||||
|
||||
/**
|
||||
* Test mobile app to demonstrate how other applications could potentially use MobileAuthApp.
|
||||
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
|
||||
* Single purpose app that launches the MobileAuthApp and gets the response back (JWT).
|
||||
* Only for demo purposes.
|
||||
*/
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -33,18 +31,19 @@ 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("idCode").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("name").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("authority").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("token").toString())
|
||||
Log.i("getResult", response.data?.getStringExtra("result").toString())
|
||||
var user = ""
|
||||
try {
|
||||
user = response.data?.getStringExtra("name").toString()
|
||||
val resultObject = JSONObject(response.data?.getStringExtra("result").toString())
|
||||
user = resultObject.getString("principal")
|
||||
} catch (e: Exception) {
|
||||
Log.i("getResult", "unable to retrieve name")
|
||||
Log.i("getResult", "unable to retrieve name from principal")
|
||||
}
|
||||
showResult(user)
|
||||
}
|
||||
@@ -55,27 +54,47 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
showLogin()
|
||||
|
||||
binding.loginOptionNfcButton.setOnClickListener {
|
||||
launchAuth()
|
||||
}
|
||||
binding.loginOptionNfcButton.setOnClickListener { getData() }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates an intent to launch the MobileAuthApp
|
||||
*/
|
||||
private fun launchAuth() {
|
||||
private fun launchAuth(challenge: String = "challenge", originUrl: String = "baseUrl", authUrl: String = "authUrl") {
|
||||
val launchIntent = Intent()
|
||||
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
|
||||
launchIntent.putExtra("action", "auth")
|
||||
launchIntent.putExtra("challenge", CHALLENGE_URL)
|
||||
launchIntent.putExtra("originUrl", BASE_URL)
|
||||
launchIntent.putExtra("authUrl", AUTH_URL)
|
||||
launchIntent.putExtra("headers","${(0..100000).random()}")
|
||||
launchIntent.putExtra("challenge", challenge)
|
||||
launchIntent.putExtra("originUrl", originUrl)
|
||||
launchIntent.putExtra("authUrl", authUrl)
|
||||
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,6 +1,5 @@
|
||||
// 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()
|
||||
@@ -8,7 +7,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,6 +31,3 @@ 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 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.
|
||||
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.
|
||||
|
||||
|
||||
## Credits...
|
||||
|
@@ -13,14 +13,11 @@
|
||||
<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>
|
||||
@@ -118,91 +115,6 @@
|
||||
</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
23
demoBackend/src/demo-website/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
.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.
@@ -1,24 +0,0 @@
|
||||
# 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/).
|
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
1
demoBackend/src/demo-website/dist/css/app.eb039c1f.css
vendored
Normal file
1
demoBackend/src/demo-website/dist/css/app.eb039c1f.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
#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:700;color:#2c3e50}#nav a.router-link-exact-active{color:#42b983}.container>div[data-v-2dcb24ca]{margin-top:2vh}.loginButton[data-v-2dcb24ca]{height:4vh;width:20vh;line-height:3vh}.loginButton>p[data-v-2dcb24ca]{font-size:3vh;text-align:center}#canvas[data-v-2dcb24ca]{height:30vh;width:30vh}nav[data-v-21165a6a]{height:5vh}div[data-v-cd8fea1a]{margin-top:2vh}
|
7
demoBackend/src/demo-website/dist/css/chunk-vendors.a251e031.css
vendored
Normal file
7
demoBackend/src/demo-website/dist/css/chunk-vendors.a251e031.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
1
demoBackend/src/demo-website/dist/index.html
vendored
Normal file
1
demoBackend/src/demo-website/dist/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!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"><link rel="icon" href="/favicon.ico"><title>demo-website</title><link href="/css/app.eb039c1f.css" rel="preload" as="style"><link href="/css/chunk-vendors.a251e031.css" rel="preload" as="style"><link href="/js/app.c2a68e49.js" rel="preload" as="script"><link href="/js/chunk-vendors.22b03028.js" rel="preload" as="script"><link href="/css/chunk-vendors.a251e031.css" rel="stylesheet"><link href="/css/app.eb039c1f.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but demo-website doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.22b03028.js"></script><script src="/js/app.c2a68e49.js"></script></body></html>
|
4
demoBackend/src/demo-website/dist/js/app.c2a68e49.js
vendored
Normal file
4
demoBackend/src/demo-website/dist/js/app.c2a68e49.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
demoBackend/src/demo-website/dist/js/app.c2a68e49.js.map
vendored
Normal file
1
demoBackend/src/demo-website/dist/js/app.c2a68e49.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
27
demoBackend/src/demo-website/dist/js/chunk-vendors.22b03028.js
vendored
Normal file
27
demoBackend/src/demo-website/dist/js/chunk-vendors.22b03028.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
demoBackend/src/demo-website/dist/js/chunk-vendors.22b03028.js.map
vendored
Normal file
1
demoBackend/src/demo-website/dist/js/chunk-vendors.22b03028.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,44 +0,0 @@
|
||||
#!/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" "$@"
|
@@ -1,19 +0,0 @@
|
||||
:: 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%" %*
|
27444
demoBackend/src/demo-website/package-lock.json
generated
27444
demoBackend/src/demo-website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
<!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>
|
@@ -1,26 +0,0 @@
|
||||
<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.
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,128 +0,0 @@
|
||||
<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>
|
@@ -1,50 +0,0 @@
|
||||
<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>
|
@@ -1,65 +0,0 @@
|
||||
<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>
|
@@ -1,99 +0,0 @@
|
||||
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' })
|
@@ -1,31 +0,0 @@
|
||||
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
|
@@ -1,22 +0,0 @@
|
||||
<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>
|
@@ -1,22 +0,0 @@
|
||||
<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
@@ -1,20 +0,0 @@
|
||||
// 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,13 +3,14 @@ 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://heaid.ee"
|
||||
|
||||
// Authentication request timeout in seconds.
|
||||
val AUTH_REQUEST_TIMEOUT_MS = 120000
|
||||
|
||||
val USER_ROLE = "USER"
|
||||
val WEBSITE_ORIGIN_URL = "https://5d0c-85-253-195-195.ngrok.io"
|
||||
}
|
||||
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -3,12 +3,9 @@ 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
|
||||
@@ -28,11 +25,12 @@ import javax.cache.Cache
|
||||
import javax.cache.CacheManager
|
||||
import javax.cache.Caching
|
||||
import javax.cache.configuration.CompleteConfiguration
|
||||
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||
import javax.cache.configuration.FactoryBuilder
|
||||
import javax.cache.configuration.MutableConfiguration
|
||||
import javax.cache.expiry.CreatedExpiryPolicy
|
||||
import javax.cache.expiry.Duration
|
||||
|
||||
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||
|
||||
@Configuration
|
||||
class ValidationConfiguration {
|
||||
@@ -41,11 +39,12 @@ 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.")
|
||||
@@ -56,8 +55,6 @@ class ValidationConfiguration {
|
||||
return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
fun nonceCache(): Cache<String, ZonedDateTime>? {
|
||||
val cacheManager: CacheManager = cacheManager()
|
||||
@@ -68,7 +65,6 @@ class ValidationConfiguration {
|
||||
LOG.warn("Creating new cache.")
|
||||
cache = createNonceCache(cacheManager)
|
||||
}
|
||||
|
||||
return cache
|
||||
}
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
package com.tarkvaratehnika.demobackend.dto
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
data class AuthDto(var roles: ArrayList<GrantedAuthority>, var userData: HashMap<String, String>)
|
@@ -1,5 +0,0 @@
|
||||
package com.tarkvaratehnika.demobackend.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class AuthTokenDTO (@JsonProperty("auth-token") val token : String)
|
@@ -0,0 +1,6 @@
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class AuthTokenDTO (val token : String, val challenge : String) {
|
||||
}
|
@@ -23,12 +23,14 @@
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration.Companion.ROLE_USER
|
||||
import com.tarkvaratehnika.demobackend.web.rest.AuthenticationController
|
||||
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
|
||||
@@ -42,15 +44,23 @@ 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, sessionId: String?) : AuthDto {
|
||||
fun authenticate(auth : Authentication) : Authentication {
|
||||
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, sessionId)
|
||||
return WebEidAuthentication.fromCertificate(userCertificate, authorities, challenge)
|
||||
} catch (e : TokenValidationException) {
|
||||
// Validation failed.
|
||||
throw AuthenticationServiceException("Token validation failed. " + e.message)
|
||||
|
@@ -22,27 +22,17 @@
|
||||
|
||||
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.collections.HashMap
|
||||
import kotlin.math.log
|
||||
|
||||
class WebEidAuthentication(
|
||||
private val principalName: String,
|
||||
@@ -50,71 +40,39 @@ class WebEidAuthentication(
|
||||
private val authorities: ArrayList<GrantedAuthority>
|
||||
) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication {
|
||||
|
||||
|
||||
// Companion object is for static functions.
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(WebEidAuthentication::class.java)
|
||||
|
||||
private val loggedInUsers = HashMap<String, Authentication>()
|
||||
|
||||
fun fromCertificate(
|
||||
userCertificate: X509Certificate,
|
||||
sessionId: String?,
|
||||
): AuthDto {
|
||||
// Get user data.
|
||||
val name = getPrincipalNameFromCertificate(userCertificate)
|
||||
authorities: ArrayList<GrantedAuthority>,
|
||||
challenge: String
|
||||
): Authentication {
|
||||
val principalName = getPrincipalNameFromCertificate(userCertificate)
|
||||
val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate))
|
||||
|
||||
// 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)
|
||||
val authentication = WebEidAuthentication(principalName, idCode, authorities)
|
||||
loggedInUsers[challenge] = authentication
|
||||
return authentication
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for getting a Spring authentication object by supplying a challenge.
|
||||
* TODO: Figure out a more secure solution in the future.
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
fun fromChallenge(challenge: String): Authentication? {
|
||||
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
|
||||
// return null
|
||||
// }
|
||||
throw ResponseStatusException(HttpStatus.REQUEST_TIMEOUT, "Token not received in time.")
|
||||
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
|
||||
}
|
||||
|
||||
// // TODO: DELETE
|
||||
|
@@ -0,0 +1,20 @@
|
||||
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"
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
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,15 @@
|
||||
package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.SessionManager
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthDto
|
||||
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
|
||||
import com.tarkvaratehnika.demobackend.security.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,33 +18,23 @@ class AuthenticationController {
|
||||
private val LOG = LoggerFactory.getLogger(AuthenticationController::class.java)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@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])
|
||||
// Create Spring Security Authentication object with supplied token as credentials.
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, body)
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||
|
||||
// Return authentication object if success.
|
||||
return AuthTokenDTOAuthenticationProvider.authenticate(auth, sessionId)
|
||||
return AuthTokenDTOAuthenticationProvider.authenticate(auth)
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : AuthDto {
|
||||
return WebEidAuthentication.fromSession(headers)
|
||||
@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("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
|
||||
|
||||
return auth
|
||||
}
|
||||
}
|
@@ -22,24 +22,14 @@
|
||||
|
||||
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) {
|
||||
@@ -47,30 +37,10 @@ class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
|
||||
|
||||
@GetMapping("challenge")
|
||||
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))
|
||||
fun challenge(): ChallengeDto {
|
||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||
LOG.warn(challengeDto.nonce)
|
||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||
return challengeDto
|
||||
}
|
||||
|
||||
|
@@ -1,2 +1 @@
|
||||
server.servlet.session.cookie.http-only=false
|
||||
|
||||
|
29
demoBackend/src/main/resources/static/css/main.css
Normal file
29
demoBackend/src/main/resources/static/css/main.css
Normal file
@@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
14
demoBackend/src/main/resources/static/js/index.js
Normal file
14
demoBackend/src/main/resources/static/js/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
71
demoBackend/src/main/resources/static/js/main.js
Normal file
71
demoBackend/src/main/resources/static/js/main.js
Normal file
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
}
|
38
demoBackend/src/main/resources/templates/index.html
Normal file
38
demoBackend/src/main/resources/templates/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!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>
|
35
demoBackend/src/main/resources/templates/signature.html
Normal file
35
demoBackend/src/main/resources/templates/signature.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!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