This commit is contained in:
Claude Brisson
2024-02-24 22:46:52 +01:00
parent 6969669e4c
commit 69d4a9c1e6
9 changed files with 45 additions and 119 deletions

View File

@@ -16,7 +16,6 @@
<description>PairGoth pairing system</description> <description>PairGoth pairing system</description>
<url>TODO</url> <url>TODO</url>
<properties> <properties>
<pac4j.version>5.7.1</pac4j.version>
</properties> </properties>
<build> <build>
<defaultGoal>package</defaultGoal> <defaultGoal>package</defaultGoal>
@@ -166,9 +165,9 @@
</dependency> </dependency>
<!-- auth --> <!-- auth -->
<dependency> <dependency>
<groupId>org.pac4j</groupId> <groupId>commons-codec</groupId>
<artifactId>pac4j-oauth</artifactId> <artifactId>commons-codec</artifactId>
<version>${pac4j.version}</version> <version>1.16.1</version>
</dependency> </dependency>
<!-- logging --> <!-- logging -->
<dependency> <dependency>

View File

@@ -1,6 +1,8 @@
package org.jeudego.pairgoth.api package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.util.AESCryptograph
import org.jeudego.pairgoth.util.Cryptograph
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpSession import javax.servlet.http.HttpSession
@@ -9,11 +11,14 @@ class TokenHandler: ApiHandler {
companion object { companion object {
const val AUTH_KEY = "pairgoth-auth" const val AUTH_KEY = "pairgoth-auth"
const val CHALLENGE_KEY = "pairgoth-challenge" const val CHALLENGE_KEY = "pairgoth-challenge"
private val cryptograph: Cryptograph = AESCryptograph().apply {
init("78659783ed8ccc0e")
}
} }
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val auth = request.session.getAttribute(AUTH_KEY) as String? val auth = request.session.getAttribute(AUTH_KEY) as String?
if (auth == null) { if (auth == null) {
failed(request.session, response) failed(request, response)
return null return null
} else { } else {
return Json.Object( return Json.Object(
@@ -25,19 +30,26 @@ class TokenHandler: ApiHandler {
override fun post(request: HttpServletRequest, response: HttpServletResponse): Json { override fun post(request: HttpServletRequest, response: HttpServletResponse): Json {
val auth = getObjectPayload(request) val auth = getObjectPayload(request)
val answer = auth.getString("answer") val answer = auth.getString("answer")
if (answer == null) { val challenge = request.session.getAttribute(CHALLENGE_KEY) as AuthChallenge?
failed(request.session, response) if (answer == null || challenge == null) {
failed(request, response)
} else { } else {
val parts = cryptograph.webDecrypt(answer).split(":")
if (parts.size != 2)
} }
} }
private fun failed(session: HttpSession, response: HttpServletResponse) { override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
request.session.removeAttribute(AUTH_KEY)
return Json.Object("success" to true)
}
private fun failed(request: HttpServletRequest, response: HttpServletResponse) {
val session = request.session
val challenge = AuthChallenge() val challenge = AuthChallenge()
session.setAttribute(CHALLENGE_KEY, challenge) session.setAttribute(CHALLENGE_KEY, challenge)
response.addHeader("WWW-Authenticate", challenge.value) response.addHeader("WWW-Authenticate", challenge.value)
response.status = HttpServletResponse.SC_UNAUTHORIZED response.status = HttpServletResponse.SC_UNAUTHORIZED
response.writer.println(Json.Object("status" to "failed")) response.writer.println(Json.Object("status" to "failed", "message" to "unauthorized"))
} }
} }

View File

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

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

View File

@@ -76,6 +76,11 @@
<artifactId>kotlinx-datetime-jvm</artifactId> <artifactId>kotlinx-datetime-jvm</artifactId>
<version>0.4.0</version> <version>0.4.0</version>
</dependency> </dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.1</version>
</dependency>
<!-- servlets and mail APIs --> <!-- servlets and mail APIs -->
<dependency> <dependency>
<groupId>jakarta.servlet</groupId> <groupId>jakarta.servlet</groupId>

View File

@@ -1,5 +1,6 @@
package org.jeudego.pairgoth.util package org.jeudego.pairgoth.util
import org.apache.commons.codec.binary.Base64
import java.io.Serializable import java.io.Serializable
/** /**
@@ -26,4 +27,10 @@ interface Cryptograph : Serializable {
* @return decrypted string * @return decrypted string
*/ */
fun decrypt(bytes: ByteArray): String fun decrypt(bytes: ByteArray): String
fun webEncrypt(str: String) = Base64.encodeBase64URLSafeString(encrypt(str))
fun webDecrypt(str: String) = decrypt(Base64.decodeBase64(str))
} }

View File

@@ -16,7 +16,6 @@
<description>PairGoth pairing system</description> <description>PairGoth pairing system</description>
<url>TODO</url> <url>TODO</url>
<properties> <properties>
<pac4j.version>5.7.1</pac4j.version>
<lucene.version>9.9.0</lucene.version> <lucene.version>9.9.0</lucene.version>
</properties> </properties>
<build> <build>
@@ -184,9 +183,9 @@
</dependency> </dependency>
<!-- auth --> <!-- auth -->
<dependency> <dependency>
<groupId>org.pac4j</groupId> <groupId>commons-codec</groupId>
<artifactId>pac4j-oauth</artifactId> <artifactId>commons-codec</artifactId>
<version>${pac4j.version}</version> <version>1.16.1</version>
</dependency> </dependency>
<!-- logging --> <!-- logging -->
<dependency> <dependency>

View File

@@ -6,7 +6,6 @@ import com.republicate.kson.Json
import org.jeudego.pairgoth.web.WebappManager import org.jeudego.pairgoth.web.WebappManager
//import com.republicate.modality.util.AESCryptograph //import com.republicate.modality.util.AESCryptograph
//import com.republicate.modality.util.Cryptograph //import com.republicate.modality.util.Cryptograph
import org.apache.commons.codec.binary.Base64
import org.apache.http.NameValuePair import org.apache.http.NameValuePair
import org.jeudego.pairgoth.util.AESCryptograph import org.jeudego.pairgoth.util.AESCryptograph
import org.jeudego.pairgoth.util.ApiClient.JsonApiClient import org.jeudego.pairgoth.util.ApiClient.JsonApiClient
@@ -14,8 +13,6 @@ import org.jeudego.pairgoth.util.Cryptograph
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
abstract class OAuthHelper { abstract class OAuthHelper {
abstract val name: String abstract val name: String
@@ -28,20 +25,22 @@ abstract class OAuthHelper {
get() = WebappManager.getMandatoryProperty("webapp.external.url").removeSuffix("/") + "/oauth/${name}" get() = WebappManager.getMandatoryProperty("webapp.external.url").removeSuffix("/") + "/oauth/${name}"
protected fun getState(sessionId: String): String { protected fun getState(sessionId: String): String {
return name + ":" + encrypt(sessionId) return name + ":" + cryptograph.webEncrypt(sessionId)
} }
fun checkState(state: String, expectedSessionId: String): Boolean { fun checkState(state: String, expectedSessionId: String): Boolean {
val foundSessionId = decrypt(state) val foundSessionId = cryptograph.webDecrypt(state)
return expectedSessionId == foundSessionId return expectedSessionId == foundSessionId
} }
protected abstract fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> protected abstract fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>>
@Throws(IOException::class) @Throws(IOException::class)
fun getAccessToken(code: String): String { fun getAccessToken(sessionID: String, code: String): String {
val (url, params) = getAccessTokenURL(code) val (url, params) = getAccessTokenURL(code)
val json = JsonApiClient.post(url, null, *params.toTypedArray()).asObject() val json = JsonApiClient.post(url, null, *params.toTypedArray()).asObject()
val state = json.getString("state") ?: throw IOException("could not get state")
if (!checkState(state, sessionID)) throw IOException("invalid state")
return json.getString("access_token") ?: throw IOException("could not get access token") return json.getString("access_token") ?: throw IOException("could not get access token")
} }
@@ -55,17 +54,8 @@ abstract class OAuthHelper {
companion object { companion object {
protected var logger: Logger = LoggerFactory.getLogger("oauth") protected var logger: Logger = LoggerFactory.getLogger("oauth")
private const val salt = "0efd28fb53cbac42" private val cryptograph: Cryptograph = AESCryptograph().apply {
private val sessionIdCrypto: Cryptograph = AESCryptograph().apply { init("0efd28fb53cbac42")
init(salt)
}
private fun encrypt(input: String): String {
return Base64.encodeBase64URLSafeString(sessionIdCrypto.encrypt(input))
}
private fun decrypt(input: String): String {
return sessionIdCrypto.decrypt(Base64.decodeBase64(input))
} }
} }
} }

View File

@@ -34,7 +34,7 @@ class AuthFilter: Filter {
if (auth == "oauth" && uri.startsWith("/oauth/")) { if (auth == "oauth" && uri.startsWith("/oauth/")) {
val provider = uri.substring("/oauth/".length) val provider = uri.substring("/oauth/".length)
val helper = OauthHelperFactory.getHelper(provider) val helper = OauthHelperFactory.getHelper(provider)
val accessToken = helper.getAccessToken(request.getParameter("code") ?: "") val accessToken = helper.getAccessToken(request.session.id, request.getParameter("code") ?: "")
val user = helper.getUserInfos(accessToken) val user = helper.getUserInfos(accessToken)
request.session.setAttribute("logged", user) request.session.setAttribute("logged", user)
response.sendRedirect("/index") response.sendRedirect("/index")