From 6969669e4c52edb58790d3f00c93b6dfa4542348 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 24 Feb 2024 12:03:29 +0100 Subject: [PATCH] Api auth in progress --- .../org/jeudego/pairgoth/api/ApiHandler.kt | 13 ++--- .../org/jeudego/pairgoth/api/AuthChallenge.kt | 15 +++++ .../jeudego/pairgoth/api/PairingHandler.kt | 7 +-- .../org/jeudego/pairgoth/api/PlayerHandler.kt | 6 +- .../jeudego/pairgoth/api/ResultsHandler.kt | 2 +- .../org/jeudego/pairgoth/api/TeamHandler.kt | 6 +- .../org/jeudego/pairgoth/api/TokenHandler.kt | 43 ++++++++++++++ .../jeudego/pairgoth/api/TournamentHandler.kt | 6 +- .../jeudego/pairgoth/server/AESCryptograph.kt | 57 +++++++++++++++++++ .../jeudego/pairgoth/server/Cryptograph.kt | 29 ++++++++++ .../org/jeudego/pairgoth/oauth/OAuthHelper.kt | 2 +- 11 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/AuthChallenge.kt create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/AESCryptograph.kt create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Cryptograph.kt 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 891d240..4d84c5c 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 @@ -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() } 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 new file mode 100644 index 0000000..72e0d98 --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/AuthChallenge.kt @@ -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 +} 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 4e798b6..84a02ea 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 @@ -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...) 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 1780b4d..26f7b14 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): 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") diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt index a87b06d..ded6361 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt @@ -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) 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 ad5a5ca..51aaed6 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): 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") 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 new file mode 100644 index 0000000..654edb9 --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TokenHandler.kt @@ -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")) + } +} 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 3f8d8c6..a50b37f 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): 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)) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/AESCryptograph.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/AESCryptograph.kt new file mode 100644 index 0000000..d202a2e --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/AESCryptograph.kt @@ -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" + + } +} diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Cryptograph.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Cryptograph.kt new file mode 100644 index 0000000..b3a34ed --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/Cryptograph.kt @@ -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 +} 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 09e22a0..7cdf5fa 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 @@ -55,7 +55,7 @@ abstract class OAuthHelper { companion object { protected var logger: Logger = LoggerFactory.getLogger("oauth") - private const val salt = "0efd28fb53cbac42" + private const val salt = "0efd28fb53cbac42" private val sessionIdCrypto: Cryptograph = AESCryptograph().apply { init(salt) }