Auth still in progress
This commit is contained in:
@@ -65,6 +65,7 @@
|
||||
<port>${pairgoth.api.port}</port>
|
||||
</httpConnector>
|
||||
<systemProperties>
|
||||
<pairgoth.auth>${pairgoth.auth}</pairgoth.auth>
|
||||
<pairgoth.env>${pairgoth.env}</pairgoth.env>
|
||||
<pairgoth.version>${pairgoth.version}</pairgoth.version>
|
||||
<pairgoth.api.external.url>${pairgoth.api.external.url}</pairgoth.api.external.url>
|
||||
@@ -164,6 +165,11 @@
|
||||
<version>2.13.0</version>
|
||||
</dependency>
|
||||
<!-- auth -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.1.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
|
@@ -21,7 +21,7 @@ interface ApiHandler {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
|
||||
fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
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 round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
||||
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 payload = getObjectPayload(request)
|
||||
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)
|
||||
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
|
||||
val payload = getObjectPayload(request)
|
||||
|
@@ -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)
|
||||
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<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)) {
|
||||
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
||||
is Element -> OpenGotha.import(payload)
|
||||
|
@@ -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()
|
||||
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")
|
||||
}
|
||||
|
12
client.sh
12
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
|
||||
|
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 {
|
||||
private val CIPHER = "AES/ECB/PKCS5Padding"
|
||||
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.port>8080</pairgoth.webapp.port>
|
||||
<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.api.protocol>http</pairgoth.api.protocol>
|
||||
<pairgoth.api.host>localhost</pairgoth.api.host>
|
||||
<pairgoth.api.port>8085</pairgoth.api.port>
|
||||
<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.mode>standalone</pairgoth.mode>
|
||||
<pairgoth.store>file</pairgoth.store>
|
||||
<pairgoth.store.file.path>tournamentfiles</pairgoth.store.file.path>
|
||||
<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.smtp.sender></pairgoth.smtp.sender>
|
||||
<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"
|
||||
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
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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) ?: ""
|
||||
}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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? {
|
||||
|
@@ -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())
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -45,6 +45,7 @@
|
||||
<!-- error messages included as html elements so that they are translated -->
|
||||
<div id="required_field" class="hidden">Required field</div>
|
||||
<script type="text/javascript">
|
||||
const apiToken = '$!api.bearer';
|
||||
#if($tour)
|
||||
const tour_id = ${tour.id};
|
||||
const tour_rounds = ${tour.rounds};
|
||||
|
Reference in New Issue
Block a user