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.oauth.providers=ffg \
-Dpairgoth.oauth.ffg.secret=43f3a67bffcb5054d2f1b0e2a2374bdc \
-Dwebapp.external.url=http://localhost:8080
-Dwebapp.external.url=http://localhost:8080 \
--projects view-webapp package jetty:run
kill $CSSWATCH

View File

@@ -71,7 +71,8 @@
<pairgoth.webapp.host>localhost</pairgoth.webapp.host>
<pairgoth.webapp.port>8080</pairgoth.webapp.port>
<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.host>localhost</pairgoth.api.host>
<pairgoth.api.port>8085</pairgoth.api.port>

View File

@@ -230,7 +230,13 @@
<version>70.1</version>
</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>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>

View File

@@ -1,29 +1,43 @@
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() {
override val name: String
get() = "facebook"
get() = "ffg"
override val clientId = "pairgoth"
private val FFG_HOST = "https://testffg"
override fun getLoginURL(sessionId: String?): String {
return "$FFG_HOST/oauth2/entry_point.php/authorize?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&response_type=code" +
"&redirect_uri=" + URLEncoder.encode(redirectURI, "UTF-8") +
"&scope=email"
}
override fun getAccessTokenURL(code: String): String? {
return "$FFG_HOST/oauth2/entry_point.php/access_token?" +
"client_id=" + clientId +
"&redirect_uri=" + redirectURI +
"&client_secret=" + secret +
"&code=" + code
override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return Pair(
"$FFG_HOST/oauth2/entry_point.php/access_token",
listOf(
param("client_id", clientId),
param("redirect_uri", redirectURI),
param("client_secret", secret),
param("code", code),
param("grant_type", "authorization_code")
)
)
}
override fun getUserInfosURL(accessToken: String): String? {
return "$FFG_HOST/oauth2/entry_point.php/user_info?" +
"field=email" +
"&access_token=" + accessToken
override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
return Pair(
"$FFG_HOST/oauth2/entry_point.php/user_info",
listOf(header("Authorization", "Bearer $accessToken"))
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package org.jeudego.pairgoth.oauth
import org.apache.http.NameValuePair
class TwitterHelper : OAuthHelper() {
override val name: String
get() = "twitter"
@@ -8,11 +10,11 @@ class TwitterHelper : OAuthHelper() {
return ""
}
override fun getAccessTokenURL(code: String): String? {
return null
override fun getAccessTokenURL(code: String): Pair<String, List<NameValuePair>> {
return Pair("", listOf())
}
override fun getUserInfosURL(accessToken: String): String? {
return null
override fun getUserInfosURL(accessToken: String): Pair<String, List<NameValuePair>> {
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.methods.*
import org.apache.http.config.SocketConfig
import org.apache.http.conn.ssl.NoopHostnameVerifier
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.entity.ContentType
import org.apache.http.entity.EntityTemplate
@@ -54,7 +55,8 @@ private val client = HttpClients.custom()
SSLConnectionSocketFactory(
SSLContexts.createSystemDefault(), arrayOf("TLSv1.2"),
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
NoopHostnameVerifier()
//SSLConnectionSocketFactory.getDefaultHostnameVerifier()
)
)
.setConnectionTimeToLive(1, TimeUnit.MINUTES)

View File

@@ -1,5 +1,6 @@
package org.jeudego.pairgoth.web
import org.jeudego.pairgoth.oauth.OauthHelperFactory
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
@@ -30,10 +31,19 @@ class AuthFilter: Filter {
val auth = WebappManager.getProperty("auth") ?: throw Error("authentication not configured")
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) {
chain.doFilter(req, resp)
} else {
// TODO - configure if unauth requests are redirected and/or forwarded
// TODO - protection against brute force attacks
if (uri.endsWith("/index")) {
response.sendRedirect("/index-ffg")

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.web
import org.apache.commons.lang3.tuple.Pair
import org.jeudego.pairgoth.oauth.OauthHelperFactory
import org.jeudego.pairgoth.ratings.RatingsManager
import org.jeudego.pairgoth.util.Translator
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
context.setAttribute("env", properties.getProperty("env") ?: "dev")
context.setAttribute("version", properties.getProperty("version") ?: "?")
properties.get("auth")?.let {
}
properties.getProperty("oauth.providers")?.let {
context.setAttribute("oauth.providers", it.split(Regex("\\s*,\\s*")))
val auth = properties.getProperty("auth") ?: "none"
context.setAttribute("auth", auth)
when (auth) {
"none", "sesame" -> {}
"oauth" -> {
properties.getProperty("oauth.providers")?.let {
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

View File

@@ -1,3 +1,5 @@
#if($auth == 'sesame')
<div id="login" class="section">
<form id="login-form" class="ui form" autocomplete="off">
<div class="field">
@@ -9,10 +11,10 @@
</form>
</div>
<script type="text/javascript">
// #[[
onLoad(()=>{
onLoad(()=> {
// #[[
$('#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 => {
if (resp !== 'error' && resp.status === 'ok') {
document.location.href = '/index'
@@ -21,6 +23,38 @@
e.preventDefault();
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>
#end