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

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