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

View File

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

View File

@@ -52,6 +52,5 @@ class AESCryptograph : Cryptograph {
companion object {
private val CIPHER = "AES/ECB/PKCS5Padding"
private val ALGORITHM = "AES"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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