Fixed the error handling a bit, added some text to login page

This commit is contained in:
TanelOrumaa 2022-01-18 00:34:45 +02:00
parent e5931692b6
commit c232a1f734
17 changed files with 124 additions and 9114 deletions

View File

@ -68,18 +68,18 @@ class AuthFragment : Fragment() {
override fun onFinish() {
Thread.sleep(750)
cancelAuth()
cancelAuth(408)
}
}.start()
// The button exists in code for testing reasons, but not visible to the user anymore unless visibility is changed in the code.
binding.nextButton.visibility = View.GONE
binding.nextButton.setOnClickListener { goToNextFragment() }
binding.cancelButton.setOnClickListener { cancelAuth() }
binding.cancelButton.setOnClickListener { cancelAuth(444) }
val adapter = NfcAdapter.getDefaultAdapter(activity)
if (adapter != null)
getInfoFromIdCard(adapter)
else { // If NFC adapter can not be detected then end the auth process as it is not possible to read an ID card
cancelAuth() // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes
cancelAuth(447) // It would be a good idea to show user some notification as it might be confusing if the app suddenly closes
}
}
@ -89,7 +89,7 @@ class AuthFragment : Fragment() {
findNavController().navigate(action)
}
private fun cancelAuth() {
private fun cancelAuth(code: Int) {
viewModel.clearUserInfo()
timer.cancel()
if (args.mobile) {
@ -97,6 +97,7 @@ class AuthFragment : Fragment() {
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(code)
requireActivity().finishAndRemoveTask()
}
}
@ -123,24 +124,28 @@ class AuthFragment : Fragment() {
}
} catch (e: Exception) {
when(e) {
is TagLostException -> requireActivity().runOnUiThread { binding!!.timeCounter.text = getString(R.string.id_card_removed_early) }
is TagLostException -> requireActivity().runOnUiThread {
binding!!.timeCounter.text = getString(R.string.id_card_removed_early)
cancelAuth(444)
}
else -> {
when ("invalid pin") {
in e.message.toString().lowercase() -> requireActivity().runOnUiThread {
val messagePieces = e.message.toString().split(" ")
binding.timeCounter.text = getString(R.string.wrong_pin, messagePieces[messagePieces.size - 1])
viewModel.deletePin(requireContext())
cancelAuth(449)
}
else -> requireActivity().runOnUiThread {
binding.timeCounter.text = getString(R.string.wrong_can_text)
viewModel.deleteCan(requireContext())
cancelAuth(449)
}
}
}
}
// Give user some time to read the error message
Thread.sleep(2000)
cancelAuth()
} finally {
adapter.disableReaderMode(activity)
}

View File

@ -90,6 +90,7 @@ class CanFragment : Fragment() {
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(444)
requireActivity().finishAndRemoveTask()
}
} else {

View File

@ -5,9 +5,17 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.viewModels
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs
import androidx.navigation.navArgs
import com.google.gson.JsonObject
import com.koushikdutta.ion.Ion
import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentResultBinding
import com.tarkvaraprojekt.mobileauthapp.model.ParametersViewModel
/**
@ -16,6 +24,8 @@ import com.tarkvaraprojekt.mobileauthapp.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var navigationController: NavController
private val paramsModel: ParametersViewModel by viewModels()
// If true the settings menu can be accessed from the toolbar in the upper part of the screen.
var menuAvailable: Boolean = true
@ -54,4 +64,24 @@ class MainActivity : AppCompatActivity() {
}
else -> super.onOptionsItemSelected(item)
}
fun returnError(errorCode: Int) {
val json = JsonObject()
json.addProperty("auth-token", "")
json.addProperty("error", errorCode)
Ion.getDefault(this).conscryptMiddleware.enable(false)
val ion = Ion.with(this)
.load(paramsModel.authUrl)
for ((header, value) in paramsModel.headers) {
ion.setHeader(header, value)
}
ion
.setJsonObjectBody(json)
.asJsonObject()
.setCallback { _, _ ->
}
}
}

View File

@ -96,6 +96,7 @@ class PinFragment : Fragment() {
requireActivity().setResult(AppCompatActivity.RESULT_CANCELED, resultIntent)
requireActivity().finish()
} else {
(activity as MainActivity).returnError(444)
requireActivity().finishAndRemoveTask()
}
} else {

View File

@ -68,8 +68,7 @@ class ResultFragment : Fragment() {
fun postToken() {
val json = JsonObject()
json.addProperty("auth-token", paramsModel.token)
json.addProperty("error", 200)
Ion.getDefault(activity).conscryptMiddleware.enable(false)
val ion = Ion.with(activity)

View File

@ -33,4 +33,4 @@ build/
.vscode/
### web-eid.js ###
!**src/demo-website/src/web-eid.js
src/demo-website/src/web-eid.js

View File

@ -94,6 +94,7 @@
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<finalName>demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>

View File

@ -8,7 +8,7 @@
"name": "demo-website",
"version": "0.1.0",
"dependencies": {
"@web-eid/web-eid-library": "github:TanelOrumaa/web-eid.js#main",
"@web-eid/web-eid-library": "../../../../web-eid.js/",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-cookie-next": "^1.3.0",
@ -32,7 +32,6 @@
"../../../../web-eid.js": {
"name": "@web-eid/web-eid-library",
"version": "1.0.1",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@types/node": "^16.11.11"
@ -1925,7 +1924,8 @@
"node_modules/@types/node": {
"version": "16.11.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz",
"integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw=="
"integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==",
"dev": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@ -2643,12 +2643,8 @@
"dev": true
},
"node_modules/@web-eid/web-eid-library": {
"version": "1.0.1",
"resolved": "git+ssh://git@github.com/TanelOrumaa/web-eid.js.git#b9c6f50c78a39b444d5df308826cd3f3476233d1",
"license": "MIT",
"dependencies": {
"@types/node": "^16.11.11"
}
"resolved": "../../../../web-eid.js",
"link": true
},
"node_modules/@webassemblyjs/ast": {
"version": "1.9.0",
@ -16604,7 +16600,8 @@
"@types/node": {
"version": "16.11.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz",
"integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw=="
"integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==",
"dev": true
},
"@types/normalize-package-data": {
"version": "2.4.1",
@ -17212,10 +17209,17 @@
"dev": true
},
"@web-eid/web-eid-library": {
"version": "git+ssh://git@github.com/TanelOrumaa/web-eid.js.git#b9c6f50c78a39b444d5df308826cd3f3476233d1",
"from": "@web-eid/web-eid-library@github:TanelOrumaa/web-eid.js#main",
"version": "file:../../../../web-eid.js",
"requires": {
"@types/node": "^16.11.11"
"@types/node": "^16.11.11",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
"eslint": "^7.25.0",
"rimraf": "^3.0.2",
"rollup": "^2.26.11",
"rollup-plugin-polyfill-node": "^0.8.0",
"rollup-plugin-terser": "^5.3.1",
"typescript": "^3.8.3"
}
},
"@webassemblyjs/ast": {

View File

@ -8,7 +8,8 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@web-eid/web-eid-library": "github:TanelOrumaa/web-eid.js#main",
"@web-eid/web-eid-library": "../../../../web-eid.js/",
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-cookie-next": "^1.3.0",

View File

@ -1,9 +1,15 @@
<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>
<h3 class="text-center">Welcome to Estonian ID card mobile authentication demo website.</h3>
<p>This website to demonstrates the viability of using your NFC-enabled ID-card and your smartphone to authenticate yourself.
This is a proof of concept solution, so currently only authentication is supported. This solution was created for <a href="https://courses.cs.ut.ee/2021/tvp/">Software Project (Tarkvaraprojekt)</a> course in the University of Tartu
in cooperation with <a href="https://github.com/martinpaljak/">Martin Paljak</a>.</p>
<p>This solution is meant to be web-eid.js compatible, so this example website uses a <a href="https://github.com/TanelOrumaa/web-eid.js">fork of web-eid.js</a> which supports the Android authentication app.</p>
<h2>Usage</h2>
<p>To get started, download and install the authentication Android app from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/releases">GitHub</a> (Android 8.0+ required).
You can then click "Login" to authenticate yourself on this demo website with the app or if you are using a non-Android device, you can use both the app or the default web-eid.js option to login using the smartcard reader.
</p>
<p class="text-center">Read more from <a href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">here.</a></p>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ class ApplicationConfiguration {
companion object {
// 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://5a0b-85-253-195-195.ngrok.io"
val WEBSITE_ORIGIN_URL = "https://6fa5-145-14-34-146.ngrok.io"
// Authentication request timeout in seconds.
val AUTH_REQUEST_TIMEOUT_MS = 120000

View File

@ -19,11 +19,10 @@ class SessionManager {
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())
sessionRegistry[sessionId] = AuthDto(arrayListOf(), hashMapOf(), 200)
}
}
@ -37,6 +36,22 @@ class SessionManager {
}
}
fun addErrorToSession(sessionId: String?, authDto: AuthDto) {
// Errors are only sent by authentication app, so we can ignore sessionId being null.
if (sessionRegistry.containsKey(sessionId)) {
sessionRegistry[sessionId]!!.errorCode = authDto.errorCode
}
}
fun getError(sessionId: String) : Int {
if (sessionRegistry.containsKey(sessionId)) {
if (sessionRegistry[sessionId]!!.errorCode != 200) {
return sessionRegistry[sessionId]!!.errorCode
}
}
return 200
}
/**
* Function adds role and userdata specified in authDto to the current session.
*/

View File

@ -2,4 +2,4 @@ package com.tarkvaratehnika.demobackend.dto
import org.springframework.security.core.GrantedAuthority
data class AuthDto(var roles: ArrayList<GrantedAuthority>, var userData: HashMap<String, String>)
data class AuthDto(var roles: ArrayList<GrantedAuthority>, var userData: HashMap<String, String>, var errorCode: Int)

View File

@ -2,4 +2,4 @@ package com.tarkvaratehnika.demobackend.dto
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (@JsonProperty("auth-token") val token : String)
class AuthTokenDTO (@JsonProperty("auth-token") val token : String, val error : Int?)

View File

@ -22,12 +22,14 @@
package com.tarkvaratehnika.demobackend.security
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration.Companion.USER_ROLE
import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.AuthDto
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.webeid.security.certificate.CertificateData
import org.springframework.security.core.Authentication
@ -78,59 +80,46 @@ class WebEidAuthentication(
}
/**
* Function for getting a Spring authentication object by supplying a challenge.
* TODO: Figure out a more secure solution in the future.
* Function for getting a Spring authentication object for this session.
*/
fun fromSession(headers: HashMap<String, String>): AuthDto {
fun fromSession(headers: HashMap<String, String>): ResponseEntity<String> {
val mapper = jacksonObjectMapper()
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.")
return ResponseEntity.status(400).body(mapper.writeValueAsString(400))
}
LOG.warn("SESSION IS NOW: " + sessionId)
}
while (currentTime.time + ApplicationConfiguration.AUTH_REQUEST_TIMEOUT_MS > Date().time) {
Thread.sleep(1000)
// Check if an error has been submitted for this session.
val error = SessionManager.getError(sessionId)
if (error != 200) {
return ResponseEntity.status(error).body(mapper.writeValueAsString(error))
}
// Check if this session has received a role.
if (SessionManager.getSessionHasRole(sessionId, USER_ROLE)) {
// Get AuthDto
val auth = SessionManager.getSessionAuth(sessionId)
// Set role and user data to current session.
SessionManager.addRoleToCurrentSession(auth!!)
LOG.warn("ROLE ADDED AND LOGGING IN.")
return auth
return ResponseEntity.status(200).body(mapper.writeValueAsString(auth))
}
}
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
// return null
// }
throw ResponseStatusException(HttpStatus.REQUEST_TIMEOUT, "Token not received in time.")
}
// // TODO: DELETE
//
// const val ROLE_USER: String = "ROLE_USER"
// private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
//
// fun addAuth(challenge: String) {
// val authorities = arrayListOf<GrantedAuthority>()
// authorities.add(USER_ROLE)
// val auth = WebEidAuthentication("Somename", "11111111111", authorities)
// loggedInUsers[challenge] = auth
// }
//
//
// // TODO: DELETE UNTIL
// In case of timeout return 408.
return ResponseEntity.status(408).body(mapper.writeValueAsString(408))
}
private fun getPrincipalNameFromCertificate(userCertificate: X509Certificate): String {
return Objects.requireNonNull(CertificateData.getSubjectGivenName(userCertificate)) + " " +

View File

@ -1,5 +1,6 @@
package com.tarkvaratehnika.demobackend.web.rest
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.tarkvaratehnika.demobackend.config.SessionManager
import com.tarkvaratehnika.demobackend.dto.AuthDto
import com.tarkvaratehnika.demobackend.dto.AuthTokenDTO
@ -8,6 +9,7 @@ import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
import org.springframework.web.bind.annotation.*
@ -19,12 +21,18 @@ class AuthenticationController {
@PostMapping("login", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun authenticate(@RequestHeader headers: Map<String, String>, @RequestBody body : AuthTokenDTO): AuthDto {
fun authenticate(@RequestHeader headers: Map<String, String>, @RequestBody authTokenDTO: AuthTokenDTO): AuthDto {
val sessionId = SessionManager.getSessionId(headers)
// Check if an error occurred in the auth app.
if (authTokenDTO.error != null && authTokenDTO.error != 200) {
val auth = AuthDto(arrayListOf(), hashMapOf(), authTokenDTO.error)
SessionManager.addErrorToSession(sessionId, auth)
return auth
}
// Create Spring Security Authentication object with supplied token as credentials.
val auth = PreAuthenticatedAuthenticationToken(null, body)
val auth = PreAuthenticatedAuthenticationToken(null, authTokenDTO)
// Return authentication object if success.
return AuthTokenDTOAuthenticationProvider.authenticate(auth, sessionId)
@ -32,7 +40,7 @@ class AuthenticationController {
@GetMapping("login", produces = [MediaType.APPLICATION_JSON_VALUE])
fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : AuthDto {
fun getAuthenticated(@RequestHeader headers: HashMap<String, String>) : ResponseEntity<String> {
return WebEidAuthentication.fromSession(headers)
}