Auth still in progress

This commit is contained in:
Claude Brisson
2024-02-26 09:54:28 +01:00
parent 69d4a9c1e6
commit 71549f185e
27 changed files with 295 additions and 84 deletions

View File

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

View File

@@ -21,7 +21,7 @@ interface ApiHandler {
notImplemented()
}
fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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