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)
|
||||
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()
|
||||
|
||||
}
|
||||
|
@@ -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()
|
||||
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")
|
||||
}
|
||||
|
Reference in New Issue
Block a user