diff --git a/google/oauth-http/build.gradle.kts b/google/oauth-http/build.gradle.kts new file mode 100644 index 00000000..fdd58941 --- /dev/null +++ b/google/oauth-http/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +plugins { + alias(libs.plugins.jetbrains.kotlin.multiplatform) +} + +kotlin { + jvm() + + sourceSets { + commonMain.dependencies { + api(projects.google.oauth) + + implementation(libs.bundles.ktor.client) + implementation(libs.bundles.ktor.server) + } + } +} diff --git a/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt b/google/oauth-http/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt similarity index 97% rename from google/oauth/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt rename to google/oauth-http/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt index 7f23bb48..a607fadb 100644 --- a/google/oauth/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt +++ b/google/oauth-http/src/commonMain/kotlin/net/opatry/google/auth/HttpGoogleAuthenticator.kt @@ -59,12 +59,14 @@ import io.ktor.server.cio.CIO as ServerEngineCIO * A Google OAuth2 authenticator using a localhost HTTP server for redirect URL interception and * HTTP client for auth & token requests. */ -open class HttpGoogleAuthenticator(private val config: ApplicationConfig) : GoogleAuthenticator { +class HttpGoogleAuthenticator(private val config: ApplicationConfig) : GoogleAuthenticator { /** * @property redirectUrl Redirect url * @property clientId OAuth2 Client ID * @property clientSecret OAuth2 Client Secret + * @property authUri OAuth2 Authorization URI + * @property tokenUri OAuth2 Token URI */ data class ApplicationConfig( val redirectUrl: String, diff --git a/google/oauth/build.gradle.kts b/google/oauth/build.gradle.kts index 61e47b5c..ce1bf903 100644 --- a/google/oauth/build.gradle.kts +++ b/google/oauth/build.gradle.kts @@ -30,8 +30,7 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.bundles.ktor.client) - implementation(libs.bundles.ktor.server) + implementation(libs.kotlinx.serialization) } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5bfb402b..fa3e8b82 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "Taskfolio" include(":google:oauth") +include(":google:oauth-http") include(":google:tasks") include(":lucide-icons") include(":tasks-core") diff --git a/tasks-app-android/src/main/assets/licenses_android.json b/tasks-app-android/src/main/assets/licenses_android.json index 78767472..ef853f5f 100644 --- a/tasks-app-android/src/main/assets/licenses_android.json +++ b/tasks-app-android/src/main/assets/licenses_android.json @@ -1877,23 +1877,6 @@ "Apache-2.0" ] }, - { - "uniqueId": "com.typesafe:config", - "developers": [ - { - "name": "Havoc Pennington" - } - ], - "artifactVersion": "1.4.3", - "description": "configuration library for JVM languages using HOCON files", - "name": "config", - "licenses": [ - "Apache-2.0" - ], - "organization": { - "name": "com.typesafe" - } - }, { "uniqueId": "io.coil-kt.coil3:coil-android", "developers": [ @@ -2272,34 +2255,6 @@ "Apache-2.0" ] }, - { - "uniqueId": "io.ktor:ktor-server-cio-jvm", - "developers": [ - { - "name": "Jetbrains Team" - } - ], - "artifactVersion": "3.0.0", - "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", - "name": "ktor-server-cio", - "licenses": [ - "Apache-2.0" - ] - }, - { - "uniqueId": "io.ktor:ktor-server-core-jvm", - "developers": [ - { - "name": "Jetbrains Team" - } - ], - "artifactVersion": "3.0.0", - "description": "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort.", - "name": "ktor-server-core", - "licenses": [ - "Apache-2.0" - ] - }, { "uniqueId": "io.ktor:ktor-sse-jvm", "developers": [ @@ -2394,26 +2349,6 @@ "name": "JUnit" } }, - { - "uniqueId": "org.fusesource.jansi:jansi", - "developers": [ - { - "name": "Hiram Chirino" - }, - { - "name": "Guillaume Nodet" - } - ], - "artifactVersion": "2.4.1", - "description": "Jansi is a java library for generating and interpreting ANSI escape sequences.", - "name": "Jansi", - "licenses": [ - "Apache-2.0" - ], - "organization": { - "name": "FuseSource, Corp." - } - }, { "uniqueId": "org.hamcrest:hamcrest-core", "developers": [ diff --git a/tasks-app-shared/build.gradle.kts b/tasks-app-shared/build.gradle.kts index 18f7a6c7..33cf416c 100644 --- a/tasks-app-shared/build.gradle.kts +++ b/tasks-app-shared/build.gradle.kts @@ -60,7 +60,7 @@ kotlin { } jvmMain.dependencies { - implementation(libs.bundles.ktor.server) + implementation(projects.google.oauthHttp) } commonMain.dependencies { diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/auth/PlayServicesGoogleAuthenticator.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/auth/PlayServicesGoogleAuthenticator.kt index 98678431..edda609e 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/auth/PlayServicesGoogleAuthenticator.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/auth/PlayServicesGoogleAuthenticator.kt @@ -26,9 +26,21 @@ import android.content.Context import com.google.android.gms.auth.api.identity.AuthorizationRequest import com.google.android.gms.auth.api.identity.Identity import com.google.android.gms.common.api.Scope +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.CurlUserAgent +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.statement.bodyAsText +import io.ktor.http.ContentType +import io.ktor.http.contentType +import io.ktor.http.isSuccess +import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.withTimeout import net.opatry.google.auth.GoogleAuthenticator -import net.opatry.google.auth.HttpGoogleAuthenticator import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -38,7 +50,32 @@ import kotlin.time.Duration.Companion.minutes class PlayServicesGoogleAuthenticator( private val context: Context, private val config: ApplicationConfig -) : HttpGoogleAuthenticator(config) { +) : GoogleAuthenticator { + + /** + * @property redirectUrl Redirect url + * @property clientId OAuth2 Client ID + * @property clientSecret OAuth2 Client Secret + * @property authUri OAuth2 Authorization URI + * @property tokenUri OAuth2 Token URI + */ + data class ApplicationConfig( + val redirectUrl: String, + val clientId: String, + val clientSecret: String, + val authUri: String, + val tokenUri: String, + ) + + private val httpClient: HttpClient by lazy { + HttpClient(CIO) { + CurlUserAgent() + install(ContentNegotiation) { + json() + } + } + } + override suspend fun authorize( scopes: List, force: Boolean, @@ -68,4 +105,30 @@ class PlayServicesGoogleAuthenticator( } } } + + override suspend fun getToken(grant: GoogleAuthenticator.Grant): GoogleAuthenticator.OAuthToken { + val response = httpClient.post(config.tokenUri) { + parameter("client_id", config.clientId) + parameter("client_secret", config.clientSecret) + parameter("grant_type", grant.type) + when (grant) { + is GoogleAuthenticator.Grant.AuthorizationCode -> { + parameter("code", grant.code) + // TODO PKCE "code_verifier" + parameter("redirect_uri", config.redirectUrl) + } + + is GoogleAuthenticator.Grant.RefreshToken -> { + parameter("refresh_token", grant.refreshToken) + } + } + contentType(ContentType.Application.FormUrlEncoded) + } + + if (response.status.isSuccess()) { + return response.body() + } else { + throw ClientRequestException(response, response.bodyAsText()) + } + } } \ No newline at end of file diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/di/authModule.android.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/di/authModule.android.kt index a1c30a5f..e9f45c6f 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/di/authModule.android.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/di/authModule.android.kt @@ -28,8 +28,8 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import net.opatry.google.auth.GoogleAuth import net.opatry.google.auth.GoogleAuthenticator -import net.opatry.google.auth.HttpGoogleAuthenticator.ApplicationConfig import net.opatry.tasks.app.auth.PlayServicesGoogleAuthenticator +import net.opatry.tasks.app.auth.PlayServicesGoogleAuthenticator.ApplicationConfig import org.koin.dsl.module diff --git a/tasks-core/build.gradle.kts b/tasks-core/build.gradle.kts index fa1d2f5f..72136894 100644 --- a/tasks-core/build.gradle.kts +++ b/tasks-core/build.gradle.kts @@ -37,7 +37,6 @@ kotlin { api(libs.kotlinx.datetime) implementation(libs.bundles.ktor.client) - implementation(libs.bundles.ktor.server) implementation(projects.google.oauth) implementation(projects.google.tasks)