mirror of
https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC.git
synced 2025-12-04 12:55:13 +02:00
MOB-42 Added backend server, two frontend webpages and rest endpoints for getting challenge, submitting authentication token and getting authentication object. MOB-21 Added JWT creation, but whole process still needs some work.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.tarkvaratehnika.demobackend
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication(exclude=[SecurityAutoConfiguration::class])
|
||||
class DemoBackendApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<DemoBackendApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.tarkvaratehnika.demobackend.config
|
||||
|
||||
class ApplicationConfiguration {
|
||||
|
||||
companion object {
|
||||
// URL for intent, do not edit.
|
||||
val AUTH_APP_LAUNCH_INTENT = "authapp://start/"
|
||||
// Endpoint for challenge.
|
||||
val CHALLENGE_ENDPOINT_URL = "/auth/challenge"
|
||||
// Endpoint for authentication
|
||||
val AUTHENTICATION_ENDPOINT_URL = "/auth/authentication"
|
||||
// URL for application. Use ngrok for HTTPS (or a tool of your own choice) and put the HTTPS link here.
|
||||
val WEBSITE_ORIGIN_URL = "https://6bb0-85-253-195-252.ngrok.io"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.tarkvaratehnika.demobackend.config
|
||||
|
||||
import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
|
||||
import org.webeid.security.exceptions.JceException
|
||||
import org.webeid.security.nonce.NonceGenerator
|
||||
import org.webeid.security.nonce.NonceGeneratorBuilder
|
||||
import org.webeid.security.validator.AuthTokenValidator
|
||||
import org.webeid.security.validator.AuthTokenValidatorBuilder
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.cache.Cache
|
||||
import javax.cache.CacheManager
|
||||
import javax.cache.Caching
|
||||
import javax.cache.configuration.CompleteConfiguration
|
||||
import javax.cache.configuration.FactoryBuilder
|
||||
import javax.cache.configuration.MutableConfiguration
|
||||
import javax.cache.expiry.CreatedExpiryPolicy
|
||||
import javax.cache.expiry.Duration
|
||||
|
||||
@Configuration
|
||||
class ValidationConfiguration {
|
||||
|
||||
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"
|
||||
|
||||
@Bean
|
||||
fun cacheManager(): CacheManager {
|
||||
return Caching.getCachingProvider(CaffeineCachingProvider::class.java.name).cacheManager
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun nonceCache(): Cache<String, ZonedDateTime>? {
|
||||
val cacheManager: CacheManager = cacheManager()
|
||||
var cache =
|
||||
cacheManager.getCache<String?, ZonedDateTime?>(CACHE_NAME)
|
||||
if (cache == null) {
|
||||
cache = createNonceCache(cacheManager)
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun generator(): NonceGenerator? {
|
||||
return NonceGeneratorBuilder()
|
||||
.withNonceTtl(java.time.Duration.ofMinutes(NONCE_TTL_MINUTES))
|
||||
.withNonceCache(nonceCache())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNonceCache(cacheManager: CacheManager): Cache<String?, ZonedDateTime?>? {
|
||||
val cacheConfig: CompleteConfiguration<String, ZonedDateTime> = MutableConfiguration<String, ZonedDateTime>()
|
||||
.setTypes(String::class.java, ZonedDateTime::class.java)
|
||||
.setExpiryPolicyFactory(
|
||||
FactoryBuilder.factoryOf(
|
||||
CreatedExpiryPolicy(
|
||||
Duration(
|
||||
TimeUnit.MINUTES,
|
||||
NONCE_TTL_MINUTES + 1
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return cacheManager.createCache(CACHE_NAME, cacheConfig)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun loadTrustedCACertificatesFromCerFiles() : Array<X509Certificate> {
|
||||
val caCertificates = ArrayList<X509Certificate>()
|
||||
|
||||
try {
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val resolver = PathMatchingResourcePatternResolver()
|
||||
val resources = resolver.getResources("$CERTS_RESOURCE_PATH/*.cer")
|
||||
|
||||
resources.forEach { resource ->
|
||||
val caCertificate = certFactory.generateCertificate(resource.inputStream) as X509Certificate
|
||||
caCertificates.add(caCertificate)
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
when (e){
|
||||
is CertificateException, is IOException -> {
|
||||
throw RuntimeException("Error initializing trusted CA certificates. $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
return caCertificates.toTypedArray()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun loadTrustedCACertificatesFromTrustStore() : Array<X509Certificate> {
|
||||
val caCertificates = ArrayList<X509Certificate>()
|
||||
|
||||
ValidationConfiguration::class.java.getResourceAsStream("$CERTS_RESOURCE_PATH/$TRUSTED_CERTIFICATES_JKS").use { inputStream ->
|
||||
try {
|
||||
if (inputStream == null) {
|
||||
// No truststore files found.
|
||||
return arrayOf()
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(inputStream, TRUSTSTORE_PASSWORD.toCharArray())
|
||||
val aliases = keyStore.aliases()
|
||||
|
||||
while (aliases.hasMoreElements()) {
|
||||
val alias = aliases.nextElement()
|
||||
val certificate = keyStore.getCertificate(alias) as X509Certificate
|
||||
caCertificates.add(certificate)
|
||||
}
|
||||
|
||||
|
||||
} catch (e : Exception) {
|
||||
when (e) {
|
||||
is IOException, is CertificateException, is KeyStoreException, is NoSuchAlgorithmException -> {
|
||||
throw RuntimeException("Error initializing trusted CA certificates from trust store. $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return caCertificates.toTypedArray()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun validator() : AuthTokenValidator {
|
||||
try {
|
||||
return AuthTokenValidatorBuilder()
|
||||
.withSiteOrigin(URI.create(ApplicationConfiguration.WEBSITE_ORIGIN_URL))
|
||||
.withNonceCache(nonceCache())
|
||||
.withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromCerFiles())
|
||||
.withTrustedCertificateAuthorities(*loadTrustedCACertificatesFromTrustStore())
|
||||
.build()
|
||||
} catch (e : JceException) {
|
||||
throw RuntimeException("Error building the Web eID auth token validator.", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.tarkvaratehnika.demobackend.dto
|
||||
|
||||
data class ChallengeDto(val nonce : String)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
class AuthTokenDTO (val token : String, val challenge : String) {
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2020, 2021 The Web eID Project
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ValidationConfiguration
|
||||
import org.springframework.security.authentication.AuthenticationServiceException
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import org.springframework.stereotype.Component
|
||||
import org.webeid.security.exceptions.TokenValidationException
|
||||
import org.webeid.security.validator.AuthTokenValidator
|
||||
import java.security.cert.CertificateEncodingException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
|
||||
@Component
|
||||
class AuthTokenDTOAuthenticationProvider {
|
||||
|
||||
companion object {
|
||||
const val ROLE_USER : String = "ROLE_USER"
|
||||
}
|
||||
private val USER_ROLE: GrantedAuthority = SimpleGrantedAuthority(ROLE_USER)
|
||||
|
||||
|
||||
val tokenValidator: AuthTokenValidator = ValidationConfiguration().validator()
|
||||
|
||||
@Throws(AuthenticationException::class)
|
||||
fun authenticate(auth : Authentication) : Authentication {
|
||||
val authentication = auth as PreAuthenticatedAuthenticationToken
|
||||
val token = (authentication.credentials as AuthTokenDTO).token
|
||||
val challenge = (authentication.credentials as AuthTokenDTO).challenge
|
||||
|
||||
val authorities = arrayListOf<GrantedAuthority>()
|
||||
authorities.add(USER_ROLE)
|
||||
|
||||
try {
|
||||
val userCertificate: X509Certificate = tokenValidator.validate(token)
|
||||
return WebEidAuthentication.fromCertificate(userCertificate, authorities, challenge)
|
||||
} catch (e : TokenValidationException) {
|
||||
// Validation failed.
|
||||
throw AuthenticationServiceException("Token validation failed. " + e.message)
|
||||
} catch (e : CertificateEncodingException) {
|
||||
// Failed to extract subject fields from the certificate.
|
||||
throw AuthenticationServiceException("Incorrect certificate subject fields: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (c) 2020, 2021 The Web eID Project
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.tarkvaratehnika.demobackend.security
|
||||
|
||||
import org.webeid.security.certificate.CertificateData
|
||||
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.log
|
||||
|
||||
class WebEidAuthentication(
|
||||
private val principalName: String,
|
||||
private val idCode: String,
|
||||
private val authorities: ArrayList<GrantedAuthority>
|
||||
) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication {
|
||||
|
||||
// Companion object is for static functions.
|
||||
companion object {
|
||||
|
||||
private val loggedInUsers = HashMap<String, Authentication>()
|
||||
|
||||
fun fromCertificate(
|
||||
userCertificate: X509Certificate,
|
||||
authorities: ArrayList<GrantedAuthority>,
|
||||
challenge: String
|
||||
): Authentication {
|
||||
val principalName = getPrincipalNameFromCertificate(userCertificate)
|
||||
val idCode = Objects.requireNonNull(CertificateData.getSubjectIdCode(userCertificate))
|
||||
val authentication = WebEidAuthentication(principalName, idCode, authorities)
|
||||
loggedInUsers[challenge] = authentication
|
||||
return authentication
|
||||
}
|
||||
|
||||
/**
|
||||
* Function for getting a Spring authentication object by supplying a challenge.
|
||||
* TODO: Figure out a more secure solution in the future.
|
||||
*/
|
||||
fun fromChallenge(challenge: String): Authentication? {
|
||||
// if (ThreadLocalRandom.current().nextFloat() < 0.5f) { // TODO: For testing.
|
||||
// return null
|
||||
// }
|
||||
val auth = loggedInUsers[challenge]
|
||||
if (auth != null) {
|
||||
// If challenge is valid, delete the authentication object from the map (so this can only be fetched once).
|
||||
loggedInUsers.remove(challenge)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
// // TODO: DELETE
|
||||
//
|
||||
// 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
|
||||
|
||||
private fun getPrincipalNameFromCertificate(userCertificate: X509Certificate): String {
|
||||
return Objects.requireNonNull(CertificateData.getSubjectGivenName(userCertificate)) + " " +
|
||||
Objects.requireNonNull(CertificateData.getSubjectSurname(userCertificate))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.tarkvaratehnika.demobackend.web
|
||||
|
||||
import com.tarkvaratehnika.demobackend.config.ApplicationConfiguration
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
@Controller
|
||||
class LoginController {
|
||||
|
||||
@GetMapping
|
||||
fun login(model : Model) : String {
|
||||
model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
|
||||
model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
|
||||
model.addAttribute("originUrl", ApplicationConfiguration.WEBSITE_ORIGIN_URL)
|
||||
model.addAttribute("loggedInUrl", "/signature")
|
||||
model.addAttribute("authenticationRequestUrl", ApplicationConfiguration.AUTHENTICATION_ENDPOINT_URL)
|
||||
return "index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.tarkvaratehnika.demobackend.web
|
||||
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider.Companion.ROLE_USER
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
|
||||
@Controller
|
||||
class SignatureController {
|
||||
|
||||
|
||||
@PreAuthorize("hasAuthority('$ROLE_USER')")
|
||||
@GetMapping("signature")
|
||||
fun signature(model : Model) : String {
|
||||
// model.addAttribute("intentUrl", ApplicationConfiguration.AUTH_APP_LAUNCH_INTENT)
|
||||
// model.addAttribute("challengeUrl", ApplicationConfiguration.CHALLENGE_ENDPOINT_URL)
|
||||
return "signature"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTO
|
||||
import com.tarkvaratehnika.demobackend.security.AuthTokenDTOAuthenticationProvider
|
||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestController
|
||||
@RequestMapping("auth")
|
||||
class AuthenticationController {
|
||||
|
||||
private val LOG = LoggerFactory.getLogger(AuthenticationController::class.java)
|
||||
|
||||
|
||||
@PostMapping("authentication", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun authenticate(@RequestBody authToken : AuthTokenDTO): Authentication {
|
||||
// Create Spring Security Authentication object with supplied token as credentials.
|
||||
val auth = PreAuthenticatedAuthenticationToken(null, authToken)
|
||||
|
||||
// Return authentication object if success.
|
||||
return AuthTokenDTOAuthenticationProvider().authenticate(auth)
|
||||
}
|
||||
|
||||
@GetMapping("authentication", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
fun getAuthenticated(@RequestParam challenge: String) : Authentication? {
|
||||
val auth = WebEidAuthentication.fromChallenge(challenge)
|
||||
if (auth == null) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Not allowed.")
|
||||
}
|
||||
return auth
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2020, 2021 The Web eID Project
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.tarkvaratehnika.demobackend.web.rest
|
||||
|
||||
import com.tarkvaratehnika.demobackend.dto.ChallengeDto
|
||||
import com.tarkvaratehnika.demobackend.security.WebEidAuthentication
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.webeid.security.nonce.NonceGenerator
|
||||
|
||||
@RestController
|
||||
@RequestMapping("auth")
|
||||
class ChallengeController (val nonceGenerator: NonceGenerator) {
|
||||
|
||||
|
||||
@GetMapping("challenge")
|
||||
fun challenge(): ChallengeDto {
|
||||
val challengeDto = ChallengeDto(nonceGenerator.generateAndStoreNonce())
|
||||
// WebEidAuthentication.addAuth(challengeDto.nonce) // For testing.
|
||||
return challengeDto
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
1
demoBackend/src/main/resources/application.properties
Normal file
1
demoBackend/src/main/resources/application.properties
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
BIN
demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer
Normal file
BIN
demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer
Normal file
Binary file not shown.
BIN
demoBackend/src/main/resources/certs/ESTEID2018.cer
Normal file
BIN
demoBackend/src/main/resources/certs/ESTEID2018.cer
Normal file
Binary file not shown.
BIN
demoBackend/src/main/resources/certs/trusted_certificates.jks
Normal file
BIN
demoBackend/src/main/resources/certs/trusted_certificates.jks
Normal file
Binary file not shown.
20
demoBackend/src/main/resources/static/css/main.css
Normal file
20
demoBackend/src/main/resources/static/css/main.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.cont {
|
||||
display: grid;
|
||||
width: 80%;
|
||||
padding-top: 10%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 10%;
|
||||
}
|
||||
|
||||
#loginButton {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.cont > * {
|
||||
margin: 1rem;
|
||||
}
|
||||
14
demoBackend/src/main/resources/static/js/index.js
Normal file
14
demoBackend/src/main/resources/static/js/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
window.onload = () => {
|
||||
// Add event listener for login button.
|
||||
let loginButton = document.getElementById("loginButton");
|
||||
|
||||
if (loginButton != null) {
|
||||
loginButton.addEventListener("click", () => {
|
||||
let action = loginButton.getAttribute("data-action");
|
||||
loginButton.setAttribute("disabled", "true");
|
||||
loginButton.textContent = "Logging in";
|
||||
launchAuthApp(action);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
67
demoBackend/src/main/resources/static/js/main.js
Normal file
67
demoBackend/src/main/resources/static/js/main.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const POLLING_INTERVAL = 1000;
|
||||
const POLLING_RETRIES = 120;
|
||||
|
||||
function launchAuthApp(action) {
|
||||
if (!isAndroid()) {
|
||||
alert("Functionality only available for Android devices.")
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch challenge.
|
||||
httpGetAsync(originUrl + challengeUrl, (body) => {
|
||||
let data = JSON.parse(body);
|
||||
let challenge = data.nonce;
|
||||
let intent = createParametrizedIntentUrl(challenge, action); // TODO: Error handling.
|
||||
console.log(intent);
|
||||
window.location.href = intent;
|
||||
pollForAuth(POLLING_INTERVAL, challenge);
|
||||
})
|
||||
}
|
||||
|
||||
function pollForAuth(timeout, challenge) {
|
||||
console.log("Polling for auth");
|
||||
let requestUrl = originUrl + authenticationRequestUrl + "?challenge=" + challenge;
|
||||
|
||||
let counter = 0;
|
||||
let timer = setInterval(() => {
|
||||
// Fetch authentication object.
|
||||
httpGetAsync(requestUrl, (body) => {
|
||||
console.log(body);
|
||||
// If this is a successful request, stop the polling.
|
||||
clearInterval(timer);
|
||||
window.location.href = originUrl + loggedInUrl;
|
||||
});
|
||||
counter++;
|
||||
if (counter > POLLING_RETRIES) {
|
||||
clearInterval(timer); // Stop polling after some time.
|
||||
let loginErrorAlert = document.getElementById("loginErrorAlert");
|
||||
loginErrorAlert.classList.remove("d-none")
|
||||
}
|
||||
}, timeout)
|
||||
|
||||
}
|
||||
|
||||
function createParametrizedIntentUrl(challenge, action) {
|
||||
if (action == null) {
|
||||
console.error("There has to be an action for intent.")
|
||||
}
|
||||
return intentUrl + "?" + "action=" + action + (challenge != null ? "&challenge=" + challenge : "");
|
||||
}
|
||||
|
||||
function isAndroid() {
|
||||
// Check if using Android device.
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return ua.indexOf("android") > -1;
|
||||
}
|
||||
|
||||
function httpGetAsync(theUrl, callback) {
|
||||
console.log("Sending a request.")
|
||||
const xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
|
||||
callback(xmlHttp.responseText);
|
||||
}
|
||||
}
|
||||
xmlHttp.open("GET", theUrl, true); // true for asynchronous
|
||||
xmlHttp.send(null);
|
||||
}
|
||||
38
demoBackend/src/main/resources/templates/index.html
Normal file
38
demoBackend/src/main/resources/templates/index.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link th:href="@{/css/main.css}" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" th:src="@{/js/index.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
||||
<script th:inline="javascript">const originUrl = [[${originUrl}]];
|
||||
const intentUrl = [[${intentUrl}]];
|
||||
const challengeUrl = [[${challengeUrl}]];
|
||||
const loggedInUrl = [[${loggedInUrl}]];
|
||||
const authenticationRequestUrl = [[${authenticationRequestUrl}]]</script> <!-- Pass some values to JS -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Auth demo web application</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="cont">
|
||||
<h4>Welcome to Estonian ID card mobile authentication demo website. When using a mobile phone, you can log in to the
|
||||
website using your ID card by using the button below.</h4>
|
||||
<h5>Make sure you've installed the authentication app from: <a
|
||||
href="https://github.com/TanelOrumaa/Estonian-ID-card-mobile-authenticator-POC">GitHub</a></h5>
|
||||
<button type="button" class="btn btn-secondary" id="loginButton" data-action="auth">Log in</button>
|
||||
<div class="alert alert-danger d-none" role="alert" id="loginErrorAlert">
|
||||
Login failed. Refresh the page to try again.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
42
demoBackend/src/main/resources/templates/signature.html
Normal file
42
demoBackend/src/main/resources/templates/signature.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link th:href="@{/css/main.css}" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" th:src="@{/js/signature.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/main.js}"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<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>
|
||||
</nav>
|
||||
<div class="cont">
|
||||
<h4>Congratulations! You have just authenticated yourself using your mobile phone and your ID-card. You can try to give a signature to a file now.</h4>
|
||||
<h5>This page is still WIP, signing a document feature will be implemented later.</h5>
|
||||
<div class="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>
|
||||
<button type="button" class="btn btn-secondary" id="signFile" data-action="auth">Sign</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.tarkvaratehnika.demobackend
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class DemoBackendApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user