Compare commits

...

7 Commits

Author SHA1 Message Date
TanelOrumaa 869f11f8a8 Merge branch 'main' of https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC 2022-01-18 01:25:59 +02:00
TanelOrumaa bfa5a91ef3
Merge pull request #19 from TanelOrumaa/MOB-55
MOB-55 to main
2022-01-18 01:16:30 +02:00
TanelOrumaa d67c815aad
Merge pull request #21 from TanelOrumaa/testappchanges
Testappchanges to MOB-55
2022-01-18 00:54:23 +02:00
TanelOrumaa c28fc2be48 Merge branch 'main' of https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC 2022-01-18 00:35:16 +02:00
TanelOrumaa c232a1f734 Fixed the error handling a bit, added some text to login page 2022-01-18 00:34:45 +02:00
Kevin 7edd8189a4
Update README.md 2021-12-14 22:52:08 +02:00
TanelOrumaa b565f6846d MOB-55 Demo website 2021-12-07 00:05:06 +02:00
26 changed files with 167 additions and 9122 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

@ -71,8 +71,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

@ -22,11 +22,4 @@ More info about installing third party applications on the Android phones: https
The project comes with a test mobile application and a test web application that can be used to try the MobileAuthApp authentication feature even if you don't have any web applications or mobile applications that require user authentication. Both projects come with a README file that help with a setup.
The mobile authentication application, when launched by the user not a website or some other application, can also read card holder's information, which can be used to verify whether the application reads the information from the ID card correctly.
### Wiki pages relevant for the "Software project" subject
* [Project Vision](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-Vision)
* [Release Notes](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Release-notes)
* [Project tasks](https://tvp-mobile-authentication.atlassian.net/jira/software/projects/MOB/boards/1/backlog) (Ask Tanel for JIRA permissions if needed).
* [Project plan](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Project-plan)
* [Use Cases](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Cases)
* [User stories](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/User-stories)
* [Use Case Tests](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki/Use-Case-Tests)
### See the [Wiki](https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC/wiki) for pages relevant for the "Software project" subject

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

@ -0,0 +1 @@
#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50}#nav{padding:30px}#nav a{font-weight:700;color:#2c3e50}#nav a.router-link-exact-active{color:#42b983}.container>div[data-v-2dcb24ca]{margin-top:2vh}.loginButton[data-v-2dcb24ca]{height:4vh;width:20vh;line-height:3vh}.loginButton>p[data-v-2dcb24ca]{font-size:3vh;text-align:center}#canvas[data-v-2dcb24ca]{height:30vh;width:30vh}nav[data-v-21165a6a]{height:5vh}div[data-v-cd8fea1a]{margin-top:2vh}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>demo-website</title><link href="/css/app.eb039c1f.css" rel="preload" as="style"><link href="/css/chunk-vendors.a251e031.css" rel="preload" as="style"><link href="/js/app.c2a68e49.js" rel="preload" as="script"><link href="/js/chunk-vendors.22b03028.js" rel="preload" as="script"><link href="/css/chunk-vendors.a251e031.css" rel="stylesheet"><link href="/css/app.eb039c1f.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but demo-website doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.22b03028.js"></script><script src="/js/app.c2a68e49.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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