Api auth in progress

This commit is contained in:
Claude Brisson
2024-02-24 12:03:29 +01:00
parent eaf6fc6e2d
commit 6969669e4c
11 changed files with 164 additions and 22 deletions

View File

@@ -9,12 +9,11 @@ import javax.servlet.http.HttpServletResponse
interface ApiHandler {
fun route(request: HttpServletRequest, response: HttpServletResponse) =
// for now, only get() needed the response object ; other methods shall be reengineered as well if needed
when (request.method) {
"GET" -> get(request, response)
"POST" -> post(request)
"PUT" -> put(request)
"DELETE" -> delete(request)
"POST" -> post(request, response)
"PUT" -> put(request, response)
"DELETE" -> delete(request, response)
else -> notImplemented()
}
@@ -22,15 +21,15 @@ interface ApiHandler {
notImplemented()
}
fun post(request: HttpServletRequest): Json {
fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
notImplemented()
}
fun put(request: HttpServletRequest): Json {
fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
notImplemented()
}
fun delete(request: HttpServletRequest): Json {
fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
notImplemented()
}

View File

@@ -0,0 +1,15 @@
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

@@ -4,7 +4,6 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID
import org.jeudego.pairgoth.model.toJson
@@ -33,7 +32,7 @@ object PairingHandler: PairgothApiHandler {
)
}
override fun post(request: HttpServletRequest): 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")
@@ -63,7 +62,7 @@ object PairingHandler: PairgothApiHandler {
return ret
}
override fun put(request: HttpServletRequest): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)
@@ -139,7 +138,7 @@ object PairingHandler: PairgothApiHandler {
}
}
override fun delete(request: HttpServletRequest): Json {
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)

View File

@@ -19,7 +19,7 @@ object PlayerHandler: PairgothApiHandler {
}
}
override fun post(request: HttpServletRequest): Json {
override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val payload = getObjectPayload(request)
val player = Player.fromJson(payload)
@@ -28,7 +28,7 @@ object PlayerHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to player.id)
}
override fun put(request: HttpServletRequest): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.players[id] ?: badRequest("invalid player id")
@@ -39,7 +39,7 @@ object PlayerHandler: PairgothApiHandler {
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
tournament.players.remove(id) ?: badRequest("invalid player id")

View File

@@ -18,7 +18,7 @@ object ResultsHandler: PairgothApiHandler {
return games.map { it.toJson() }.toJsonArray()
}
override fun put(request: HttpServletRequest): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getObjectPayload(request)

View File

@@ -19,7 +19,7 @@ object TeamHandler: PairgothApiHandler {
}
}
override fun post(request: HttpServletRequest): 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)
@@ -29,7 +29,7 @@ object TeamHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to team.id)
}
override fun put(request: HttpServletRequest): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
@@ -41,7 +41,7 @@ object TeamHandler: PairgothApiHandler {
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector")

View File

@@ -0,0 +1,43 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
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"
}
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val auth = request.session.getAttribute(AUTH_KEY) as String?
if (auth == null) {
failed(request.session, 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")
if (answer == null) {
failed(request.session, response)
} else {
}
}
private fun failed(session: HttpSession, response: HttpServletResponse) {
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"))
}
}

View File

@@ -36,7 +36,7 @@ object TournamentHandler: PairgothApiHandler {
}
}
override fun post(request: HttpServletRequest): 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)
@@ -47,7 +47,7 @@ object TournamentHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to tournament.id)
}
override fun put(request: HttpServletRequest): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
// BC TODO - some checks are needed here (cannot lower rounds number if games have been played in removed rounds, for instance)
val tournament = getTournament(request)
val payload = getObjectPayload(request)
@@ -67,7 +67,7 @@ object TournamentHandler: PairgothApiHandler {
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request)
Store.deleteTournament(tournament)
tournament.dispatchEvent(TournamentDeleted, Json.Object("id" to tournament.id))

View File

@@ -0,0 +1,57 @@
package org.jeudego.pairgoth.server
import java.nio.charset.Charset
import javax.crypto.Cipher
import javax.crypto.Cipher.DECRYPT_MODE
import javax.crypto.Cipher.ENCRYPT_MODE
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
/**
* Basic AES encryption. Please note that it uses the ECB block mode, which has the advantage
* to not require random bytes, thus providing some *persistence* for the encrypted data, but
* at the expense of some security weaknesses. The purpose here is just to encrypt temporary
* session ids in URLs, not to protect state secrets.
*/
class AESCryptograph : Cryptograph {
override fun init(key: String) {
val bytes = key.toByteArray(Charset.defaultCharset())
if (bytes.size < 16) {
throw Error("not enough secret bytes")
}
val secret: SecretKey = SecretKeySpec(bytes, 0, 16, ALGORITHM)
try {
encrypt.init(ENCRYPT_MODE, secret)
decrypt.init(DECRYPT_MODE, secret)
} catch (e: Exception) {
throw RuntimeException("cyptograph initialization failed", e)
}
}
override fun encrypt(str: String): ByteArray {
return try {
encrypt.doFinal(str.toByteArray(Charset.defaultCharset()))
} catch (e: Exception) {
throw RuntimeException("encryption failed failed", e)
}
}
override fun decrypt(bytes: ByteArray): String {
return try {
String(decrypt.doFinal(bytes), Charset.defaultCharset())
} catch (e: Exception) {
throw RuntimeException("encryption failed failed", e)
}
}
private var encrypt = Cipher.getInstance(CIPHER)
private var decrypt = Cipher.getInstance(CIPHER)
companion object {
private val CIPHER = "AES/ECB/PKCS5Padding"
private val ALGORITHM = "AES"
}
}

View File

@@ -0,0 +1,29 @@
package org.jeudego.pairgoth.server
import java.io.Serializable
/**
* Cryptograph - used to encrypt and decrypt strings.
*
*/
interface Cryptograph : Serializable {
/**
* init.
* @param random random string
*/
fun init(random: String)
/**
* encrypt.
* @param str string to encrypt
* @return encrypted string
*/
fun encrypt(str: String): ByteArray
/**
* decrypt.
* @param bytes to decrypt
* @return decrypted string
*/
fun decrypt(bytes: ByteArray): String
}