diff --git a/MobileAuthApp/app/build.gradle b/MobileAuthApp/app/build.gradle index 5cd7f6a..0d59bc9 100644 --- a/MobileAuthApp/app/build.gradle +++ b/MobileAuthApp/app/build.gradle @@ -62,4 +62,9 @@ dependencies { //SecureDataStoring implementation("androidx.security:security-crypto:1.0.0") + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2', + 'org.bouncycastle:bcprov-jdk15on:1.60', + 'io.jsonwebtoken:jjwt-gson:0.11.2' + } \ No newline at end of file diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt index ab14a76..6cf616e 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/AuthFragment.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.tarkvaraprojekt.mobileauthapp.NFC.Comms +import com.tarkvaraprojekt.mobileauthapp.auth.Authenticator import com.tarkvaraprojekt.mobileauthapp.databinding.FragmentAuthBinding import com.tarkvaraprojekt.mobileauthapp.model.SmartCardViewModel import java.lang.Exception diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java index 928312b..76e1c3a 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/NFC/Comms.java @@ -419,6 +419,7 @@ public class Comms { InternalAuthenticate[4] = (byte) (0x1d + 16 * (token.length / 16)); InternalAuthenticate[6] = (byte) (0x11 + 16 * (token.length / 16)); response = getResponse(token, InternalAuthenticate, "Internal Authenticate"); + if (response[response.length - 2] != (byte) 0x90 || response[response.length - 1] != 0x00) { throw new RuntimeException("Signing the token failed."); } @@ -429,6 +430,7 @@ public class Comms { return Arrays.copyOf(signature, indexOfTerminator); } + private byte[] getResponse(byte[] data, byte[] command, String log) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, IOException { byte[] response = idCard.transceive(createSecureAPDU(data, command)); Log.i(log, Hex.toHexString(response)); diff --git a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt index e2f86fd..0d7c862 100644 --- a/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt +++ b/MobileAuthApp/app/src/main/java/com/tarkvaraprojekt/mobileauthapp/auth/Authenticator.kt @@ -1,22 +1,61 @@ package com.tarkvaraprojekt.mobileauthapp.auth -import android.nfc.tech.IsoDep +import android.os.Message +import android.util.Log import com.tarkvaraprojekt.mobileauthapp.NFC.Comms -import java.math.BigInteger +import io.jsonwebtoken.SignatureAlgorithm +import org.bouncycastle.util.encoders.Base64 +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.time.LocalDateTime +import java.time.ZoneOffset +import javax.crypto.Mac +import kotlin.experimental.and class Authenticator(val comms : Comms) { - public fun authenticate(nonce: BigInteger, challengeUrl: String, pin1: String) { + val type = "JWT" + val algorithm = "ES384" + var iss = "https://self-issued.me" // Will be specified at a later date. + val algorithmUsedForSigning = SignatureAlgorithm.ES384 + + fun authenticate(challenge: String, originUrl: String, pin1: String) : String { // Ask PIN 1 from the user and get the authentication certificate from the ID card. val authenticationCertificate : ByteArray = comms.getCertificate(true); - // Create the authentication token (OpenID X509) + // Encode the certificate in base64. + val base64cert = String(Base64.encode(authenticationCertificate)) - // Hash the authentication token. + // Get current epoch time. + val epoch = LocalDateTime.now(ZoneOffset.UTC).atZone(ZoneOffset.UTC).toEpochSecond() + + // Get expiration time. + val exp = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(5 * 60L).atZone(ZoneOffset.UTC).toEpochSecond() + + // Get subject value. + val sub = authenticationCertificate[0] // TODO: + + // Get header and claims. + val header = """{"typ":"$type","alg":"$algorithm","x5c":"$base64cert"}""" + val claims = """{"iat":"$epoch","exp":"$exp","aud":"$originUrl","iss":"$iss","sub":"$sub","nonce":"$challenge","cnf":{"tbh":""}}""" + + var jwt = String(Base64.encode(header.toByteArray(Charsets.UTF_8))) + "." + String(Base64.encode(claims.toByteArray(Charsets.UTF_8))) + jwt = jwt.replace("=", "") + + Log.v("JWT", jwt) // Send the authentication token hash to the ID card for signing and get signed authentication token as response. + val encoded = MessageDigest.getInstance("SHA-384").digest(jwt.toByteArray()) + val signed = comms.authenticate(pin1, encoded) + + val jws = jwt + "." + String(Base64.encode(signed)) + + Log.v("Token", jws) // Return the signed authentication token. + return jws } + + } \ No newline at end of file diff --git a/demoBackend/.gitignore b/demoBackend/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/demoBackend/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java b/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/demoBackend/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/demoBackend/.mvn/wrapper/maven-wrapper.jar b/demoBackend/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/demoBackend/.mvn/wrapper/maven-wrapper.jar differ diff --git a/demoBackend/.mvn/wrapper/maven-wrapper.properties b/demoBackend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..a9f1ef8 --- /dev/null +++ b/demoBackend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/demoBackend/mvnw b/demoBackend/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/demoBackend/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/demoBackend/mvnw.cmd b/demoBackend/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/demoBackend/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/demoBackend/pom.xml b/demoBackend/pom.xml new file mode 100644 index 0000000..bd4ba2a --- /dev/null +++ b/demoBackend/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.6 + + + com.tarkvaratehnika + demoBackend + 0.0.1-SNAPSHOT + demoBackend + demoBackend + + 11 + 1.5.31 + 2.8.5 + 1.1.1 + + + + org.springframework.boot + spring-boot-starter-web + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.webeid.security + authtoken-validation + 1.2.0 + + + javax.cache + cache-api + ${javaxcache.version} + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-config + + + + + + gitlab + https://gitlab.com/api/v4/projects/19948337/packages/maven + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt new file mode 100644 index 0000000..271a50a --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplication.kt @@ -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) { + runApplication(*args) +} diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt new file mode 100644 index 0000000..d025eed --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ApplicationConfiguration.kt @@ -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" + } + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt new file mode 100644 index 0000000..237fcdd --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/config/ValidationConfiguration.kt @@ -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? { + val cacheManager: CacheManager = cacheManager() + var cache = + cacheManager.getCache(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? { + val cacheConfig: CompleteConfiguration = MutableConfiguration() + .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 { + val caCertificates = ArrayList() + + 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 { + val caCertificates = ArrayList() + + 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) + } + } + + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt new file mode 100644 index 0000000..9703034 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/dto/ChallengeDto.kt @@ -0,0 +1,3 @@ +package com.tarkvaratehnika.demobackend.dto + +data class ChallengeDto(val nonce : String) \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt new file mode 100644 index 0000000..6b50be8 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTO.kt @@ -0,0 +1,6 @@ +package com.tarkvaratehnika.demobackend.security + +import com.fasterxml.jackson.annotation.JsonProperty + +class AuthTokenDTO (val token : String, val challenge : String) { +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt new file mode 100644 index 0000000..6bdeab7 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/AuthTokenDTOAuthenticationProvider.kt @@ -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() + 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) + } + } + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt new file mode 100644 index 0000000..c87ed42 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/security/WebEidAuthentication.kt @@ -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 +) : PreAuthenticatedAuthenticationToken(principalName, idCode, authorities), Authentication { + + // Companion object is for static functions. + companion object { + + private val loggedInUsers = HashMap() + + fun fromCertificate( + userCertificate: X509Certificate, + authorities: ArrayList, + 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() +// 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)) + } + } + + +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt new file mode 100644 index 0000000..e4c6535 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/LoginController.kt @@ -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" + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt new file mode 100644 index 0000000..bcc12ad --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/SignatureController.kt @@ -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" + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt new file mode 100644 index 0000000..dc93dd0 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/AuthenticationController.kt @@ -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 + } +} \ No newline at end of file diff --git a/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt new file mode 100644 index 0000000..ae29d63 --- /dev/null +++ b/demoBackend/src/main/kotlin/com/tarkvaratehnika/demobackend/web/rest/ChallengeController.kt @@ -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 + } + +} + diff --git a/demoBackend/src/main/resources/application.properties b/demoBackend/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demoBackend/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer b/demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer new file mode 100644 index 0000000..b166955 Binary files /dev/null and b/demoBackend/src/main/resources/certs/ESTEID-SK_2015.cer differ diff --git a/demoBackend/src/main/resources/certs/ESTEID2018.cer b/demoBackend/src/main/resources/certs/ESTEID2018.cer new file mode 100644 index 0000000..6200181 Binary files /dev/null and b/demoBackend/src/main/resources/certs/ESTEID2018.cer differ diff --git a/demoBackend/src/main/resources/certs/trusted_certificates.jks b/demoBackend/src/main/resources/certs/trusted_certificates.jks new file mode 100644 index 0000000..0276f80 Binary files /dev/null and b/demoBackend/src/main/resources/certs/trusted_certificates.jks differ diff --git a/demoBackend/src/main/resources/static/css/main.css b/demoBackend/src/main/resources/static/css/main.css new file mode 100644 index 0000000..b28c55e --- /dev/null +++ b/demoBackend/src/main/resources/static/css/main.css @@ -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; +} \ No newline at end of file diff --git a/demoBackend/src/main/resources/static/js/index.js b/demoBackend/src/main/resources/static/js/index.js new file mode 100644 index 0000000..083655f --- /dev/null +++ b/demoBackend/src/main/resources/static/js/index.js @@ -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); + }) + } +} + diff --git a/demoBackend/src/main/resources/static/js/main.js b/demoBackend/src/main/resources/static/js/main.js new file mode 100644 index 0000000..4e2f7f0 --- /dev/null +++ b/demoBackend/src/main/resources/static/js/main.js @@ -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); +} \ No newline at end of file diff --git a/demoBackend/src/main/resources/templates/index.html b/demoBackend/src/main/resources/templates/index.html new file mode 100644 index 0000000..e6ae490 --- /dev/null +++ b/demoBackend/src/main/resources/templates/index.html @@ -0,0 +1,38 @@ + + + + Login + + + + + + + + + + + +
+

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.

+
Make sure you've installed the authentication app from: GitHub
+ + +
+ + \ No newline at end of file diff --git a/demoBackend/src/main/resources/templates/signature.html b/demoBackend/src/main/resources/templates/signature.html new file mode 100644 index 0000000..0c8ce1c --- /dev/null +++ b/demoBackend/src/main/resources/templates/signature.html @@ -0,0 +1,42 @@ + + + + Login + + + + + + + + + + +
+

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.

+
This page is still WIP, signing a document feature will be implemented later.
+
+
+ + +
+
+ +
+ + \ No newline at end of file diff --git a/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt b/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt new file mode 100644 index 0000000..9e9ca3a --- /dev/null +++ b/demoBackend/src/test/kotlin/com/tarkvaratehnika/demobackend/DemoBackendApplicationTests.kt @@ -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() { + } + +}