OAuth FFG ok

This commit is contained in:
Claude Brisson
2024-02-02 19:03:05 +01:00
parent b431d7ab5c
commit 4f66852e6d
20 changed files with 158 additions and 244 deletions

View File

@@ -1,28 +0,0 @@
package org.jeudego.pairgoth.oauth
class FacebookHelper : OAuthHelper() {
override val name: String
get() = "facebook"
override fun getLoginURL(sessionId: String?): String {
return "https://www.facebook.com/v14.0/dialog/oauth?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&scope=email" +
"&state=" + getState(sessionId!!)
}
override fun getAccessTokenURL(code: String): String? {
return "https://graph.facebook.com/v14.0/oauth/access_token?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&client_secret=" + secret +
"&code=" + code
}
override fun getUserInfosURL(accessToken: String): String? {
return "https://graph.facebook.com/me?" +
"field=email" +
"&access_token=" + accessToken
}
}

View File

@@ -1,18 +0,0 @@
package org.jeudego.pairgoth.oauth
class GoogleHelper : OAuthHelper() {
override val name: String
get() = "google"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -1,18 +0,0 @@
package org.jeudego.pairgoth.oauth
class InstagramHelper : OAuthHelper() {
override val name: String
get() = "instagram"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -1,77 +0,0 @@
package org.jeudego.pairgoth.oauth
// In progress
import com.republicate.kson.Json
import org.jeudego.pairgoth.server.WebappManager
//import com.republicate.modality.util.AESCryptograph
//import com.republicate.modality.util.Cryptograph
import org.apache.commons.codec.binary.Base64
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
abstract class OAuthHelper {
abstract val name: String
abstract fun getLoginURL(sessionId: String?): String
protected val clientId: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".client_id")
protected val secret: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
protected val redirectURI: String?
protected get() = try {
val uri: String = WebappManager.getProperty("webapp.external.url") + "/oauth.html"
URLEncoder.encode(uri, "UTF-8")
} catch (uee: UnsupportedEncodingException) {
logger.error("could not encode redirect URI", uee)
null
}
protected fun getState(sessionId: String): String {
return name + ":" + encrypt(sessionId)
}
fun checkState(state: String, expectedSessionId: String): Boolean {
val foundSessionId = decrypt(state)
return expectedSessionId == foundSessionId
}
protected abstract fun getAccessTokenURL(code: String): String?
@Throws(IOException::class)
fun getAccessToken(code: String): String {
val json: Json.Object = Json.Object() // TODO - apiClient.get(getAccessTokenURL(code))
return json.getString("access_token")!! // ?!
}
protected abstract fun getUserInfosURL(accessToken: String): String?
@Throws(IOException::class)
fun getUserEmail(accessToken: String): String {
val json: Json.Object = Json.Object()
// TODO
// apiClient.get(getUserInfosURL(accessToken))
return json.getString("email") ?: throw IOException("could not fetch email")
}
companion object {
protected var logger = LoggerFactory.getLogger("oauth")
private const val salt = "0efd28fb53cbac42"
// private val sessionIdCrypto: Cryptograph = AESCryptograph().apply {
// init(salt)
// }
private fun encrypt(input: String): String {
return "TODO"
// return Base64.encodeBase64URLSafeString(sessionIdCrypto.encrypt(input))
}
private fun decrypt(input: String): String {
return "TODO"
// return sessionIdCrypto.decrypt(Base64.decodeBase64(input))
}
// TODO
// private val apiClient: ApiClient = ApiClient()
}
}

View File

@@ -1,17 +0,0 @@
package org.jeudego.pairgoth.oauth
object OauthHelperFactory {
private val facebook: OAuthHelper = FacebookHelper()
private val google: OAuthHelper = GoogleHelper()
private val instagram: OAuthHelper = InstagramHelper()
private val twitter: OAuthHelper = TwitterHelper()
fun getHelper(provider: String?): OAuthHelper {
return when (provider) {
"facebook" -> facebook
"google" -> google
"instagram" -> instagram
"twitter" -> twitter
else -> throw RuntimeException("wrong provider")
}
}
}

View File

@@ -1,18 +0,0 @@
package org.jeudego.pairgoth.oauth
class TwitterHelper : OAuthHelper() {
override val name: String
get() = "twitter"
override fun getLoginURL(sessionId: String?): String {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
}
override fun getUserInfosURL(accessToken: String): String? {
return null
}
}

View File

@@ -10,6 +10,6 @@ mvn -DskipTests=true \
-Dpairgoth.auth=oauth \ -Dpairgoth.auth=oauth \
-Dpairgoth.oauth.providers=ffg \ -Dpairgoth.oauth.providers=ffg \
-Dpairgoth.oauth.ffg.secret=43f3a67bffcb5054d2f1b0e2a2374bdc \ -Dpairgoth.oauth.ffg.secret=43f3a67bffcb5054d2f1b0e2a2374bdc \
-Dwebapp.external.url=http://localhost:8080 -Dwebapp.external.url=http://localhost:8080 \
--projects view-webapp package jetty:run --projects view-webapp package jetty:run
kill $CSSWATCH kill $CSSWATCH

View File

@@ -71,7 +71,8 @@
<pairgoth.webapp.host>localhost</pairgoth.webapp.host> <pairgoth.webapp.host>localhost</pairgoth.webapp.host>
<pairgoth.webapp.port>8080</pairgoth.webapp.port> <pairgoth.webapp.port>8080</pairgoth.webapp.port>
<pairgoth.webapp.context>/</pairgoth.webapp.context> <pairgoth.webapp.context>/</pairgoth.webapp.context>
<pairgoth.webapp.external.url>${pairgoth.webapp.protocol}://${pairgoth.webapp.host}:${pairgoth.webapp.port}/${pairgoth.webapp.context}</pairgoth.webapp.external.url> <!-- 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.protocol>http</pairgoth.api.protocol>
<pairgoth.api.host>localhost</pairgoth.api.host> <pairgoth.api.host>localhost</pairgoth.api.host>
<pairgoth.api.port>8085</pairgoth.api.port> <pairgoth.api.port>8085</pairgoth.api.port>

View File

@@ -230,7 +230,13 @@
<version>70.1</version> <version>70.1</version>
</dependency> </dependency>
--> -->
<!-- http client --> <!-- http clients -->
<!-- CB TODO - migrate API client to okhttp3 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>

View File

@@ -1,29 +1,43 @@
package org.jeudego.pairgoth.oauth package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
import org.jeudego.pairgoth.util.ApiClient.header
import org.jeudego.pairgoth.util.ApiClient.param
import java.net.URLEncoder
class FFGHelper : OAuthHelper() { class FFGHelper : OAuthHelper() {
override val name: String override val name: String
get() = "facebook" get() = "ffg"
override val clientId = "pairgoth"
private val FFG_HOST = "https://testffg" private val FFG_HOST = "https://testffg"
override fun getLoginURL(sessionId: String?): String { override fun getLoginURL(sessionId: String?): String {
return "$FFG_HOST/oauth2/entry_point.php/authorize?" + return "$FFG_HOST/oauth2/entry_point.php/authorize?" +
"client_id=" + clientId + "client_id=" + clientId +
"&redirect_uri=" + redirectURI + "&response_type=code" +
"&redirect_uri=" + URLEncoder.encode(redirectURI, "UTF-8") +
"&scope=email" "&scope=email"
} }
override fun getAccessTokenURL(code: String): String? { override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return "$FFG_HOST/oauth2/entry_point.php/access_token?" + return Pair(
"client_id=" + clientId + "$FFG_HOST/oauth2/entry_point.php/access_token",
"&redirect_uri=" + redirectURI + listOf(
"&client_secret=" + secret + param("client_id", clientId),
"&code=" + code param("redirect_uri", redirectURI),
param("client_secret", secret),
param("code", code),
param("grant_type", "authorization_code")
)
)
} }
override fun getUserInfosURL(accessToken: String): String? { override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return "$FFG_HOST/oauth2/entry_point.php/user_info?" + return Pair(
"field=email" + "$FFG_HOST/oauth2/entry_point.php/user_info",
"&access_token=" + accessToken listOf(header("Authorization", "Bearer $accessToken"))
)
} }
} }

