Auth ok
This commit is contained in:
@@ -8,6 +8,9 @@ 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 org.jeudego.pairgoth.web.toHex
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import java.util.Random
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
@@ -18,6 +21,7 @@ object TokenHandler: ApiHandler {
|
||||
const val AUTH_HEADER = "Authorization"
|
||||
const val AUTH_PREFIX = "Bearer"
|
||||
|
||||
private val hasher = MessageDigest.getInstance("SHA-256")
|
||||
private val cryptograph = AESCryptograph().apply { init(sharedSecret) }
|
||||
|
||||
private data class AuthorizationPayload(
|
||||
@@ -26,21 +30,26 @@ object TokenHandler: ApiHandler {
|
||||
val userInfos: Json
|
||||
)
|
||||
|
||||
private fun getAuthorizationPayload(request: HttpServletRequest): AuthorizationPayload? {
|
||||
private fun parseAuthorizationHeader(request: HttpServletRequest): Pair<String, String>? {
|
||||
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]
|
||||
return Pair(parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getAuthorizationPayload(request: HttpServletRequest): AuthorizationPayload? {
|
||||
parseAuthorizationHeader(request)?.let { (sessionId, accessKey) ->
|
||||
val accessPayload = accesses.getIfPresent(accessKey)
|
||||
if (accessPayload != null && sessionId == accessPayload.getString("session")) {
|
||||
return AuthorizationPayload(sessionId, accessKey, accessPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -63,15 +72,15 @@ object TokenHandler: ApiHandler {
|
||||
if (challenge != null) {
|
||||
val email = auth.getString("email")
|
||||
val signature = auth.getString("signature")
|
||||
val expectedSignature = cryptograph.webEncrypt(
|
||||
val expectedSignature = hasher.digest(
|
||||
"${
|
||||
session
|
||||
}:${
|
||||
challenge
|
||||
}:${
|
||||
email
|
||||
}"
|
||||
)
|
||||
}".toByteArray(StandardCharsets.UTF_8)
|
||||
).toHex()
|
||||
if (signature == expectedSignature) {
|
||||
val accessKey = Randomizer.randomString(32)
|
||||
accesses.put(accessKey, Json.Object(
|
||||
@@ -93,10 +102,11 @@ object TokenHandler: ApiHandler {
|
||||
}
|
||||
|
||||
private fun failed(request: HttpServletRequest, response: HttpServletResponse) {
|
||||
val authPayload = getAuthorizationPayload(request)
|
||||
if (authPayload != null && authPayload.sessionId.isNotEmpty()) {
|
||||
val authValues = parseAuthorizationHeader(request)
|
||||
if (authValues != null && authValues.first.isNotEmpty()) {
|
||||
val sessionId = authValues.first
|
||||
val challenge = Randomizer.randomString(32)
|
||||
challenges.put(authPayload.sessionId, challenge)
|
||||
challenges.put(sessionId, challenge)
|
||||
response.addHeader("WWW-Authenticate", challenge)
|
||||
response.status = HttpServletResponse.SC_UNAUTHORIZED
|
||||
response.writer.println(Json.Object("status" to "failed", "message" to "unauthorized"))
|
||||
|
@@ -56,6 +56,7 @@ class ApiServlet: HttpServlet() {
|
||||
val requestLock = if (request.method == "GET") lock.readLock() else lock.writeLock()
|
||||
try {
|
||||
requestLock.lock()
|
||||
logger.logRequest(request, !request.requestURI.contains(".") && request.requestURI.length > 1)
|
||||
if (checkAuthorization(request, response)) {
|
||||
doProtectedRequest(request, response)
|
||||
} else {
|
||||
@@ -68,7 +69,6 @@ class ApiServlet: HttpServlet() {
|
||||
|
||||
private fun doProtectedRequest(request: HttpServletRequest, response: HttpServletResponse) {
|
||||
val uri = request.requestURI
|
||||
logger.logRequest(request, !uri.contains(".") && uri.length > 1)
|
||||
|
||||
var payload: Json? = null
|
||||
var reason = "OK"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# debug version
|
||||
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
|
||||
mvn -DskipTests package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5006 -Dpairgoth.mode=client -jar application/target/pairgoth-engine.jar
|
||||
|
||||
mvn -DskipTests=true package && java -Dpairgoth.mode=client -jar application/target/pairgoth-engine.jar
|
||||
# mvn -DskipTests package && java -Dpairgoth.mode=client -jar application/target/pairgoth-engine.jar
|
||||
|
@@ -16,3 +16,5 @@ val sharedSecret: String by lazy {
|
||||
if (it.length != 16) throw RuntimeException("shared secret must be 16 ascii chars long")
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# debug version
|
||||
# mvn package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5006 -jar application/target/pairgoth-engine.jar
|
||||
mvn -DskipTests package && java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dpairgoth.mode=server -jar application/target/pairgoth-engine.jar
|
||||
|
||||
mvn -DskipTests=true package && java -Dpairgoth.mode=server -jar application/target/pairgoth-engine.jar
|
||||
# mvn -DskipTests=true package && java -Dpairgoth.mode=server -jar application/target/pairgoth-engine.jar
|
||||
|
@@ -40,8 +40,9 @@ abstract class OAuthHelper {
|
||||
fun getAccessToken(sessionID: String, code: String): String {
|
||||
val (url, params) = getAccessTokenURL(code)
|
||||
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")
|
||||
// CB TODO - do not check state for now
|
||||
// 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")
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import org.jeudego.pairgoth.web.AuthFilter
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class ApiTool {
|
||||
companion object {
|
||||
@@ -36,7 +37,22 @@ class ApiTool {
|
||||
private fun Json.toRequestBody() = toString().toRequestBody(JSON.toMediaType())
|
||||
private fun Request.Builder.process(): Json {
|
||||
try {
|
||||
return client.newCall(build()).execute().use { response ->
|
||||
val apiReq = build()
|
||||
if (logger.isTraceEnabled) {
|
||||
logger.trace(">> ${apiReq.method} ${apiReq.url}")
|
||||
apiReq.headers.forEach { header ->
|
||||
logger.trace(" ${header.first} ${header.second}")
|
||||
}
|
||||
}
|
||||
|
||||
logger.trace(" ")
|
||||
return client.newCall(apiReq).execute().use { response ->
|
||||
if (logger.isTraceEnabled) {
|
||||
logger.trace("<< ${response.code} ${response.message}")
|
||||
response.headers.forEach { header ->
|
||||
logger.trace(" ${header.first} ${header.second}")
|
||||
}
|
||||
}
|
||||
if (response.isSuccessful) {
|
||||
when (response.body?.contentType()?.subtype) {
|
||||
null -> throw Error("null body or content type")
|
||||
@@ -44,6 +60,10 @@ class ApiTool {
|
||||
else -> throw Error("unhandled content type: ${response.body!!.contentType()}")
|
||||
}
|
||||
} else {
|
||||
if (response.code == HttpServletResponse.SC_UNAUTHORIZED) {
|
||||
request.session.removeAttribute(AuthFilter.SESSION_KEY_API_TOKEN)
|
||||
request.session.removeAttribute(AuthFilter.SESSION_KEY_USER)
|
||||
}
|
||||
when (response.body?.contentType()?.subtype) {
|
||||
"json" -> Json.parse(response.body!!.string()) ?: throw Error("could not parse error json")
|
||||
else -> throw Error("${response.code} ${response.message}")
|
||||
|
@@ -9,6 +9,7 @@ import org.jeudego.pairgoth.oauth.OauthHelperFactory
|
||||
import org.jeudego.pairgoth.util.AESCryptograph
|
||||
import org.jeudego.pairgoth.view.ApiTool
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.servlet.Filter
|
||||
@@ -95,11 +96,16 @@ class AuthFilter: Filter {
|
||||
}
|
||||
|
||||
fun fetchApiToken(req: HttpServletRequest, user: Json.Object): String? {
|
||||
try {
|
||||
logger.trace("getting challenge...")
|
||||
val challengeReq = Request.Builder().url("${ApiTool.apiRoot}tour/token")
|
||||
.header("Accept", "application/json")
|
||||
.header("Authorization", "Bearer ${getBearer(req)}")
|
||||
.build()
|
||||
val challengeResp = client.newCall(challengeReq).execute()
|
||||
challengeResp.use {
|
||||
if (challengeResp.code == HttpServletResponse.SC_UNAUTHORIZED) {
|
||||
logger.trace("building answer...")
|
||||
val email = user.getString("email") ?: "-"
|
||||
val challenge = challengeResp.headers["WWW-Authenticate"]
|
||||
if (challenge != null) {
|
||||
@@ -110,25 +116,36 @@ class AuthFilter: Filter {
|
||||
challenge
|
||||
}:${
|
||||
email
|
||||
}".toByteArray(StandardCharsets.UTF_8))
|
||||
}".toByteArray(StandardCharsets.UTF_8)
|
||||
).toHex()
|
||||
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 answerReq = Request.Builder().url("${ApiTool.apiRoot}tour/token")
|
||||
.header("Accept", "application/json")
|
||||
.post(answer.toString().toRequestBody(ApiTool.JSON.toMediaType()))
|
||||
.build()
|
||||
val answerResp = client.newCall(answerReq).execute()
|
||||
answerResp.use {
|
||||
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
|
||||
if (token != null) {
|
||||
logger.trace("got token $token")
|
||||
return token
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
logger.warn("could not fetch access token", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
@@ -1 +0,0 @@
|
||||
level = info
|
@@ -59,17 +59,44 @@ private fun cleanup() {
|
||||
FileUtils.deleteDirectory(webapps.toFile())
|
||||
}
|
||||
|
||||
private val allowedModes = setOf("standalone", "server", "client")
|
||||
|
||||
private fun readProperties() {
|
||||
val defaultProps = getResource("/server.default.properties") ?: throw Error("missing default server properties")
|
||||
// do a first pass at determining the final 'mode', since it will influence other default value
|
||||
var mode = "standalone"
|
||||
val userProperties = File("./pairgoth.properties")
|
||||
if (userProperties.exists()) {
|
||||
val userProps = Properties()
|
||||
userProps.load(FileReader(userProperties))
|
||||
if (userProps.contains("mode")) mode = userProps.getProperty("mode")
|
||||
}
|
||||
val systemMode: String? = System.getProperty("pairgoth.mode")
|
||||
if (systemMode != null) {
|
||||
mode = systemMode
|
||||
}
|
||||
|
||||
if (!allowedModes.contains(mode)) throw Error("invalid mode: $mode")
|
||||
|
||||
// read default properties
|
||||
val defaultProps = getResource("/${mode}.default.properties") ?: throw Error("missing default server properties")
|
||||
defaultProps.openStream().use {
|
||||
serverProps.load(InputStreamReader(it, StandardCharsets.UTF_8))
|
||||
}
|
||||
val properties = File("./pairgoth.properties")
|
||||
if (properties.exists()) {
|
||||
serverProps.load(FileReader(properties))
|
||||
}
|
||||
// default env depends upon the presence of the pom.xml file
|
||||
val env = if (File("./pom.xml").exists()) "dev" else "prod"
|
||||
serverProps["env"] = env
|
||||
// read user properties
|
||||
if (userProperties.exists()) {
|
||||
serverProps.load(FileReader(userProperties))
|
||||
}
|
||||
// read system properties
|
||||
System.getProperties().forEach {
|
||||
val key = it.key as String
|
||||
val value = it.value as String
|
||||
if (key.startsWith("pairgoth.")) {
|
||||
serverProps[key.removePrefix("pairgoth.")] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishProperties() {
|
||||
@@ -149,12 +176,23 @@ private fun launchServer() {
|
||||
}
|
||||
}
|
||||
|
||||
val webappUrl = URL(
|
||||
serverProps.getProperty("webapp.protocol") ?: throw Error("missing property webapp.protocol"),
|
||||
val webappUrl = when (mode) {
|
||||
"client", "standalone" ->
|
||||
URL(
|
||||
serverProps.getProperty("webapp.protocol") ?: throw Error("missing property webapp.protocol"),
|
||||
serverProps.getProperty("webapp.host") ?: throw Error("missing property webapp.host"),
|
||||
serverProps.getProperty("webapp.port")?.toInt() ?: 80,
|
||||
"/"
|
||||
)
|
||||
"server" ->
|
||||
URL(
|
||||
serverProps.getProperty("api.protocol") ?: throw Error("missing property api.protocol"),
|
||||
serverProps.getProperty("api.host") ?: throw Error("missing property api.host"),
|
||||
serverProps.getProperty("api.port")?.toInt() ?: 80,
|
||||
"/"
|
||||
)
|
||||
else -> throw Error("invalid mode: $mode")
|
||||
}
|
||||
val secure = webappUrl.protocol == "https"
|
||||
|
||||
// create server
|
||||
|
18
webserver/src/main/resources/client.default.properties
Normal file
18
webserver/src/main/resources/client.default.properties
Normal file
@@ -0,0 +1,18 @@
|
||||
mode = standalone
|
||||
# webapp connector
|
||||
webapp.protocol = http
|
||||
webapp.host = localhost
|
||||
webapp.port = 8080
|
||||
webapp.context = /
|
||||
webapp.external.url = http://localhost:8080
|
||||
|
||||
# api connector
|
||||
api.protocol = http
|
||||
api.host = localhost
|
||||
api.port = 8085
|
||||
api.context = /api/tour
|
||||
api.external.url = http://localhost:8085/api/
|
||||
|
||||
webapp.ssl.key = jar:file:$jar!/ssl/localhost.key
|
||||
webapp.ssl.pass =
|
||||
webapp.ssl.cert = jar:file:$jar!/ssl/localhost.crt
|
@@ -1,17 +1,17 @@
|
||||
mode = standalone
|
||||
mode = server
|
||||
# webapp connector
|
||||
webapp.protocol = http
|
||||
webapp.interface = localhost
|
||||
webapp.host = localhost
|
||||
webapp.port = 8080
|
||||
webapp.context = /
|
||||
webapp.external.url = http://localhost:8080
|
||||
|
||||
# api connector
|
||||
api.protocol = http
|
||||
api.interface = localhost
|
||||
api.port = 8080
|
||||
api.host = localhost
|
||||
api.port = 8085
|
||||
api.context = /api/tour
|
||||
api.external.url = http://localhost:8080/api/
|
||||
api.external.url = http://localhost:8085/api/
|
||||
|
||||
webapp.ssl.key = jar:file:$jar!/ssl/localhost.key
|
||||
webapp.ssl.pass =
|
||||
|
18
webserver/src/main/resources/standalone.default.properties
Normal file
18
webserver/src/main/resources/standalone.default.properties
Normal file
@@ -0,0 +1,18 @@
|
||||
mode = standalone
|
||||
# webapp connector
|
||||
webapp.protocol = http
|
||||
webapp.host = localhost
|
||||
webapp.port = 8080
|
||||
webapp.context = /
|
||||
webapp.external.url = http://localhost:8080
|
||||
|
||||
# api connector
|
||||
api.protocol = http
|
||||
api.host = localhost
|
||||
api.port = 8080
|
||||
api.context = /api/tour
|
||||
api.external.url = http://localhost:8080/api/
|
||||
|
||||
webapp.ssl.key = jar:file:$jar!/ssl/localhost.key
|
||||
webapp.ssl.pass =
|
||||
webapp.ssl.cert = jar:file:$jar!/ssl/localhost.crt
|
Reference in New Issue
Block a user