diff --git a/api-webapp/pom.xml b/api-webapp/pom.xml
index 4e54e7a..9948f76 100644
--- a/api-webapp/pom.xml
+++ b/api-webapp/pom.xml
@@ -65,6 +65,7 @@
${pairgoth.api.port}
+ ${pairgoth.auth}
${pairgoth.env}
${pairgoth.version}
${pairgoth.api.external.url}
@@ -164,6 +165,11 @@
2.13.0
+
+ com.github.ben-manes.caffeine
+ caffeine
+ 3.1.8
+
commons-codec
commons-codec
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
index 4d84c5c..5bd1047 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiHandler.kt
@@ -21,7 +21,7 @@ interface ApiHandler {
notImplemented()
}
- fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
+ fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented()
}
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/AuthChallenge.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/AuthChallenge.kt
deleted file mode 100644
index 72e0d98..0000000
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/AuthChallenge.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.jeudego.pairgoth.api
-
-class AuthChallenge {
- companion object {
- private val validChars = ('a'..'z') + ('A'..'Z') + ('0'..'9')
- private fun randomString(length: Int) = CharArray(length) { validChars.random() }.concatToString()
- private val lifespan = 30000L
- }
- private val _value = randomString(64)
- private val _gen = System.currentTimeMillis()
-
- val value get() =
- if (System.currentTimeMillis() - _gen > lifespan) null
- else _value
-}
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt
index 84a02ea..3c105ee 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt
@@ -32,7 +32,7 @@ object PairingHandler: PairgothApiHandler {
)
}
- override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
+ override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
index 26f7b14..c4e1e10 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PlayerHandler.kt
@@ -19,7 +19,7 @@ object PlayerHandler: PairgothApiHandler {
}
}
- override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
+ override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val payload = getObjectPayload(request)
val player = Player.fromJson(payload)
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt
index 51aaed6..0f6ce87 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TeamHandler.kt
@@ -19,7 +19,7 @@ object TeamHandler: PairgothApiHandler {
}
}
- override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
+ override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val payload = getObjectPayload(request)
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt
index a4d6e94..d6ef4a9 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt
@@ -1,55 +1,120 @@
package org.jeudego.pairgoth.api
+import com.github.benmanes.caffeine.cache.Cache
+import com.github.benmanes.caffeine.cache.Caffeine
import com.republicate.kson.Json
+import org.jeudego.pairgoth.server.ApiServlet
import org.jeudego.pairgoth.util.AESCryptograph
import org.jeudego.pairgoth.util.Cryptograph
+import org.jeudego.pairgoth.util.Randomizer
+import org.jeudego.pairgoth.web.sharedSecret
+import java.util.Random
+import java.util.concurrent.TimeUnit
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
-import javax.servlet.http.HttpSession
-class TokenHandler: ApiHandler {
- companion object {
- const val AUTH_KEY = "pairgoth-auth"
- const val CHALLENGE_KEY = "pairgoth-challenge"
- private val cryptograph: Cryptograph = AESCryptograph().apply {
- init("78659783ed8ccc0e")
+object TokenHandler: ApiHandler {
+
+ const val AUTH_HEADER = "Authorization"
+ const val AUTH_PREFIX = "Bearer"
+
+ private val cryptograph = AESCryptograph().apply { init(sharedSecret) }
+
+ private data class AuthorizationPayload(
+ val sessionId: String,
+ val accessKey: String,
+ val userInfos: Json
+ )
+
+ private fun getAuthorizationPayload(request: HttpServletRequest): AuthorizationPayload? {
+ val authorize = request.getHeader(AUTH_HEADER) as String?
+ if (authorize != null && authorize.startsWith("$AUTH_PREFIX ")) {
+ val bearer = authorize.substring(AUTH_PREFIX.length + 1)
+ val clear = cryptograph.webDecrypt(bearer)
+ val parts = clear.split(':')
+ if (parts.size == 2) {
+ val sessionId = parts[0]
+ val accessKey = parts[1]
+ val accessPayload = accesses.getIfPresent(accessKey)
+ if (accessPayload != null && sessionId == accessPayload.getString("session")) {
+ return AuthorizationPayload(sessionId, accessKey, accessPayload)
+ }
+ }
}
+ return null
}
+
+ fun getLoggedUser(request: HttpServletRequest) = getAuthorizationPayload(request)?.userInfos
+
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
- val auth = request.session.getAttribute(AUTH_KEY) as String?
- if (auth == null) {
+ if (getLoggedUser(request) == null) {
failed(request, response)
return null
} else {
- return Json.Object(
- "success" to true,
- "auth" to auth
- )
- }
- }
- override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
- val auth = getObjectPayload(request)
- val answer = auth.getString("answer")
- val challenge = request.session.getAttribute(CHALLENGE_KEY) as AuthChallenge?
- if (answer == null || challenge == null) {
- failed(request, response)
- } else {
- val parts = cryptograph.webDecrypt(answer).split(":")
- if (parts.size != 2)
+ return Json.Object("success" to true)
}
}
+ override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
+ val auth = getObjectPayload(request)
+ val session = auth.getString("session")
+ val challenge = challenges.getIfPresent(session)
+ challenges.invalidate(session)
+ if (challenge != null) {
+ val email = auth.getString("email")
+ val signature = auth.getString("signature")
+ val expectedSignature = cryptograph.webEncrypt(
+ "${
+ session
+ }:${
+ challenge
+ }:${
+ email
+ }"
+ )
+ if (signature == expectedSignature) {
+ val accessKey = Randomizer.randomString(32)
+ accesses.put(accessKey, Json.Object(
+ "session" to session,
+ "email" to email
+ ))
+ return Json.Object("token" to accessKey)
+ }
+ }
+ failed(request, response)
+ return null
+ }
+
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
- request.session.removeAttribute(AUTH_KEY)
+ getAuthorizationPayload(request)?.let { payload ->
+ accesses.invalidate(payload.accessKey)
+ }
return Json.Object("success" to true)
}
private fun failed(request: HttpServletRequest, response: HttpServletResponse) {
- val session = request.session
- val challenge = AuthChallenge()
- session.setAttribute(CHALLENGE_KEY, challenge)
- response.addHeader("WWW-Authenticate", challenge.value)
- response.status = HttpServletResponse.SC_UNAUTHORIZED
- response.writer.println(Json.Object("status" to "failed", "message" to "unauthorized"))
+ val authPayload = getAuthorizationPayload(request)
+ if (authPayload != null && authPayload.sessionId.isNotEmpty()) {
+ val challenge = Randomizer.randomString(32)
+ challenges.put(authPayload.sessionId, challenge)
+ response.addHeader("WWW-Authenticate", challenge)
+ response.status = HttpServletResponse.SC_UNAUTHORIZED
+ response.writer.println(Json.Object("status" to "failed", "message" to "unauthorized"))
+ } else {
+ response.status = HttpServletResponse.SC_BAD_REQUEST
+ }
}
+
+ // a short-lived cache for sessionid <--> challenge association
+ private val challenges: Cache = Caffeine.newBuilder()
+ .expireAfterWrite(1, TimeUnit.MINUTES)
+ .maximumSize(100)
+ .build()
+
+ // a long-lived cache for access key <--> user association
+ private val accesses: Cache = Caffeine.newBuilder()
+ .expireAfterWrite(30, TimeUnit.DAYS)
+ .maximumSize(100)
+ .build()
+
}
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt
index a50b37f..d9abce3 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt
@@ -36,7 +36,7 @@ object TournamentHandler: PairgothApiHandler {
}
}
- override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
+ override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
is Element -> OpenGotha.import(payload)
diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt
index f37dbb1..df6ba1c 100644
--- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt
+++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt
@@ -1,5 +1,7 @@
package org.jeudego.pairgoth.server
+import com.github.benmanes.caffeine.cache.Cache
+import com.github.benmanes.caffeine.cache.Caffeine
import com.republicate.kson.Json
import org.apache.commons.io.input.BOMInputStream
import org.jeudego.pairgoth.api.ApiHandler
@@ -8,24 +10,29 @@ import org.jeudego.pairgoth.api.PlayerHandler
import org.jeudego.pairgoth.api.ResultsHandler
import org.jeudego.pairgoth.api.StandingsHandler
import org.jeudego.pairgoth.api.TeamHandler
+import org.jeudego.pairgoth.api.TokenHandler
import org.jeudego.pairgoth.api.TournamentHandler
+import org.jeudego.pairgoth.util.AESCryptograph
import org.jeudego.pairgoth.util.Colorizer.blue
import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.Colorizer.red
import org.jeudego.pairgoth.util.XmlUtils
import org.jeudego.pairgoth.util.parse
import org.jeudego.pairgoth.util.toString
+import org.jeudego.pairgoth.web.sharedSecret
import org.slf4j.LoggerFactory
import org.w3c.dom.Element
import java.io.IOException
import java.io.InputStreamReader
import java.util.*
+import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
+
class ApiServlet: HttpServlet() {
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
@@ -49,7 +56,11 @@ class ApiServlet: HttpServlet() {
val requestLock = if (request.method == "GET") lock.readLock() else lock.writeLock()
try {
requestLock.lock()
- doProtectedRequest(request, response)
+ if (checkAuthorization(request, response)) {
+ doProtectedRequest(request, response)
+ } else {
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
+ }
} finally {
requestLock.unlock()
}
@@ -85,7 +96,8 @@ class ApiServlet: HttpServlet() {
val handler = when (entity) {
"tour" ->
- when (subEntity) {
+ if ("token" == selector) TokenHandler
+ else when (subEntity) {
null -> TournamentHandler
"part" -> PlayerHandler
"pair" -> PairingHandler
@@ -274,11 +286,19 @@ class ApiServlet: HttpServlet() {
}
}
+ private fun checkAuthorization(request: HttpServletRequest, response: HttpServletResponse): Boolean {
+ val auth = WebappManager.getMandatoryProperty("auth")
+ return auth == "none" ||
+ "/api/tour/token" == request.requestURI ||
+ TokenHandler.getLoggedUser(request)?.also {
+ request.setAttribute(USER_KEY, it)
+ } != null
+ }
+
companion object {
private var logger = LoggerFactory.getLogger("api")
private const val EXPECTED_CHARSET = "utf8"
- const val AUTH_HEADER = "Authorization"
- const val AUTH_PREFIX = "Bearer"
+ const val USER_KEY = "pairgoth-user"
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
}
diff --git a/client.sh b/client.sh
index c9801a1..c9af334 100755
--- a/client.sh
+++ b/client.sh
@@ -1,10 +1,6 @@
-#!/bin/bash
+#!/bin/sh
-trap 'kill $CSSWATCH; exit' INT
-( cd view-webapp; ./csswatch.sh ) &
-CSSWATCH=$!
+# debug version
+# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
-export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5006"
-#mvn --projects view-webapp -Dpairgoth.api.url=http://localhost:8085/api/ package jetty:run
-mvn -DskipTests=true --projects view-webapp package jetty:run
-kill $CSSWATCH
+mvn -DskipTests=true package && java -Dpairgoth.mode=client -jar application/target/pairgoth-engine.jar
diff --git a/debug-client.sh b/debug-client.sh
new file mode 100755
index 0000000..ad70934
--- /dev/null
+++ b/debug-client.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+trap 'kill $CSSWATCH; exit' INT
+( cd view-webapp; ./csswatch.sh ) &
+CSSWATCH=$!
+
+export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5006"
+#mvn --projects view-webapp -Dpairgoth.api.url=http://localhost:8085/api/ package jetty:run
+mvn -DskipTests=true --projects view-webapp package jetty:run -Dpairgoth.mode=client
+kill $CSSWATCH
diff --git a/debug-server.sh b/debug-server.sh
new file mode 100755
index 0000000..d6b0bcf
--- /dev/null
+++ b/debug-server.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
+mvn -DskipTests=true --projects api-webapp package jetty:run -Dpairgoth.mode=server
diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt
index 691fde9..e76c1d6 100644
--- a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt
+++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt
@@ -52,6 +52,5 @@ class AESCryptograph : Cryptograph {
companion object {
private val CIPHER = "AES/ECB/PKCS5Padding"
private val ALGORITHM = "AES"
-
}
}
diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Randomizer.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Randomizer.kt
new file mode 100644
index 0000000..8a264f7
--- /dev/null
+++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Randomizer.kt
@@ -0,0 +1,6 @@
+package org.jeudego.pairgoth.util
+
+object Randomizer {
+ private val validChars = ('a'..'z') + ('A'..'Z') + ('0'..'9')
+ fun randomString(length: Int) = CharArray(length) { validChars.random() }.concatToString()
+}
diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Shared.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Shared.kt
new file mode 100644
index 0000000..44b907c
--- /dev/null
+++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Shared.kt
@@ -0,0 +1,18 @@
+package org.jeudego.pairgoth.web
+
+import org.jeudego.pairgoth.util.Randomizer
+import java.lang.RuntimeException
+
+
+// a randomly generated secret shared by the API and View webapps
+val sharedSecret: String by lazy {
+ BaseWebappManager.properties.getProperty("auth.shared_secret") ?: when (BaseWebappManager.properties.getProperty("mode")) {
+ "standalone" -> Randomizer.randomString(16)
+ else -> when (BaseWebappManager.properties.getProperty("auth")) {
+ "none" -> " ".repeat(16)
+ else -> throw RuntimeException("missing property auth.shared_secret")
+ }
+ }.also {
+ if (it.length != 16) throw RuntimeException("shared secret must be 16 ascii chars long")
+ }
+}
diff --git a/pom.xml b/pom.xml
index 15348a1..50264e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,17 +71,17 @@
localhost
8080
/
-
${pairgoth.webapp.protocol}://${pairgoth.webapp.host}:${pairgoth.webapp.port}${pairgoth.webapp.context}
http
localhost
8085
/api
${pairgoth.api.protocol}://${pairgoth.api.host}:${pairgoth.api.port}${pairgoth.api.context}
+ standalone
file
tournamentfiles
none
- this_should_be_overriden_with_a_command_line_option
+ this_should_be_overriden
pairtogh
diff --git a/server.sh b/server.sh
index eeccd40..3088fa4 100755
--- a/server.sh
+++ b/server.sh
@@ -1,4 +1,6 @@
-#!/bin/bash
+#!/bin/sh
-export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
-mvn -DskipTests=true --projects api-webapp package jetty:run
+# debug version
+# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
+
+mvn -DskipTests=true package && java -Dpairgoth.mode=server -jar application/target/pairgoth-engine.jar
diff --git a/standalone.sh b/standalone.sh
index 8571b14..8d12f47 100755
--- a/standalone.sh
+++ b/standalone.sh
@@ -3,4 +3,4 @@
# debug version
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
-mvn -DskipTests=true package && java -jar application/target/pairgoth-engine.jar
+mvn -DskipTests=true package && java -Dpairgoth.mode=standalone -jar application/target/pairgoth-engine.jar
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/oauth/OAuthHelper.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/oauth/OAuthHelper.kt
index 27665cc..5b28182 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/oauth/OAuthHelper.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/oauth/OAuthHelper.kt
@@ -10,6 +10,7 @@ import org.apache.http.NameValuePair
import org.jeudego.pairgoth.util.AESCryptograph
import org.jeudego.pairgoth.util.ApiClient.JsonApiClient
import org.jeudego.pairgoth.util.Cryptograph
+import org.jeudego.pairgoth.util.Randomizer
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.IOException
@@ -55,7 +56,7 @@ abstract class OAuthHelper {
companion object {
protected var logger: Logger = LoggerFactory.getLogger("oauth")
private val cryptograph: Cryptograph = AESCryptograph().apply {
- init("0efd28fb53cbac42")
+ init(Randomizer.randomString(16))
}
}
}
\ No newline at end of file
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/CredentialsChecker.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/CredentialsChecker.kt
index 99bffff..a9cdf69 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/CredentialsChecker.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/CredentialsChecker.kt
@@ -15,11 +15,11 @@ object CredentialsChecker {
val sha256 = hasher.digest(password.toByteArray(StandardCharsets.UTF_8)).toHexString()
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
val rs =
- conn.prepareStatement("SELECT 1 FROM cred WHERE email = ? AND password = ?").apply {
+ conn.prepareStatement("SELECT id FROM cred WHERE email = ? AND password = ?").apply {
setString(1, email)
setString(2, password)
}.executeQuery()
- return if (rs.next()) Json.Object("email" to email) else null
+ return if (rs.next()) Json.Object("id" to "${rs.getInt("id")}", "email" to email) else null
}
}
@@ -27,7 +27,7 @@ object CredentialsChecker {
fun initDatabase() {
if (!File(CREDENTIALS_DB).exists()) {
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
- conn.createStatement().executeUpdate("CREATE TABLE cred (email VARCHAR(200) UNIQUE NOT NULL, password VARCHAR(200) NOT NULL)")
+ conn.createStatement().executeUpdate("CREATE TABLE cred (id INTEGER PRIMARY KEY AUTOINCREMENT, email VARCHAR(200) UNIQUE NOT NULL, password VARCHAR(200) NOT NULL)")
}
}
}
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt
index 5a66066..7bcf508 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt
@@ -6,8 +6,10 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.internal.EMPTY_REQUEST
+import org.jeudego.pairgoth.web.AuthFilter
import org.jeudego.pairgoth.web.WebappManager
import org.slf4j.LoggerFactory
+import javax.servlet.http.HttpServletRequest
class ApiTool {
companion object {
@@ -19,8 +21,18 @@ class ApiTool {
}
val logger = LoggerFactory.getLogger("api")
}
+ private lateinit var request: HttpServletRequest
+ fun setRequest(req: HttpServletRequest) {
+ request = req
+ }
+ private fun getBearer() = AuthFilter.getBearer(request)
+
private val client = OkHttpClient()
- private fun prepare(url: String) = Request.Builder().url("$apiRoot$url").header("Accept", JSON)
+ private fun prepare(url: String) =
+ Request.Builder().url("$apiRoot$url")
+ .header("Accept", JSON)
+ .header("Authorization", "Bearer ${getBearer()}")
+
private fun Json.toRequestBody() = toString().toRequestBody(JSON.toMediaType())
private fun Request.Builder.process(): Json {
try {
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
index eeb5c61..c1a21b5 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt
@@ -2,12 +2,15 @@ package org.jeudego.pairgoth.web
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.proxy.AsyncProxyServlet;
+import org.jeudego.pairgoth.view.ApiTool
import javax.servlet.http.HttpServletRequest
class ApiServlet : AsyncProxyServlet() {
override fun addProxyHeaders(clientRequest: HttpServletRequest, proxyRequest: Request) {
- // proxyRequest.header("X-EGC-User", some user id...)
+ AuthFilter.getBearer(clientRequest)?.let { bearer ->
+ proxyRequest.header("Authorization", "Bearer $bearer")
+ }
}
override fun rewriteTarget(clientRequest: HttpServletRequest): String {
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt
index 3fbbfb6..54cac0e 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt
@@ -1,6 +1,16 @@
package org.jeudego.pairgoth.web
+import com.republicate.kson.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
import org.jeudego.pairgoth.oauth.OauthHelperFactory
+import org.jeudego.pairgoth.util.AESCryptograph
+import org.jeudego.pairgoth.view.ApiTool
+import org.slf4j.LoggerFactory
+import java.nio.charset.StandardCharsets
+import java.security.MessageDigest
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
@@ -36,12 +46,13 @@ class AuthFilter: Filter {
val helper = OauthHelperFactory.getHelper(provider)
val accessToken = helper.getAccessToken(request.session.id, request.getParameter("code") ?: "")
val user = helper.getUserInfos(accessToken)
- request.session.setAttribute("logged", user)
+ handleSuccessfulLogin(req, user)
+ request.session.setAttribute(SESSION_KEY_USER, user)
response.sendRedirect("/index")
return
}
- if (auth == "none" || whitelisted(uri) || forwarded || session?.getAttribute("logged") != null) {
+ if (auth == "none" || whitelisted(uri) || forwarded || session?.getAttribute(SESSION_KEY_USER) != null) {
chain.doFilter(req, resp)
} else {
// TODO - protection against brute force attacks
@@ -54,6 +65,14 @@ class AuthFilter: Filter {
}
companion object {
+ const val SESSION_KEY_USER = "logged"
+ const val SESSION_KEY_API_TOKEN = "pairgoth-api-token"
+
+ private val logger = LoggerFactory.getLogger("auth")
+ private val cryptograph = AESCryptograph().apply { init(sharedSecret) }
+ private val hasher = MessageDigest.getInstance("SHA-256")
+ private val client = OkHttpClient()
+
private val whitelist = setOf(
"/login",
"/index-ffg",
@@ -66,5 +85,66 @@ class AuthFilter: Filter {
val nolangUri = uri.replace(Regex("^/../"), "/")
return whitelist.contains(nolangUri)
}
+
+ fun handleSuccessfulLogin(req: HttpServletRequest, user: Json.Object) {
+ logger.info("successful login for $user")
+ req.session.setAttribute(SESSION_KEY_USER, user)
+ fetchApiToken(req, user)?.also { token ->
+ req.session.setAttribute(SESSION_KEY_API_TOKEN, token)
+ }
+ }
+
+ fun fetchApiToken(req: HttpServletRequest, user: Json.Object): String? {
+ val challengeReq = Request.Builder().url("${ApiTool.apiRoot}tour/token")
+ .header("Authorization", "Bearer ${getBearer(req)}")
+ .build()
+ val challengeResp = client.newCall(challengeReq).execute()
+ if (challengeResp.code == HttpServletResponse.SC_UNAUTHORIZED) {
+ val email = user.getString("email") ?: "-"
+ val challenge = challengeResp.headers["WWW-Authenticate"]
+ if (challenge != null) {
+ val signature = hasher.digest(
+ "${
+ req.session.id
+ }:${
+ challenge
+ }:${
+ email
+ }".toByteArray(StandardCharsets.UTF_8))
+ val answer = Json.Object(
+ "session" to req.session.id,
+ "email" to email,
+ "signature" to signature
+ )
+ val answerReq = Request.Builder().url("${ApiTool.apiRoot}tour/token").post(
+ answer.toString().toRequestBody(ApiTool.JSON.toMediaType())
+ ).build()
+ val answerResp = client.newCall(answerReq).execute()
+ if (answerResp.isSuccessful && "json" == answerResp.body?.contentType()?.subtype) {
+ val payload = Json.parse(answerResp.body!!.string())
+ if (payload != null && payload.isObject) {
+ val token = payload.asObject().getString("token")
+ if (token != null) return token
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ fun clearApiToken(req: HttpServletRequest) {
+ val deleteTokenReq = Request.Builder().url("${ApiTool.apiRoot}tour/token").delete().build()
+ client.newCall(deleteTokenReq).execute()
+ }
+
+ fun getBearer(req: HttpServletRequest): String {
+ val session = req.session
+ return cryptograph.webEncrypt(
+ "${
+ session.id
+ }:${
+ session.getAttribute(SESSION_KEY_API_TOKEN) ?: ""
+ }")
+ }
}
}
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt
index f301b90..36bbd80 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt
@@ -20,7 +20,7 @@ class LoginServlet: HttpServlet() {
"sesame" -> checkSesame(payload)
else -> checkLoginPass(payload)
} ?: throw Error("authentication failed")
- req.session.setAttribute("logged", user)
+ AuthFilter.handleSuccessfulLogin(req, user)
val ret = Json.Object("status" to "ok")
resp.contentType = "application/json"
resp.writer.println(ret.toString())
@@ -34,7 +34,7 @@ class LoginServlet: HttpServlet() {
fun checkSesame(payload: Json.Object): Json.Object? {
val expected = WebappManager.properties.getProperty("auth.sesame") ?: throw Error("sesame wrongly configured")
- return if (payload.getString("sesame")?.equals(expected) == true) Json.Object("logged" to true) else null
+ return if (payload.getString("sesame")?.equals(expected) == true) Json.Object(AuthFilter.SESSION_KEY_USER to true) else null
}
fun checkLoginPass(payload: Json.Object): Json.Object? {
diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LogoutServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LogoutServlet.kt
index 8ed8075..88e1052 100644
--- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LogoutServlet.kt
+++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LogoutServlet.kt
@@ -1,6 +1,8 @@
package org.jeudego.pairgoth.web
import com.republicate.kson.Json
+import okhttp3.Request
+import org.jeudego.pairgoth.view.ApiTool
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
@@ -9,7 +11,9 @@ import javax.servlet.http.HttpServletResponse
class LogoutServlet: HttpServlet() {
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
- req.session.removeAttribute("logged")
+ AuthFilter.clearApiToken(req)
+ req.session.removeAttribute(AuthFilter.SESSION_KEY_USER)
+ req.session.removeAttribute(AuthFilter.SESSION_KEY_API_TOKEN)
val ret = Json.Object("status" to "ok")
resp.contentType = "application/json"
resp.writer.println(ret.toString())
diff --git a/view-webapp/src/main/webapp/js/api.js b/view-webapp/src/main/webapp/js/api.js
index 0fd329f..5b0e08f 100644
--- a/view-webapp/src/main/webapp/js/api.js
+++ b/view-webapp/src/main/webapp/js/api.js
@@ -19,9 +19,8 @@ let headers = function(withJson) {
if (withJson) {
ret['Content-Type'] = 'application/json';
}
- let accessToken = store('accessToken');
- if (accessToken) {
- ret['Authorization'] = `Bearer ${accessToken}`;
+ if (apiToken) {
+ ret['Authorization'] = `Bearer ${apiToken}`;
}
return ret;
};
diff --git a/view-webapp/src/main/webapp/tour.html b/view-webapp/src/main/webapp/tour.html
index 897c1c9..fff7b30 100644
--- a/view-webapp/src/main/webapp/tour.html
+++ b/view-webapp/src/main/webapp/tour.html
@@ -45,6 +45,7 @@
Required field