6 Commits

Author SHA1 Message Date
Henrik Lepson
2c5430977d Updated main readme 2021-11-17 09:31:30 +02:00
Henrik Lepson
68a7db2e77 Created a readme for TestMobileApp 2021-11-17 09:26:21 +02:00
Henrik Lepson
a4caf24a35 MOB-41 fixed some remaining issues 2021-11-17 09:15:29 +02:00
TanelOrumaa
5b70a8f997 MOB-42 Added log out button to backend, fixed issue with challenge for test app 2021-11-16 21:30:58 +02:00
Henrik Lepson
168c9be010 fixed app not closing bug, when started from website 2021-11-14 10:13:40 +02:00
TanelOrumaa
636beeb7f3 MOB-42 Fixed token authentication issues (wrong library version, cache getting recreated every request, origin in wrong form) 2021-11-11 21:47:27 +02:00
28 changed files with 234 additions and 108 deletions

View File

@@ -69,6 +69,7 @@ class AuthFragment : Fragment() {
goToTheStart() goToTheStart()
} }
}.start() }.start()
//binding!!.nextButton.visibility = View.INVISIBLE
binding!!.nextButton.setOnClickListener { goToNextFragment() } binding!!.nextButton.setOnClickListener { goToNextFragment() }
binding!!.cancelButton.setOnClickListener { goToTheStart() } binding!!.cancelButton.setOnClickListener { goToTheStart() }
val adapter = NfcAdapter.getDefaultAdapter(activity) val adapter = NfcAdapter.getDefaultAdapter(activity)
@@ -90,7 +91,7 @@ class AuthFragment : Fragment() {
if (args.auth) { if (args.auth) {
val jws = Authenticator(comms).authenticate( val jws = Authenticator(comms).authenticate(
intentParameters.challenge, intentParameters.challenge,
intentParameters.authUrl, intentParameters.origin,
viewModel.userPin viewModel.userPin
) )
intentParameters.setToken(jws) intentParameters.setToken(jws)
@@ -140,8 +141,7 @@ class AuthFragment : Fragment() {
} else { } else {
if (!args.mobile) { if (!args.mobile) {
//Currently for some reason the activity is not killed entirely. Must be looked into further. //Currently for some reason the activity is not killed entirely. Must be looked into further.
requireActivity().finish() requireActivity().finishAndRemoveTask()
exitProcess(0)
} else { } else {
val resultIntent = Intent() val resultIntent = Intent()
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)

View File

@@ -127,10 +127,14 @@ class CanFragment : Fragment() {
// TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment. // TODO: Needs special handling when the app is launched with intent. Temporary solution at the moment.
if (args.saving) { if (args.saving) {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment) findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
} else if (args.auth) { } else if (args.auth || args.mobile) {
val resultIntent = Intent() if (args.mobile) {
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) val resultIntent = Intent()
requireActivity().finish() requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
requireActivity().finishAndRemoveTask()
}
} else { } else {
findNavController().navigate(R.id.action_canFragment_to_homeFragment) findNavController().navigate(R.id.action_canFragment_to_homeFragment)
} }

View File

@@ -14,6 +14,7 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentHomeBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel
import java.lang.Exception 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 * 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 private var binding: FragmentHomeBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -56,11 +58,14 @@ class HomeFragment : Fragment() {
// We use !! because we want an exception when something is not right. // We use !! because we want an exception when something is not right.
intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!) intentParams.setChallenge(requireActivity().intent.getStringExtra("challenge")!!)
intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!) intentParams.setAuthUrl(requireActivity().intent.getStringExtra("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.getStringExtra("originUrl")!!)
} else { //Website } else { //Website
// Currently the test website won't send the authUrl parameter var challenge = requireActivity().intent.data!!.getQueryParameter("challenge")!!
//Log.i("intentDebugging", requireActivity().intent.data.toString()) // TODO: Since due to encoding plus gets converted to space, temporary solution is to replace it back.
intentParams.setChallenge(requireActivity().intent.data!!.getQueryParameter("challenge")!!) challenge = challenge.replace(" ", "+")
intentParams.setChallenge(challenge)
intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!) intentParams.setAuthUrl(requireActivity().intent.data!!.getQueryParameter("authUrl")!!)
intentParams.setOrigin(requireActivity().intent.data!!.getQueryParameter("originUrl")!!)
} }
} catch (e: Exception) { } catch (e: Exception) {
// There was a problem with parameters, which means that authentication is not possible. // There was a problem with parameters, which means that authentication is not possible.

View File

@@ -128,10 +128,14 @@ class PinFragment : Fragment() {
private fun goToTheStart() { private fun goToTheStart() {
if (args.saving) { if (args.saving) {
findNavController().navigate(R.id.action_canFragment_to_settingsFragment) findNavController().navigate(R.id.action_canFragment_to_settingsFragment)
} else if (args.auth) { } else if (args.auth || args.mobile) {
val resultIntent = Intent() if (args.mobile) {
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent) val resultIntent = Intent()
requireActivity().finish() requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
requireActivity().finishAndRemoveTask()
}
} else { } else {
findNavController().navigate(R.id.action_canFragment_to_homeFragment) findNavController().navigate(R.id.action_canFragment_to_homeFragment)
} }

View File

@@ -14,15 +14,6 @@ import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel 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 * 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding!!.resultBackButton.setOnClickListener { binding!!.resultBackButton.visibility = View.GONE
// if (args.mobile) { postToken()
// createResponse()
// }
postToken()
}
} }
/** /**
@@ -64,40 +51,40 @@ class ResultFragment : Fragment() {
json.addProperty("token", paramsModel.token) json.addProperty("token", paramsModel.token)
json.addProperty("challenge", paramsModel.challenge) json.addProperty("challenge", paramsModel.challenge)
Ion.getDefault(activity).getConscryptMiddleware().enable(false) Ion.getDefault(activity).conscryptMiddleware.enable(false)
Ion.with(activity) Ion.with(activity)
.load("https://6bb0-85-253-195-252.ngrok.io/auth/authentication") .load(paramsModel.origin + paramsModel.authUrl)
.setJsonObjectBody(json) .setJsonObjectBody(json)
.asJsonObject() .asJsonObject()
.setCallback { e, result -> .setCallback { e, result ->
// do stuff with the result or error // 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. * 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 responseCode = if (success) AppCompatActivity.RESULT_OK else AppCompatActivity.RESULT_CANCELED
val resultIntent = Intent() val resultIntent = Intent()
resultIntent.putExtra("result", result)
resultIntent.putExtra("token", token)
requireActivity().setResult(responseCode, resultIntent) requireActivity().setResult(responseCode, resultIntent)
requireActivity().finish() requireActivity().finish()
} }

View File

@@ -8,7 +8,7 @@ import java.security.MessageDigest
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
class Authenticator(val comms : Comms) { class Authenticator(val comms: Comms) {
val type = "JWT" val type = "JWT"
val algorithm = "ES384" val algorithm = "ES384"
@@ -36,7 +36,7 @@ class Authenticator(val comms : Comms) {
// Get header and claims. // Get header and claims.
val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}""" val header = """{"typ":"$type","alg":"$algorithm","x5c":["$base64cert"]}"""
val claims = 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( val jwt = base64Encode(header.toByteArray(Charsets.UTF_8)) + "." + base64Encode(
claims.toByteArray(Charsets.UTF_8) claims.toByteArray(Charsets.UTF_8)
@@ -51,7 +51,7 @@ class Authenticator(val comms : Comms) {
return jwt + "." + base64Encode(signed) return jwt + "." + base64Encode(signed)
} }
fun base64Encode(bytes: ByteArray) : String? { fun base64Encode(bytes: ByteArray): String? {
val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes) val encoded = java.util.Base64.getUrlEncoder().encodeToString(bytes)
return encoded.replace("=", "") return encoded.replace("=", "")
} }

View File

@@ -13,6 +13,9 @@ class ParametersViewModel: ViewModel() {
private var _token: String = "" private var _token: String = ""
val token get() = _token val token get() = _token
private var _origin: String = ""
val origin get() = _origin
fun setChallenge(newChallenge: String) { fun setChallenge(newChallenge: String) {
_challenge = newChallenge _challenge = newChallenge
} }
@@ -24,4 +27,8 @@ class ParametersViewModel: ViewModel() {
fun setToken(newToken: String) { fun setToken(newToken: String) {
_token = newToken _token = newToken
} }
fun setOrigin(newOrigin: String) {
_origin = newOrigin
}
} }

View File

@@ -56,8 +56,8 @@
<string name="clear_button">FORGET</string> <string name="clear_button">FORGET</string>
<!-- string resources for ResultFragment layout--> <!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string> <string name="result_text">Controlling the created token</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string> <string name="result_info">Wait for the app to close</string>
<!-- menu --> <!-- menu -->
<string name="menu_settings_title">Settings</string> <string name="menu_settings_title">Settings</string>

View File

@@ -55,8 +55,8 @@
<string name="gender_label">SUGU</string> <string name="gender_label">SUGU</string>
<!-- string resources for ResultFragment layout--> <!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string> <string name="result_text">Tulemust kontrollitakse</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string> <string name="result_info">Rakendus sulgeb ennast ise</string>
<!-- menu --> <!-- menu -->
<string name="menu_settings_title">Seaded</string> <string name="menu_settings_title">Seaded</string>

View File

@@ -55,8 +55,8 @@
<string name="clear_button">FORGET</string> <string name="clear_button">FORGET</string>
<!-- string resources for ResultFragment layout--> <!-- string resources for ResultFragment layout-->
<string name="result_text">See Fragment vastutab vastuse tagastamise eest.</string> <string name="result_text">Controlling the created token</string>
<string name="result_info">Hiljem sulgeb rakendus automaatselt.</string> <string name="result_info">Wait for the app to close</string>
<!-- menu --> <!-- menu -->
<string name="menu_settings_title">Settings</string> <string name="menu_settings_title">Settings</string>

View File

@@ -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.** **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 ### 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* * [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* * [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
View 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.

View File

@@ -5,10 +5,18 @@ import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import com.example.testmobileapp.databinding.ActivityMainBinding import com.example.testmobileapp.databinding.ActivityMainBinding
import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion 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. * 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 authLauncher: ActivityResultLauncher<Intent>
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response -> authLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { response ->
@@ -28,25 +38,45 @@ class MainActivity : AppCompatActivity() {
// Currently we are not actually checking whether we get a valid token. // 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. // 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) 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) { if (response.resultCode == Activity.RESULT_CANCELED) {
binding.loginTextView.text = getString(R.string.auth_failure) binding.loginTextView.text = getString(R.string.auth_failure)
} }
} }
binding.loginOptionNfcButton.setOnClickListener { launchAuth() } showLogin()
//binding.loginOptionNfcButton.setOnClickListener { getData() }
binding.loginOptionNfcButton.setOnClickListener { getData() }
} }
/** /**
* Method that creates an intent to launch the MobileAuthApp * 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() val launchIntent = Intent()
launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity") launchIntent.setClassName("com.tarkvaraprojekt.mobileauthapp", "com.tarkvaraprojekt.mobileauthapp.MainActivity")
launchIntent.putExtra("action", "auth") launchIntent.putExtra("action", "auth")
launchIntent.putExtra("challenge", challenge) launchIntent.putExtra("challenge", challenge)
launchIntent.putExtra("originUrl", originUrl)
launchIntent.putExtra("authUrl", authUrl) launchIntent.putExtra("authUrl", authUrl)
launchIntent.putExtra("mobile", true) launchIntent.putExtra("mobile", true)
authLauncher.launch(launchIntent) authLauncher.launch(launchIntent)
@@ -58,19 +88,35 @@ class MainActivity : AppCompatActivity() {
*/ */
private fun getData() { private fun getData() {
// Enter the server endpoint address to here // Enter the server endpoint address to here
val baseUrl = "enter-base-url-here" val url = "$BASE_URL/auth/challenge"
val url = "$baseUrl/auth/challenge" Ion.getDefault(this).conscryptMiddleware.enable(false)
Ion.with(applicationContext) Ion.with(applicationContext)
.load(url) .load(url)
.asJsonObject() .asJsonObject()
.setCallback { _, result -> .setCallback { _, result ->
try { try {
// Get data from the result and call launchAuth method // Get data from the result and call launchAuth method
val challenge = result.asJsonObject["nonce"].toString() val challenge = result.asJsonObject["nonce"].toString().replace("\"", "")
launchAuth(challenge, baseUrl) Log.v("Challenge", challenge)
launchAuth(challenge, BASE_URL, "/auth/authentication")
} catch (e: Exception) { } catch (e: Exception) {
Log.i("GETrequest", "was unsuccessful") 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
}
}
} }

View File

@@ -26,7 +26,8 @@
android:layout_margin="12dp" android:layout_margin="12dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_text_view" app:layout_constraintTop_toBottomOf="@id/login_text_view"
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone">
<TextView <TextView
android:id="@+id/choose_method_text_view" android:id="@+id/choose_method_text_view"
@@ -46,4 +47,30 @@
</LinearLayout> </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> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,6 +4,8 @@
<string name="login_text">Login</string> <string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string> <string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</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="auth_failure">Response failed</string>
<string name="forget_button">Forget</string>
<string name="hello">Hello, %s!</string>
</resources> </resources>

View File

@@ -4,6 +4,8 @@
<string name="login_text">Logi sisse</string> <string name="login_text">Logi sisse</string>
<string name="choose_login_method">Vali sobiv meetod</string> <string name="choose_login_method">Vali sobiv meetod</string>
<string name="method_nfc">NFC auth</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="auth_failure">Vastust ei õnnestunud kätte saada</string>
<string name="forget_button">Unusta</string>
<string name="hello">Tere, %s!</string>
</resources> </resources>

View File

@@ -3,6 +3,8 @@
<string name="login_text">Login</string> <string name="login_text">Login</string>
<string name="choose_login_method">Choose login method</string> <string name="choose_login_method">Choose login method</string>
<string name="method_nfc">NFC auth</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="auth_failure">Response failed</string>
<string name="forget_button">Forget</string>
<string name="hello">Hello, %s!</string>
</resources> </resources>

View File

@@ -42,6 +42,11 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.webeid.security</groupId> <groupId>org.webeid.security</groupId>
<artifactId>authtoken-validation</artifactId> <artifactId>authtoken-validation</artifactId>

View File

@@ -10,7 +10,7 @@ class ApplicationConfiguration {
// Endpoint for authentication // Endpoint for authentication
val AUTHENTICATION_ENDPOINT_URL = "/auth/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. // 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"
} }
} }

View File

@@ -1,6 +1,8 @@
package com.tarkvaratehnika.demobackend.config package com.tarkvaratehnika.demobackend.config
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider 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.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.core.io.support.PathMatchingResourcePatternResolver 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.CreatedExpiryPolicy
import javax.cache.expiry.Duration import javax.cache.expiry.Duration
import javax.cache.configuration.FactoryBuilder.factoryOf
@Configuration @Configuration
class ValidationConfiguration { class ValidationConfiguration {
private val LOG: Logger = LoggerFactory.getLogger(ValidationConfiguration::class.java)
private val NONCE_TTL_MINUTES: Long = 5 private val NONCE_TTL_MINUTES: Long = 5
private val CACHE_NAME = "nonceCache" 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 TRUSTED_CERTIFICATES_JKS = "trusted_certificates.jks"
private val TRUSTSTORE_PASSWORD = "changeit" private val TRUSTSTORE_PASSWORD = "changeit"
companion object {
const val ROLE_USER : String = "ROLE_USER"
}
init {
LOG.warn("Creating new ValidationConfiguration.")
}
@Bean @Bean
fun cacheManager(): CacheManager { fun cacheManager(): CacheManager {
@@ -47,7 +60,9 @@ class ValidationConfiguration {
val cacheManager: CacheManager = cacheManager() val cacheManager: CacheManager = cacheManager()
var cache = var cache =
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME) cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
if (cache == null) { if (cache == null) {
LOG.warn("Creating new cache.")
cache = createNonceCache(cacheManager) cache = createNonceCache(cacheManager)
} }
return cache return cache
@@ -65,7 +80,7 @@ class ValidationConfiguration {
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>() val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
.setTypes(String::class.java, ZonedDateTime::class.java) .setTypes(String::class.java, ZonedDateTime::class.java)
.setExpiryPolicyFactory( .setExpiryPolicyFactory(
FactoryBuilder.factoryOf( factoryOf(
CreatedExpiryPolicy( CreatedExpiryPolicy(
Duration( Duration(
TimeUnit.MINUTES, TimeUnit.MINUTES,

View File

@@ -23,6 +23,9 @@
package com.tarkvaratehnika.demobackend.security package com.tarkvaratehnika.demobackend.security
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration 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.authentication.AuthenticationServiceException
import org.springframework.security.core.Authentication import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException import org.springframework.security.core.AuthenticationException
@@ -37,11 +40,11 @@ import java.security.cert.X509Certificate
@Component @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) private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
@@ -52,7 +55,6 @@ class AuthTokenDTOAuthenticationProvider {
val authentication = auth as PreAuthenticatedAuthenticationToken val authentication = auth as PreAuthenticatedAuthenticationToken
val token = (authentication.credentials as AuthTokenDTO).token val token = (authentication.credentials as AuthTokenDTO).token
val challenge = (authentication.credentials as AuthTokenDTO).challenge val challenge = (authentication.credentials as AuthTokenDTO).challenge
val authorities = arrayListOf<GrantedAuthority>() val authorities = arrayListOf<GrantedAuthority>()
authorities.add(USER_ROLE) authorities.add(USER_ROLE)

View File

@@ -1,6 +1,6 @@
package com.tarkvaratehnika.demobackend.web 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.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model

View File

@@ -26,7 +26,7 @@ class AuthenticationController {
val auth = PreAuthenticatedAuthenticationToken(null, authToken) val auth = PreAuthenticatedAuthenticationToken(null, authToken)
// Return authentication object if success. // Return authentication object if success.
return AuthTokenDTOAuthenticationProvider().authenticate(auth) return AuthTokenDTOAuthenticationProvider.authenticate(auth)
} }
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE]) @GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])

View File

@@ -24,6 +24,7 @@ package com.tarkvaratehnika.demobackend.web.rest
import com.tarkvaratehnika.demobackend.dto.ChallengeDto import com.tarkvaratehnika.demobackend.dto.ChallengeDto
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@@ -33,10 +34,12 @@ import org.webeid.security.nonce.NonceGenerator
@RequestMapping("auth") @RequestMapping("auth")
class ChallengeController (val nonceGenerator: NonceGenerator) { class ChallengeController (val nonceGenerator: NonceGenerator) {
private val LOG = LoggerFactory.getLogger(ChallengeController::class.java)
@GetMapping("challenge") @GetMapping("challenge")
fun challenge(): ChallengeDto { fun challenge(): ChallengeDto {
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce()) val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
LOG.warn(challengeDto.nonce)
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing. // WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
return challengeDto return challengeDto
} }

View File

@@ -1,3 +1,12 @@
html {
font-size: 2vh;
}
.navbar {
padding-left: 1rem;
padding-right: 1rem;
}
.cont { .cont {
display: grid; display: grid;
width: 80%; width: 80%;

View File

@@ -11,7 +11,7 @@ function launchAuthApp(action) {
httpGetAsync(originUrl + challengeUrl, (body) => { httpGetAsync(originUrl + challengeUrl, (body) => {
let data = JSON.parse(body); let data = JSON.parse(body);
let challenge = data.nonce; let challenge = data.nonce;
let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling. let intent = createParametrizedIntentUrl(challenge, action, originUrl); // TODO: Error handling.
console.log(intent); console.log(intent);
window.location.href = intent; window.location.href = intent;
pollForAuth(POLLING_INTERVAL, challenge); pollForAuth(POLLING_INTERVAL, challenge);
@@ -20,8 +20,8 @@ function launchAuthApp(action) {
function pollForAuth(timeout, challenge) { function pollForAuth(timeout, challenge) {
console.log("Polling for auth"); 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 counter = 0;
let timer = setInterval(() => { let timer = setInterval(() => {
// Fetch authentication object. // Fetch authentication object.
@@ -48,7 +48,7 @@ function createParametrizedIntentUrl(challenge, action) {
else if (challenge == null) { else if (challenge == null) {
console.error("Challenge missing, can't authenticate without it.") console.error("Challenge missing, can't authenticate without it.")
} else { } else {
return intentUrl + "?" + "action=" + action + "&challenge=" + challenge + "&authUrl=" + originUrl + authenticationRequestUrl; return intentUrl + "?" + "action=" + action + "&challenge=" + encodeURIComponent(challenge) + "&authUrl=" + authenticationRequestUrl + "&originUrl=" + originUrl;
} }
} }

View File

@@ -21,7 +21,7 @@
<body> <body>
<nav class="navbar navbar-dark bg-dark"> <nav class="navbar navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">Auth demo web application</a> <a class="navbar-brand" href="#">Auth demo webapp</a>
</div> </div>
</nav> </nav>
<div class="cont"> <div class="cont">
@@ -29,7 +29,7 @@
website using your ID card by using the button below.</h4> website using your ID card by using the button below.</h4>
<h5>Make sure you've installed the authentication app from: <a <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> 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"> <div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
Login failed. Refresh the page to try again. Login failed. Refresh the page to try again.
</div> </div>

View File

@@ -3,38 +3,31 @@
<head> <head>
<title>Login</title> <title>Login</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <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"> <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/signature.js}"></script>
<script type="text/javascript" th:src="@{/js/main.js}"></script> <script type="text/javascript" th:src="@{/js/main.js}"></script>
</head> </head>
<body> <body>
<nav class="navbar navbar-dark bg-dark"> <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 web application</a> <ul class="navbar-nav mr-auto">
</div> <li class="nav-item">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <a href="/" class="btn btn-danger">Log out</a>
<span class="navbar-toggler-icon"></span> </li>
</button> </ul>
<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>
</nav> </nav>
<div class="cont"> <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> <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">
<div class="custom-file"> <input type="file" class="custom-file-input" id="customFile">
<input type="file" class="custom-file-input" id="inputGroupFile01">
<label class="custom-file-label" for="inputGroupFile01">Choose file</label>
</div>
</div> </div>
<button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button> <button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button>
</div> </div>