mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2025-08-30 07:10:59 +03:00
Compare commits
6 Commits
iteration-
...
MOB-42
Author | SHA1 | Date | |
---|---|---|---|
|
2c5430977d | ||
|
68a7db2e77 | ||
|
a4caf24a35 | ||
|
5b70a8f997 | ||
|
168c9be010 | ||
|
636beeb7f3 |
@@ -69,6 +69,7 @@ class AuthFragment : Fragment() {
|
||||
goToTheStart()
|
||||
}
|
||||
}.start()
|
||||
//binding!!.nextButton.visibility = View.INVISIBLE
|
||||
binding!!.nextButton.setOnClickListener { goToNextFragment() }
|
||||
binding!!.cancelButton.setOnClickListener { goToTheStart() }
|
||||
val adapter = NfcAdapter.getDefaultAdapter(activity)
|
||||
@@ -90,7 +91,7 @@ class AuthFragment : Fragment() {
|
||||
if (args.auth) {
|
||||
val jws = Authenticator(comms).authenticate(
|
||||
intentParameters.challenge,
|
||||
intentParameters.authUrl,
|
||||
intentParameters.origin,
|
||||
viewModel.userPin
|
||||
)
|
||||
intentParameters.setToken(jws)
|
||||
@@ -140,8 +141,7 @@ class AuthFragment : Fragment() {
|
||||
} else {
|
||||
if (!args.mobile) {
|
||||
//Currently for some reason the activity is not killed entirely. Must be looked into further.
|
||||
requireActivity().finish()
|
||||
exitProcess(0)
|
||||
requireActivity().finishAndRemoveTask()
|
||||
} else {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
|
@@ -127,10 +127,14 @@ class CanFragment : Fragment() {
|
||||
// TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment.
|
||||
if (args.saving) {
|
||||
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
||||
} else if (args.auth) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else if (args.auth || args.mobile) {
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import java.lang.Exception
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
* HomeFragment is only shown to the user when then the user launches the application. When the application
|
||||
@@ -30,6 +31,7 @@ class HomeFragment : Fragment() {
|
||||
|
||||
private var binding: FragmentHomeBinding? = null
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -56,11 +58,14 @@ class HomeFragment : Fragment() {
|
||||
// We use !! because we want an exception when something is not right.
|
||||
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
|
||||
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
|
||||
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
|
||||
} else { //Website
|
||||
// Currently the test website won't send the authUrl parameter
|
||||
//Log.i("intentDebugging", requireActivity().intent.data.toString())
|
||||
intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!)
|
||||
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")!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// There was a problem with parameters, which means that authentication is not possible.
|
||||
|
@@ -128,10 +128,14 @@ class PinFragment : Fragment() {
|
||||
private fun goToTheStart() {
|
||||
if (args.saving) {
|
||||
findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
|
||||
} else if (args.auth) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else if (args.auth || args.mobile) {
|
||||
if (args.mobile) {
|
||||
val resultIntent = Intent()
|
||||
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_canFragment_to_homeFragment)
|
||||
}
|
||||
|
@@ -14,15 +14,6 @@ import com.google.gson.JsonObject
|
||||
import com.koushikdutta.ion.Ion
|
||||
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
|
||||
import com.tarkvaraprojekt.mobileauthapp.network.BASE_URL
|
||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenApi
|
||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenApiService
|
||||
import com.tarkvaraprojekt.mobileauthapp.network.TokenItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* ResultFragment is used to create a JWT and to send response to the website/application
|
||||
@@ -48,12 +39,8 @@ class ResultFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding!!.resultBackButton.setOnClickListener {
|
||||
// if (args.mobile) {
|
||||
// createResponse()
|
||||
// }
|
||||
postToken()
|
||||
}
|
||||
binding!!.resultBackButton.visibility = View.GONE
|
||||
postToken()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,40 +51,40 @@ class ResultFragment : Fragment() {
|
||||
json.addProperty("token", paramsModel.token)
|
||||
json.addProperty("challenge", paramsModel.challenge)
|
||||
|
||||
Ion.getDefault(activity).getConscryptMiddleware().enable(false)
|
||||
|
||||
Ion.getDefault(activity).conscryptMiddleware.enable(false)
|
||||
Ion.with(activity)
|
||||
.load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication")
|
||||
.load(paramsModel.origin + paramsModel.authUrl)
|
||||
.setJsonObjectBody(json)
|
||||
.asJsonObject()
|
||||
.setCallback { e, result ->
|
||||
// do stuff with the result or error
|
||||
Log.i("Log thingy", result.toString())
|
||||
if (result == null) {
|
||||
// TODO: Set auth message failed and close the app
|
||||
Log.i("Log thingy fail", "result was null")
|
||||
if (args.mobile) {
|
||||
createResponse(false)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
Log.i("POST request response", result.toString())
|
||||
if (args.mobile) {
|
||||
createResponse(true, result.toString(), paramsModel.token)
|
||||
} else {
|
||||
requireActivity().finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
// CoroutineScope(Dispatchers.Default).launch {
|
||||
// val response = TokenApi.retrofitService.postToken(jsonBody)
|
||||
// Log.v("Response", response.message())
|
||||
// if (response.isSuccessful) {
|
||||
// //Success scenario here
|
||||
// } else {
|
||||
// //Failure scenario here
|
||||
// if (args.mobile) {
|
||||
// createResponse(false)
|
||||
// } else {
|
||||
// //Currently for some reason the activity is not killed entirely. Must be looked into further.
|
||||
// requireActivity().finish()
|
||||
// exitProcess(0)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used when the MobileAuthApp was launched by an app. Not for website use.
|
||||
*/
|
||||
private fun createResponse(success: Boolean = true) {
|
||||
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("result", result)
|
||||
resultIntent.putExtra("token", token)
|
||||
requireActivity().setResult(responseCode, resultIntent)
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ import java.security.MessageDigest
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class Authenticator(val comms : Comms) {
|
||||
class Authenticator(val comms: Comms) {
|
||||
|
||||
val type = "JWT"
|
||||
val algorithm = "ES384"
|
||||
@@ -36,7 +36,7 @@ class Authenticator(val comms : Comms) {
|
||||
// Get header and claims.
|
||||
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
|
||||
val claims =
|
||||
"""{"iat":"$epoch","exp":"$exp","aud":"$originUrl","iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
||||
"""{"iat":"$epoch","exp":"$exp","aud":["$originUrl"],"iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}"""
|
||||
|
||||
val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
|
||||
claims.toByteArray(Charsets.UTF_8)
|
||||
@@ -51,7 +51,7 @@ class Authenticator(val comms : Comms) {
|
||||
return jwt + "." + base64Encode(signed)
|
||||
}
|
||||
|
||||
fun base64Encode(bytes: ByteArray) : String? {
|
||||
fun base64Encode(bytes: ByteArray): String? {
|
||||
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
|
||||
return encoded.replace("=", "")
|
||||
}
|
||||
|
@@ -13,6 +13,9 @@ class ParametersViewModel: ViewModel() {
|
||||
private var _token: String = ""
|
||||
val token get() = _token
|
||||
|
||||
private var _origin: String = ""
|
||||
val origin get() = _origin
|
||||
|
||||
fun setChallenge(newChallenge: String) {
|
||||
_challenge = newChallenge
|
||||
}
|
||||
@@ -24,4 +27,8 @@ class ParametersViewModel: ViewModel() {
|
||||
fun setToken(newToken: String) {
|
||||
_token = newToken
|
||||
}
|
||||
|
||||
fun setOrigin(newOrigin: String) {
|
||||
_origin = newOrigin
|
||||
}
|
||||
}
|
@@ -56,8 +56,8 @@
|
||||
<string name="clear_button">FORGET</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">Wait for the app to close</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
|
@@ -55,8 +55,8 @@
|
||||
<string name="gender_label">SUGU</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
||||
<string name="result_text">Tulemust kontrollitakse</string>
|
||||
<string name="result_info">Rakendus sulgeb ennast ise</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Seaded</string>
|
||||
|
@@ -55,8 +55,8 @@
|
||||
<string name="clear_button">FORGET</string>
|
||||
|
||||
<!-- string resources for ResultFragment layout-->
|
||||
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string>
|
||||
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string>
|
||||
<string name="result_text">Controlling the created token</string>
|
||||
<string name="result_info">Wait for the app to close</string>
|
||||
|
||||
<!-- menu -->
|
||||
<string name="menu_settings_title">Settings</string>
|
||||
|
@@ -20,6 +20,9 @@ More info about installing third party applications on the Android phones: https
|
||||
|
||||
**NB! Before using the application make sure that the NFC is enabled on the phone, otherwise information can not be read from the ID card.**
|
||||
|
||||
### Testing the application
|
||||
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.
|
||||
|
||||
### Wiki pages relevant for the "Software project" subject
|
||||
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision) *last updated on 10.10*
|
||||
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes) *last updated for iteration3 on 08.11*
|
||||
|
10
TestMobileApp/README.md
Normal file
10
TestMobileApp/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# TestMobileApp overview
|
||||
### The purpose
|
||||
The TestMobileApp was created in order to demonstrate how a different application on the Android smartphone could use the MobileAuthApp for user authentication purposes.
|
||||
### Installing the application
|
||||
The application installation process is the same as with the MobileAuthApp. Check the guide in the project's [main readme file](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC#installing-the-application-on-the-phone).
|
||||
### Using the application
|
||||
In order to use this application a backend server must be running that can issue challenges and verify the token created by the MobileAuthApp.
|
||||
Use demoBackend application that is included in the project. Follow the demoBackend setup guide and once you have a backend running take the https address of the backend
|
||||
and add it in the TestMobileApp's MainActivty.kt file as the new value for the constant variable BASE_URL (this is easly noticeable in the class as it is pointed out with a comment).
|
||||
Now the app can be used.
|
@@ -5,10 +5,18 @@ import android.content.Intent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
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"
|
||||
|
||||
/**
|
||||
* Test mobile app to demonstrate how other applications can use MobileAuthApp.
|
||||
@@ -18,9 +26,11 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var authLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
|
||||
@@ -28,25 +38,45 @@ class MainActivity : AppCompatActivity() {
|
||||
// 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())
|
||||
var user = ""
|
||||
try {
|
||||
val resultObject = JSONObject(response.data?.getStringExtra("result").toString())
|
||||
user = resultObject.getString("principal")
|
||||
} catch (e: Exception) {
|
||||
Log.i("getResult", "unable to retrieve name from principal")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
binding.loginOptionNfcButton.setOnClickListener { launchAuth() }
|
||||
//binding.loginOptionNfcButton.setOnClickListener { getData() }
|
||||
showLogin()
|
||||
|
||||
binding.loginOptionNfcButton.setOnClickListener { getData() }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that creates an intent to launch the MobileAuthApp
|
||||
*/
|
||||
private fun launchAuth(challenge: String = "challenge", authUrl: String = "authUrl") {
|
||||
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)
|
||||
launchIntent.putExtra("originUrl", originUrl)
|
||||
launchIntent.putExtra("authUrl", authUrl)
|
||||
launchIntent.putExtra("mobile", true)
|
||||
authLauncher.launch(launchIntent)
|
||||
@@ -58,19 +88,35 @@ class MainActivity : AppCompatActivity() {
|
||||
*/
|
||||
private fun getData() {
|
||||
// Enter the server endpoint address to here
|
||||
val baseUrl = "enter-base-url-here"
|
||||
val url = "$baseUrl/auth/challenge"
|
||||
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()
|
||||
launchAuth(challenge, baseUrl)
|
||||
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
|
||||
}
|
||||
|
||||
private fun showResult(user: String) {
|
||||
binding.loginOptions.visibility = View.GONE
|
||||
binding.resultLayout.visibility = View.VISIBLE
|
||||
binding.resultObject.text = getString(R.string.hello, user)
|
||||
binding.buttonForget.setOnClickListener {
|
||||
binding.resultObject.text = ""
|
||||
binding.resultLayout.visibility = View.GONE
|
||||
binding.loginOptions.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,7 +26,8 @@
|
||||
android:layout_margin="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/choose_method_text_view"
|
||||
@@ -46,4 +47,30 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/result_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintTop_toBottomOf="@id/login_text_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/result_object"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:textSize="18sp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_forget"
|
||||
android:text="@string/forget_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@@ -4,6 +4,8 @@
|
||||
<string name="login_text">Login</string>
|
||||
<string name="choose_login_method">Choose login method</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Successful response</string>
|
||||
<string name="auth_success">Logged in</string>
|
||||
<string name="auth_failure">Response failed</string>
|
||||
<string name="forget_button">Forget</string>
|
||||
<string name="hello">Hello, %s!</string>
|
||||
</resources>
|
@@ -4,6 +4,8 @@
|
||||
<string name="login_text">Logi sisse</string>
|
||||
<string name="choose_login_method">Vali sobiv meetod</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Vastus kätte saadud</string>
|
||||
<string name="auth_success">Sisse logimine õnnestus</string>
|
||||
<string name="auth_failure">Vastust ei õnnestunud kätte saada</string>
|
||||
<string name="forget_button">Unusta</string>
|
||||
<string name="hello">Tere, %s!</string>
|
||||
</resources>
|
@@ -3,6 +3,8 @@
|
||||
<string name="login_text">Login</string>
|
||||
<string name="choose_login_method">Choose login method</string>
|
||||
<string name="method_nfc">NFC auth</string>
|
||||
<string name="auth_success">Successful response</string>
|
||||
<string name="auth_success">Logged in</string>
|
||||
<string name="auth_failure">Response failed</string>
|
||||
<string name="forget_button">Forget</string>
|
||||
<string name="hello">Hello, %s!</string>
|
||||
</resources>
|
@@ -42,6 +42,11 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.9.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.webeid.security</groupId>
|
||||
<artifactId>authtoken-validation</artifactId>
|
||||
|
@@ -10,7 +10,7 @@ class ApplicationConfiguration {
|
||||
// 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://6bb0-85-253-195-252.ngrok.io"
|
||||
val WEBSITE_ORIGIN_URL = "https://5d0c-85-253-195-195.ngrok.io"
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package com.tarkvaratehnika.demobackend.config
|
||||
|
||||
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||
@@ -28,14 +30,25 @@ import javax.cache.configuration.MutableConfiguration
|
||||
import javax.cache.expiry.CreatedExpiryPolicy
|
||||
import javax.cache.expiry.Duration
|
||||
|
||||
import javax.cache.configuration.FactoryBuilder.factoryOf
|
||||
|
||||
@Configuration
|
||||
class ValidationConfiguration {
|
||||
|
||||
private val LOG: Logger = LoggerFactory.getLogger(ValidationConfiguration::class.java)
|
||||
|
||||
private val NONCE_TTL_MINUTES: Long = 5
|
||||
private val CACHE_NAME = "nonceCache"
|
||||
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.")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun cacheManager(): CacheManager {
|
||||
@@ -47,7 +60,9 @@ class ValidationConfiguration {
|
||||
val cacheManager: CacheManager = cacheManager()
|
||||
var cache =
|
||||
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
||||
|
||||
if (cache == null) {
|
||||
LOG.warn("Creating new cache.")
|
||||
cache = createNonceCache(cacheManager)
|
||||
}
|
||||
return cache
|
||||
@@ -65,7 +80,7 @@ class ValidationConfiguration {
|
||||
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
||||
.setTypes(String::class.java, ZonedDateTime::class.java)
|
||||
.setExpiryPolicyFactory(
|
||||
FactoryBuilder.factoryOf(
|
||||
factoryOf(
|
||||
CreatedExpiryPolicy(
|
||||
Duration(
|
||||
TimeUnit.MINUTES,
|
||||
|
@@ -23,6 +23,9 @@
|
||||
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 org.slf4j.LoggerFactory
|
||||
import org.springframework.security.authentication.AuthenticationServiceException
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
@@ -37,11 +40,11 @@ import java.security.cert.X509Certificate
|
||||
|
||||
|
||||
@Component
|
||||
class AuthTokenDTOAuthenticationProvider {
|
||||
object AuthTokenDTOAuthenticationProvider {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(AuthTokenDTOAuthenticationProvider::class.java)
|
||||
|
||||
|
||||
companion object {
|
||||
const val ROLE_USER : String = "ROLE_USER"
|
||||
}
|
||||
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||
|
||||
|
||||
@@ -52,7 +55,6 @@ class AuthTokenDTOAuthenticationProvider {
|
||||
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)
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.tarkvaratehnika.demobackend.web
|
||||
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER
|
||||
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
|
||||
|
@@ -26,7 +26,7 @@ class AuthenticationController {
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||
|
||||
// Return authentication object if success.
|
||||
return AuthTokenDTOAuthenticationProvider().authenticate(auth)
|
||||
return AuthTokenDTOAuthenticationProvider.authenticate(auth)
|
||||
}
|
||||
|
||||
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
|
@@ -24,6 +24,7 @@ package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
@@ -33,10 +34,12 @@ import org.webeid.security.nonce.NonceGenerator
|
||||
@RequestMapping("auth")
|
||||
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
|
||||
|
||||
@GetMapping("challenge")
|
||||
fun challenge(): ChallengeDto {
|
||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||
LOG.warn(challengeDto.nonce)
|
||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||
return challengeDto
|
||||
}
|
||||
|
@@ -1,3 +1,12 @@
|
||||
html {
|
||||
font-size: 2vh;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.cont {
|
||||
display: grid;
|
||||
width: 80%;
|
||||
|
@@ -11,7 +11,7 @@ function launchAuthApp(action) {
|
||||
httpGetAsync(originUrl + challengeUrl, (body) => {
|
||||
let data = JSON.parse(body);
|
||||
let challenge = data.nonce;
|
||||
let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling.
|
||||
let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling.
|
||||
console.log(intent);
|
||||
window.location.href = intent;
|
||||
pollForAuth(POLLING_INTERVAL, challenge);
|
||||
@@ -20,8 +20,8 @@ function launchAuthApp(action) {
|
||||
|
||||
function pollForAuth(timeout, challenge) {
|
||||
console.log("Polling for auth");
|
||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge;
|
||||
|
||||
let encodedChallenge = encodeURIComponent(challenge);
|
||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + encodedChallenge;
|
||||
let counter = 0;
|
||||
let timer = setInterval(() => {
|
||||
// Fetch authentication object.
|
||||
@@ -48,7 +48,7 @@ function createParametrizedIntentUrl(challenge, action) {
|
||||
else if (challenge == null) {
|
||||
console.error("Challenge missing, can't authenticate without it.")
|
||||
} else {
|
||||
return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl;
|
||||
return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
||||
<a class="navbar-brand" href="#">Auth demo webapp</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="cont">
|
||||
@@ -29,7 +29,7 @@
|
||||
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-secondary" id="loginButton" data-action="auth">Log in</button>
|
||||
<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>
|
||||
|
@@ -3,38 +3,31 @@
|
||||
<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 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 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">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">Log out<span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<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>
|
||||
<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="input-group mb-3">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" id="inputGroupFile01">
|
||||
<label class="custom-file-label" for="inputGroupFile01">Choose file</label>
|
||||
</div>
|
||||
<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>
|
||||
|
Reference in New Issue
Block a user