View File

@@ -1,5 +1,9 @@
package org.jeudego.pairgoth.oauth package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
import org.jeudego.pairgoth.util.ApiClient.param
import java.net.URLEncoder
class FacebookHelper : OAuthHelper() { class FacebookHelper : OAuthHelper() {
override val name: String override val name: String
get() = "facebook" get() = "facebook"
@@ -7,22 +11,27 @@ class FacebookHelper : OAuthHelper() {
override fun getLoginURL(sessionId: String?): String { override fun getLoginURL(sessionId: String?): String {
return "https://www.facebook.com/v14.0/dialog/oauth?" + return "https://www.facebook.com/v14.0/dialog/oauth?" +
"client_id=" + clientId + "client_id=" + clientId +
"&redirect_uri=" + redirectURI + "&redirect_uri=" + URLEncoder.encode(redirectURI, "UTF-8") +
"&scope=email" + "&scope=email" +
"&state=" + getState(sessionId!!) "&state=" + getState(sessionId!!)
} }
override fun getAccessTokenURL(code: String): String? { override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return "https://graph.facebook.com/v14.0/oauth/access_token?" + return Pair(
"client_id=" + clientId + "https://graph.facebook.com/v14.0/oauth/access_token",
"&redirect_uri=" + redirectURI + listOf(
"&client_secret=" + secret + param("client_id=", clientId),
"&code=" + code param("redirect_uri=", redirectURI),
param("client_secret=", secret),
param("code=", code)
)
)
} }
override fun getUserInfosURL(accessToken: String): String? { override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return "https://graph.facebook.com/me?" + return Pair(
"field=email" + "https://graph.facebook.com/me?field=email&access_token=$accessToken",
"&access_token=" + accessToken listOf()
)
} }
} }

