Auth still in progress
This commit is contained in:
@@ -65,6 +65,7 @@
|
|||||||
<port>${pairgoth.api.port}</port>
|
<port>${pairgoth.api.port}</port>
|
||||||
</httpConnector>
|
</httpConnector>
|
||||||
<systemProperties>
|
<systemProperties>
|
||||||
|
<pairgoth.auth>${pairgoth.auth}</pairgoth.auth>
|
||||||
<pairgoth.env>${pairgoth.env}</pairgoth.env>
|
<pairgoth.env>${pairgoth.env}</pairgoth.env>
|
||||||
<pairgoth.version>${pairgoth.version}</pairgoth.version>
|
<pairgoth.version>${pairgoth.version}</pairgoth.version>
|
||||||
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
|
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
|
||||||
@@ -164,6 +165,11 @@
|
|||||||
<version>2.13.0</version>
|
<version>2.13.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- auth -->
|
<!-- auth -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>3.1.8</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-codec</groupId>
|
<groupId>commons-codec</groupId>
|
||||||
<artifactId>commons-codec</artifactId>
|
<artifactId>commons-codec</artifactId>
|
||||||
|
@@ -21,7 +21,7 @@ interface ApiHandler {
|
|||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
|
fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
}
|
|
@@ -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 tournament = getTournament(request)
|
||||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
||||||
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
|
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
|
||||||
|
@@ -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 tournament = getTournament(request)
|
||||||
val payload = getObjectPayload(request)
|
val payload = getObjectPayload(request)
|
||||||
val player = Player.fromJson(payload)
|
val player = Player.fromJson(payload)
|
||||||
|
@@ -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)
|
val tournament = getTournament(request)
|
||||||
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
|
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
|
||||||
val payload = getObjectPayload(request)
|
val payload = getObjectPayload(request)
|
||||||
|
@@ -1,55 +1,120 @@
|
|||||||
package org.jeudego.pairgoth.api
|
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 com.republicate.kson.Json
|
||||||
|
import org.jeudego.pairgoth.server.ApiServlet
|
||||||
import org.jeudego.pairgoth.util.AESCryptograph
|
import org.jeudego.pairgoth.util.AESCryptograph
|
||||||
import org.jeudego.pairgoth.util.Cryptograph
|
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.HttpServletRequest
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse
|
||||||
import javax.servlet.http.HttpSession
|
|
||||||
|
|
||||||
class TokenHandler: ApiHandler {
|
object TokenHandler: ApiHandler {
|
||||||
companion object {
|
|
||||||
const val AUTH_KEY = "pairgoth-auth"
|
const val AUTH_HEADER = "Authorization"
|
||||||
const val CHALLENGE_KEY = "pairgoth-challenge"
|
const val AUTH_PREFIX = "Bearer"
|
||||||
private val cryptograph: Cryptograph = AESCryptograph().apply {
|
|
||||||
init("78659783ed8ccc0e")
|
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? {
|
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||||
val auth = request.session.getAttribute(AUTH_KEY) as String?
|
if (getLoggedUser(request) == null) {
|
||||||
if (auth == null) {
|
|
||||||
failed(request, response)
|
failed(request, response)
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
return Json.Object(
|
return Json.Object("success" to true)
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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)
|
return Json.Object("success" to true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun failed(request: HttpServletRequest, response: HttpServletResponse) {
|
private fun failed(request: HttpServletRequest, response: HttpServletResponse) {
|
||||||
val session = request.session
|
val authPayload = getAuthorizationPayload(request)
|
||||||
val challenge = AuthChallenge()
|
if (authPayload != null && authPayload.sessionId.isNotEmpty()) {
|
||||||
session.setAttribute(CHALLENGE_KEY, challenge)
|
val challenge = Randomizer.randomString(32)
|
||||||
response.addHeader("WWW-Authenticate", challenge.value)
|
challenges.put(authPayload.sessionId, challenge)
|
||||||
|
response.addHeader("WWW-Authenticate", challenge)
|
||||||
response.status = HttpServletResponse.SC_UNAUTHORIZED
|
response.status = HttpServletResponse.SC_UNAUTHORIZED
|
||||||
response.writer.println(Json.Object("status" to "failed", "message" to "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<String, String> = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(1, TimeUnit.MINUTES)
|
||||||
|
.maximumSize(100)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// a long-lived cache for access key <--> user association
|
||||||
|
private val accesses: Cache<String, Json.Object> = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(30, TimeUnit.DAYS)
|
||||||
|
.maximumSize(100)
|
||||||
|
.build()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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)) {
|
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
|
||||||
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
||||||
is Element -> OpenGotha.import(payload)
|
is Element -> OpenGotha.import(payload)
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package org.jeudego.pairgoth.server
|
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 com.republicate.kson.Json
|
||||||
import org.apache.commons.io.input.BOMInputStream
|
import org.apache.commons.io.input.BOMInputStream
|
||||||
import org.jeudego.pairgoth.api.ApiHandler
|
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.ResultsHandler
|
||||||
import org.jeudego.pairgoth.api.StandingsHandler
|
import org.jeudego.pairgoth.api.StandingsHandler
|
||||||
import org.jeudego.pairgoth.api.TeamHandler
|
import org.jeudego.pairgoth.api.TeamHandler
|
||||||
|
import org.jeudego.pairgoth.api.TokenHandler
|
||||||
import org.jeudego.pairgoth.api.TournamentHandler
|
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.blue
|
||||||
import org.jeudego.pairgoth.util.Colorizer.green
|
import org.jeudego.pairgoth.util.Colorizer.green
|
||||||
import org.jeudego.pairgoth.util.Colorizer.red
|
import org.jeudego.pairgoth.util.Colorizer.red
|
||||||
import org.jeudego.pairgoth.util.XmlUtils
|
import org.jeudego.pairgoth.util.XmlUtils
|
||||||
import org.jeudego.pairgoth.util.parse
|
import org.jeudego.pairgoth.util.parse
|
||||||
import org.jeudego.pairgoth.util.toString
|
import org.jeudego.pairgoth.util.toString
|
||||||
|
import org.jeudego.pairgoth.web.sharedSecret
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.locks.ReadWriteLock
|
import java.util.concurrent.locks.ReadWriteLock
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
import javax.servlet.http.HttpServlet
|
import javax.servlet.http.HttpServlet
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
import javax.servlet.http.HttpServletResponse
|
import javax.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
|
|
||||||
class ApiServlet: HttpServlet() {
|
class ApiServlet: HttpServlet() {
|
||||||
|
|
||||||
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
|
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()
|
val requestLock = if (request.method == "GET") lock.readLock() else lock.writeLock()
|
||||||
try {
|
try {
|
||||||
requestLock.lock()
|
requestLock.lock()
|
||||||
|
if (checkAuthorization(request, response)) {
|
||||||
doProtectedRequest(request, response)
|
doProtectedRequest(request, response)
|
||||||
|
} else {
|
||||||
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
requestLock.unlock()
|
requestLock.unlock()
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,8 @@ class ApiServlet: HttpServlet() {
|
|||||||
|
|
||||||
val handler = when (entity) {
|
val handler = when (entity) {
|
||||||
"tour" ->
|
"tour" ->
|
||||||
when (subEntity) {
|
if ("token" == selector) TokenHandler
|
||||||
|
else when (subEntity) {
|
||||||
null -> TournamentHandler
|
null -> TournamentHandler
|
||||||
"part" -> PlayerHandler
|
"part" -> PlayerHandler
|
||||||
"pair" -> PairingHandler
|
"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 {
|
companion object {
|
||||||
private var logger = LoggerFactory.getLogger("api")
|
private var logger = LoggerFactory.getLogger("api")
|
||||||
private const val EXPECTED_CHARSET = "utf8"
|
private const val EXPECTED_CHARSET = "utf8"
|
||||||
const val AUTH_HEADER = "Authorization"
|
const val USER_KEY = "pairgoth-user"
|
||||||
const val AUTH_PREFIX = "Bearer"
|
|
||||||
fun isJson(mimeType: String) = "text/json" == mimeType || "application/json" == mimeType || mimeType.endsWith("+json")
|
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")
|
fun isXml(mimeType: String) = "text/xml" == mimeType || "application/xml" == mimeType || mimeType.endsWith("+xml")
|
||||||
}
|
}
|
||||||
|
12
client.sh
12
client.sh
@@ -1,10 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
trap 'kill $CSSWATCH; exit' INT
|
# debug version
|
||||||
( cd view-webapp; ./csswatch.sh ) &
|
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
|
||||||
CSSWATCH=$!
|
|
||||||
|
|
||||||
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5006"
|
mvn -DskipTests=true package && java -Dpairgoth.mode=client -jar application/target/pairgoth-engine.jar
|
||||||
#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
|
|
||||||
|
10
debug-client.sh
Executable file
10
debug-client.sh
Executable file
@@ -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
|
4
debug-server.sh
Executable file
4
debug-server.sh
Executable file
@@ -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
|
@@ -52,6 +52,5 @@ class AESCryptograph : Cryptograph {
|
|||||||
companion object {
|
companion object {
|
||||||
private val CIPHER = "AES/ECB/PKCS5Padding"
|
private val CIPHER = "AES/ECB/PKCS5Padding"
|
||||||
private val ALGORITHM = "AES"
|
private val ALGORITHM = "AES"
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
|
}
|
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
4
pom.xml
4
pom.xml
@@ -71,17 +71,17 @@
|
|||||||
<pairgoth.webapp.host>localhost</pairgoth.webapp.host>
|
<pairgoth.webapp.host>localhost</pairgoth.webapp.host>
|
||||||
<pairgoth.webapp.port>8080</pairgoth.webapp.port>
|
<pairgoth.webapp.port>8080</pairgoth.webapp.port>
|
||||||
<pairgoth.webapp.context>/</pairgoth.webapp.context>
|
<pairgoth.webapp.context>/</pairgoth.webapp.context>
|
||||||
<!-- CB TODO - what if webapp context does not qtart with '/' ? -->
|
|
||||||
<pairgoth.webapp.external.url>${pairgoth.webapp.protocol}://${pairgoth.webapp.host}:${pairgoth.webapp.port}${pairgoth.webapp.context}</pairgoth.webapp.external.url>
|
<pairgoth.webapp.external.url>${pairgoth.webapp.protocol}://${pairgoth.webapp.host}:${pairgoth.webapp.port}${pairgoth.webapp.context}</pairgoth.webapp.external.url>
|
||||||
<pairgoth.api.protocol>http</pairgoth.api.protocol>
|
<pairgoth.api.protocol>http</pairgoth.api.protocol>
|
||||||
<pairgoth.api.host>localhost</pairgoth.api.host>
|
<pairgoth.api.host>localhost</pairgoth.api.host>
|
||||||
<pairgoth.api.port>8085</pairgoth.api.port>
|
<pairgoth.api.port>8085</pairgoth.api.port>
|
||||||
<pairgoth.api.context>/api</pairgoth.api.context>
|
<pairgoth.api.context>/api</pairgoth.api.context>
|
||||||
<pairgoth.api.external.url>${pairgoth.api.protocol}://${pairgoth.api.host}:${pairgoth.api.port}${pairgoth.api.context}</pairgoth.api.external.url>
|
<pairgoth.api.external.url>${pairgoth.api.protocol}://${pairgoth.api.host}:${pairgoth.api.port}${pairgoth.api.context}</pairgoth.api.external.url>
|
||||||
|
<pairgoth.mode>standalone</pairgoth.mode>
|
||||||
<pairgoth.store>file</pairgoth.store>
|
<pairgoth.store>file</pairgoth.store>
|
||||||
<pairgoth.store.file.path>tournamentfiles</pairgoth.store.file.path>
|
<pairgoth.store.file.path>tournamentfiles</pairgoth.store.file.path>
|
||||||
<pairgoth.auth>none</pairgoth.auth>
|
<pairgoth.auth>none</pairgoth.auth>
|
||||||
<pairgoth.auth.sesame>this_should_be_overriden_with_a_command_line_option</pairgoth.auth.sesame>
|
<pairgoth.auth.sesame>this_should_be_overriden</pairgoth.auth.sesame>
|
||||||
<pairgoth.oauth.ffg.client_id>pairtogh</pairgoth.oauth.ffg.client_id>
|
<pairgoth.oauth.ffg.client_id>pairtogh</pairgoth.oauth.ffg.client_id>
|
||||||
<pairgoth.smtp.sender></pairgoth.smtp.sender>
|
<pairgoth.smtp.sender></pairgoth.smtp.sender>
|
||||||
<pairgoth.smtp.host></pairgoth.smtp.host>
|
<pairgoth.smtp.host></pairgoth.smtp.host>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/sh
|
||||||
|
|
||||||
export MAVEN_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005"
|
# debug version
|
||||||
mvn -DskipTests=true --projects api-webapp package jetty:run
|
# 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
|
||||||
|
@@ -3,4 +3,4 @@
|
|||||||
# debug version
|
# debug version
|
||||||
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
|
# 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
|
||||||
|
@@ -10,6 +10,7 @@ import org.apache.http.NameValuePair
|
|||||||
import org.jeudego.pairgoth.util.AESCryptograph
|
import org.jeudego.pairgoth.util.AESCryptograph
|
||||||
import org.jeudego.pairgoth.util.ApiClient.JsonApiClient
|
import org.jeudego.pairgoth.util.ApiClient.JsonApiClient
|
||||||
import org.jeudego.pairgoth.util.Cryptograph
|
import org.jeudego.pairgoth.util.Cryptograph
|
||||||
|
import org.jeudego.pairgoth.util.Randomizer
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -55,7 +56,7 @@ abstract class OAuthHelper {
|
|||||||
companion object {
|
companion object {
|
||||||
protected var logger: Logger = LoggerFactory.getLogger("oauth")
|
protected var logger: Logger = LoggerFactory.getLogger("oauth")
|
||||||
private val cryptograph: Cryptograph = AESCryptograph().apply {
|
private val cryptograph: Cryptograph = AESCryptograph().apply {
|
||||||
init("0efd28fb53cbac42")
|
init(Randomizer.randomString(16))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -15,11 +15,11 @@ object CredentialsChecker {
|
|||||||
val sha256 = hasher.digest(password.toByteArray(StandardCharsets.UTF_8)).toHexString()
|
val sha256 = hasher.digest(password.toByteArray(StandardCharsets.UTF_8)).toHexString()
|
||||||
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
|
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
|
||||||
val rs =
|
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(1, email)
|
||||||
setString(2, password)
|
setString(2, password)
|
||||||
}.executeQuery()
|
}.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() {
|
fun initDatabase() {
|
||||||
if (!File(CREDENTIALS_DB).exists()) {
|
if (!File(CREDENTIALS_DB).exists()) {
|
||||||
DriverManager.getConnection("jdbc:sqlite:$CREDENTIALS_DB").use { conn ->
|
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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,8 +6,10 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.internal.EMPTY_REQUEST
|
import okhttp3.internal.EMPTY_REQUEST
|
||||||
|
import org.jeudego.pairgoth.web.AuthFilter
|
||||||
import org.jeudego.pairgoth.web.WebappManager
|
import org.jeudego.pairgoth.web.WebappManager
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
class ApiTool {
|
class ApiTool {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -19,8 +21,18 @@ class ApiTool {
|
|||||||
}
|
}
|
||||||
val logger = LoggerFactory.getLogger("api")
|
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 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 Json.toRequestBody() = toString().toRequestBody(JSON.toMediaType())
|
||||||
private fun Request.Builder.process(): Json {
|
private fun Request.Builder.process(): Json {
|
||||||
try {
|
try {
|
||||||
|
@@ -2,12 +2,15 @@ package org.jeudego.pairgoth.web
|
|||||||
|
|
||||||
import org.eclipse.jetty.client.api.Request;
|
import org.eclipse.jetty.client.api.Request;
|
||||||
import org.eclipse.jetty.proxy.AsyncProxyServlet;
|
import org.eclipse.jetty.proxy.AsyncProxyServlet;
|
||||||
|
import org.jeudego.pairgoth.view.ApiTool
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
|
|
||||||
class ApiServlet : AsyncProxyServlet() {
|
class ApiServlet : AsyncProxyServlet() {
|
||||||
|
|
||||||
override fun addProxyHeaders(clientRequest: HttpServletRequest, proxyRequest: Request) {
|
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 {
|
override fun rewriteTarget(clientRequest: HttpServletRequest): String {
|
||||||
|
@@ -1,6 +1,16 @@
|
|||||||
package org.jeudego.pairgoth.web
|
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.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.Filter
|
||||||
import javax.servlet.FilterChain
|
import javax.servlet.FilterChain
|
||||||
import javax.servlet.FilterConfig
|
import javax.servlet.FilterConfig
|
||||||
@@ -36,12 +46,13 @@ class AuthFilter: Filter {
|
|||||||
val helper = OauthHelperFactory.getHelper(provider)
|
val helper = OauthHelperFactory.getHelper(provider)
|
||||||
val accessToken = helper.getAccessToken(request.session.id, request.getParameter("code") ?: "")
|
val accessToken = helper.getAccessToken(request.session.id, request.getParameter("code") ?: "")
|
||||||
val user = helper.getUserInfos(accessToken)
|
val user = helper.getUserInfos(accessToken)
|
||||||
request.session.setAttribute("logged", user)
|
handleSuccessfulLogin(req, user)
|
||||||
|
request.session.setAttribute(SESSION_KEY_USER, user)
|
||||||
response.sendRedirect("/index")
|
response.sendRedirect("/index")
|
||||||
return
|
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)
|
chain.doFilter(req, resp)
|
||||||
} else {
|
} else {
|
||||||
// TODO - protection against brute force attacks
|
// TODO - protection against brute force attacks
|
||||||
@@ -54,6 +65,14 @@ class AuthFilter: Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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(
|
private val whitelist = setOf(
|
||||||
"/login",
|
"/login",
|
||||||
"/index-ffg",
|
"/index-ffg",
|
||||||
@@ -66,5 +85,66 @@ class AuthFilter: Filter {
|
|||||||
val nolangUri = uri.replace(Regex("^/../"), "/")
|
val nolangUri = uri.replace(Regex("^/../"), "/")
|
||||||
return whitelist.contains(nolangUri)
|
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) ?: ""
|
||||||
|
}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ class LoginServlet: HttpServlet() {
|
|||||||
"sesame" -> checkSesame(payload)
|
"sesame" -> checkSesame(payload)
|
||||||
else -> checkLoginPass(payload)
|
else -> checkLoginPass(payload)
|
||||||
} ?: throw Error("authentication failed")
|
} ?: throw Error("authentication failed")
|
||||||
req.session.setAttribute("logged", user)
|
AuthFilter.handleSuccessfulLogin(req, user)
|
||||||
val ret = Json.Object("status" to "ok")
|
val ret = Json.Object("status" to "ok")
|
||||||
resp.contentType = "application/json"
|
resp.contentType = "application/json"
|
||||||
resp.writer.println(ret.toString())
|
resp.writer.println(ret.toString())
|
||||||
@@ -34,7 +34,7 @@ class LoginServlet: HttpServlet() {
|
|||||||
|
|
||||||
fun checkSesame(payload: Json.Object): Json.Object? {
|
fun checkSesame(payload: Json.Object): Json.Object? {
|
||||||
val expected = WebappManager.properties.getProperty("auth.sesame") ?: throw Error("sesame wrongly configured")
|
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? {
|
fun checkLoginPass(payload: Json.Object): Json.Object? {
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package org.jeudego.pairgoth.web
|
package org.jeudego.pairgoth.web
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jeudego.pairgoth.view.ApiTool
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import javax.servlet.http.HttpServlet
|
import javax.servlet.http.HttpServlet
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
@@ -9,7 +11,9 @@ import javax.servlet.http.HttpServletResponse
|
|||||||
class LogoutServlet: HttpServlet() {
|
class LogoutServlet: HttpServlet() {
|
||||||
|
|
||||||
override fun doPost(req: HttpServletRequest, resp: HttpServletResponse) {
|
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")
|
val ret = Json.Object("status" to "ok")
|
||||||
resp.contentType = "application/json"
|
resp.contentType = "application/json"
|
||||||
resp.writer.println(ret.toString())
|
resp.writer.println(ret.toString())
|
||||||
|
@@ -19,9 +19,8 @@ let headers = function(withJson) {
|
|||||||
if (withJson) {
|
if (withJson) {
|
||||||
ret['Content-Type'] = 'application/json';
|
ret['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
let accessToken = store('accessToken');
|
if (apiToken) {
|
||||||
if (accessToken) {
|
ret['Authorization'] = `Bearer ${apiToken}`;
|
||||||
ret['Authorization'] = `Bearer ${accessToken}`;
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
@@ -45,6 +45,7 @@
|
|||||||
<!-- error messages included as html elements so that they are translated -->
|
<!-- error messages included as html elements so that they are translated -->
|
||||||
<div id="required_field" class="hidden">Required field</div>
|
<div id="required_field" class="hidden">Required field</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
const apiToken = '$!api.bearer';
|
||||||
#if($tour)
|
#if($tour)
|
||||||
const tour_id = ${tour.id};
|
const tour_id = ${tour.id};
|
||||||
const tour_rounds = ${tour.rounds};
|
const tour_rounds = ${tour.rounds};
|
||||||
|
Reference in New Issue
Block a user