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:
TanelOrumaa
2021-11-08 17:30:56 +02:00
parent 44469b8533
commit f9cd30922e
32 changed files with 1492 additions and 5 deletions

View File

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

View File

@@ -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"
}
}

View File

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

View File

@@ -0,0 +1,3 @@
package com.tarkvaratehnika.demobackend.dto
data class ChallengeDto(val nonce : String)

View File

@@ -0,0 +1,6 @@
package com.tarkvaratehnika.demobackend.security
import com.fasterxml.jackson.annotation.JsonProperty
class AuthTokenDTO (val token : String, val challenge : String) {
}

View File

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

View File

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

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

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

View File

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

View File

@@ -0,0 +1 @@

Binary file not shown.

View 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;
}

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

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

View 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>

View 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>

View File

@@ -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() {
}
}