View File

@@ -1,5 +1,7 @@
package org.jeudego.pairgoth.oauth package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
class GoogleHelper : OAuthHelper() { class GoogleHelper : OAuthHelper() {
override val name: String override val name: String
get() = "google" get() = "google"
@@ -8,11 +10,11 @@ class GoogleHelper : OAuthHelper() {
return "" return ""
} }
override fun getAccessTokenURL(code: String): String? { override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
override fun getUserInfosURL(accessToken: String): String? { override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
} }

View File

@@ -1,5 +1,7 @@
package org.jeudego.pairgoth.oauth package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
class InstagramHelper : OAuthHelper() { class InstagramHelper : OAuthHelper() {
override val name: String override val name: String
get() = "instagram" get() = "instagram"
@@ -8,11 +10,11 @@ class InstagramHelper : OAuthHelper() {
return "" return ""
} }
override fun getAccessTokenURL(code: String): String? { override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
override fun getUserInfosURL(accessToken: String): String? { override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
} }

View File

@@ -7,6 +7,7 @@ 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.commons.codec.binary.Base64
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
import org.jeudego.pairgoth.util.Cryptograph import org.jeudego.pairgoth.util.Cryptograph
@@ -19,18 +20,12 @@ import java.net.URLEncoder
abstract class OAuthHelper { abstract class OAuthHelper {
abstract val name: String abstract val name: String
abstract fun getLoginURL(sessionId: String?): String abstract fun getLoginURL(sessionId: String?): String
protected val clientId: String protected open val clientId: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".client_id") get() = WebappManager.getMandatoryProperty("oauth.$name.client_id")
protected val secret: String protected val secret: String
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret") get() = WebappManager.getMandatoryProperty("oauth.$name.secret")
protected val redirectURI: String? protected open val redirectURI: String
protected get() = try { get() = WebappManager.getMandatoryProperty("webapp.external.url").removeSuffix("/") + "/oauth/${name}"
val uri: String = WebappManager.getMandatoryProperty("webapp.external.url") + "/oauth"
URLEncoder.encode(uri, "UTF-8")
} catch (uee: UnsupportedEncodingException) {
logger.error("could not encode redirect URI", uee)
null
}
protected fun getState(sessionId: String): String { protected fun getState(sessionId: String): String {
return name + ":" + encrypt(sessionId) return name + ":" + encrypt(sessionId)
@@ -41,19 +36,21 @@ abstract class OAuthHelper {
return expectedSessionId == foundSessionId return expectedSessionId == foundSessionId
} }
protected abstract fun getAccessTokenURL(code: String): String? protected abstract fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>>
@Throws(IOException::class) @Throws(IOException::class)
fun getAccessToken(code: String): String { fun getAccessToken(code: String): String {
val json: Json.Object = Json.Object() // TODO - apiClient.get(getAccessTokenURL(code)) val (url, params) = getAccessTokenURL(code)
return json.getString("access_token")!! // ?! val json = JsonApiClient.post(url, null, *params.toTypedArray()).asObject()
return json.getString("access_token") ?: throw IOException("could not get access token")
} }
protected abstract fun getUserInfosURL(accessToken: String): String? protected abstract fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>>
@Throws(IOException::class) @Throws(IOException::class)
fun getUserEmail(accessToken: String): String { fun getUserInfos(accessToken: String): Json {
val json = getUserInfosURL(accessToken)?.let { JsonApiClient.get(it).asObject() } val (url, params) = getUserInfosURL(accessToken)
return json?.getString("email") ?: throw IOException("could not fetch email") return JsonApiClient.get(url, *params.toTypedArray()).asObject()
} }
companion object { companion object {

View File

@@ -8,6 +8,7 @@ object OauthHelperFactory {
private val twitter: OAuthHelper = TwitterHelper() private val twitter: OAuthHelper = TwitterHelper()
fun getHelper(provider: String?): OAuthHelper { fun getHelper(provider: String?): OAuthHelper {
return when (provider) { return when (provider) {
"ffg" -> ffg
"facebook" -> facebook "facebook" -> facebook
"google" -> google "google" -> google
"instagram" -> instagram "instagram" -> instagram

View File

@@ -1,5 +1,7 @@
package org.jeudego.pairgoth.oauth package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
class TwitterHelper : OAuthHelper() { class TwitterHelper : OAuthHelper() {
override val name: String override val name: String
get() = "twitter" get() = "twitter"
@@ -8,11 +10,11 @@ class TwitterHelper : OAuthHelper() {
return "" return ""
} }
override fun getAccessTokenURL(code: String): String? { override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
override fun getUserInfosURL(accessToken: String): String? { override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return null return Pair("", listOf())
} }
} }

View File

@@ -10,6 +10,7 @@ import org.apache.http.client.config.RequestConfig
import org.apache.http.client.entity.UrlEncodedFormEntity import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods.* import org.apache.http.client.methods.*
import org.apache.http.config.SocketConfig import org.apache.http.config.SocketConfig
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.conn.ssl.SSLConnectionSocketFactory import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.entity.ContentType import org.apache.http.entity.ContentType
import org.apache.http.entity.EntityTemplate import org.apache.http.entity.EntityTemplate
@@ -54,7 +55,8 @@ private val client = HttpClients.custom()
SSLConnectionSocketFactory( SSLConnectionSocketFactory(
SSLContexts.createSystemDefault(), arrayOf("TLSv1.2"), SSLContexts.createSystemDefault(), arrayOf("TLSv1.2"),
null, null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier() NoopHostnameVerifier()
//SSLConnectionSocketFactory.getDefaultHostnameVerifier()
) )
) )
.setConnectionTimeToLive(1, TimeUnit.MINUTES) .setConnectionTimeToLive(1, TimeUnit.MINUTES)

View File

@@ -1,5 +1,6 @@
package org.jeudego.pairgoth.web package org.jeudego.pairgoth.web
import org.jeudego.pairgoth.oauth.OauthHelperFactory
import javax.servlet.Filter import javax.servlet.Filter
import javax.servlet.FilterChain import javax.servlet.FilterChain
import javax.servlet.FilterConfig import javax.servlet.FilterConfig
@@ -30,10 +31,19 @@ class AuthFilter: Filter {
val auth = WebappManager.getProperty("auth") ?: throw Error("authentication not configured") val auth = WebappManager.getProperty("auth") ?: throw Error("authentication not configured")
val forwarded = request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) != null val forwarded = request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) != null
if (auth == "oauth" && uri.startsWith("/oauth/")) {
val provider = uri.substring("/oauth/".length)
val helper = OauthHelperFactory.getHelper(provider)
val accessToken = helper.getAccessToken(request.getParameter("code") ?: "")
val user = helper.getUserInfos(accessToken)
request.session.setAttribute("logged", user)
response.sendRedirect("/index")
return
}
if (auth == "none" || whitelisted(uri) || forwarded || session?.getAttribute("logged") != null) { if (auth == "none" || whitelisted(uri) || forwarded || session?.getAttribute("logged") != null) {
chain.doFilter(req, resp) chain.doFilter(req, resp)
} else { } else {
// TODO - configure if unauth requests are redirected and/or forwarded
// TODO - protection against brute force attacks // TODO - protection against brute force attacks
if (uri.endsWith("/index")) { if (uri.endsWith("/index")) {
response.sendRedirect("/index-ffg") response.sendRedirect("/index-ffg")

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.web package org.jeudego.pairgoth.web
import org.apache.commons.lang3.tuple.Pair import org.apache.commons.lang3.tuple.Pair
import org.jeudego.pairgoth.oauth.OauthHelperFactory
import org.jeudego.pairgoth.ratings.RatingsManager import org.jeudego.pairgoth.ratings.RatingsManager
import org.jeudego.pairgoth.util.Translator import org.jeudego.pairgoth.util.Translator
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -69,11 +70,20 @@ class WebappManager : ServletContextListener, ServletContextAttributeListener, H
// publish some properties to the webapp context; for easy access from the template // publish some properties to the webapp context; for easy access from the template
context.setAttribute("env", properties.getProperty("env") ?: "dev") context.setAttribute("env", properties.getProperty("env") ?: "dev")
context.setAttribute("version", properties.getProperty("version") ?: "?") context.setAttribute("version", properties.getProperty("version") ?: "?")
properties.get("auth")?.let { val auth = properties.getProperty("auth") ?: "none"
context.setAttribute("auth", auth)
} when (auth) {
"none", "sesame" -> {}
"oauth" -> {
properties.getProperty("oauth.providers")?.let { properties.getProperty("oauth.providers")?.let {
context.setAttribute("oauth.providers", it.split(Regex("\\s*,\\s*"))) val providers = it.split(Regex("\\s*,\\s*"))
context.setAttribute("oauthProviders", providers)
providers.forEach { provider ->
context.setAttribute("${provider}Provider", OauthHelperFactory.getHelper(provider))
}
}
}
else -> throw Error("Unhandled auth: $auth")
} }
// set system user agent string to empty string // set system user agent string to empty string

View File

@@ -1,3 +1,5 @@
#if($auth == 'sesame')
<div id="login" class="section"> <div id="login" class="section">
<form id="login-form" class="ui form" autocomplete="off"> <form id="login-form" class="ui form" autocomplete="off">
<div class="field"> <div class="field">
@@ -9,8 +11,8 @@
</form> </form>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
// #[[
onLoad(()=> { onLoad(()=> {
// #[[
$('#login-form').on('submit', e => { $('#login-form').on('submit', e => {
api.postJson('login', {sesame: $('input[name="sesame"]')[0].value}) api.postJson('login', {sesame: $('input[name="sesame"]')[0].value})
.then(resp => { .then(resp => {
@@ -21,6 +23,38 @@
e.preventDefault(); e.preventDefault();
return false; return false;
}); });
// ]]#
});
</script>
#elseif($auth == 'oauth')
<div id="login" class="section">
<div>Log in using</div>
<div id="oauth-buttons">
#foreach($provider in $oauthProviders)
<div>
<button id="login-$provider" class="ui floating basic button">$provider</button>
</div>
#end
</div>
</div>
<script type="text/javascript">
onLoad(()=> {
#foreach($provider in $oauthProviders)
let buttonId = '#login-$provider';
let loginURL= '$application.getAttribute("${provider}Provider").getLoginURL($session.id)';
// #[[
console.log(`buttonId = ${buttonId}`);
console.log(`loginURL = ${loginURL}`);
$(buttonId).on('click', e => {
document.location.href = loginURL;
}); });
// ]]# // ]]#
#end
});
</script> </script>
#end