From 94359ce994c9486fd1fcae189b28d73b012ee4b0 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 16:23:32 +0200 Subject: [PATCH 01/10] Add support for third party auth and add Slack OIDC provider --- .../kotlin/io/github/jan/supabase/gotrue/Auth.kt | 2 +- .../supabase/gotrue/AuthenticatedSupabaseApi.kt | 6 +++++- .../jan/supabase/gotrue/providers/Providers.kt | 6 ++++++ .../github/jan/supabase/AccessTokenProvider.kt | 14 ++++++++++++++ .../io/github/jan/supabase/SupabaseClient.kt | 6 +++++- .../github/jan/supabase/SupabaseClientBuilder.kt | 16 +++++++++++++++- 6 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt index 3ed62db8b..bfd66db4c 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt @@ -442,7 +442,7 @@ sealed interface Auth : MainPlugin, CustomSerializationPlugin { * The Auth plugin handles everything related to Supabase's authentication system */ val SupabaseClient.auth: Auth - get() = pluginManager.getPlugin(Auth) + get() = if(accessTokenProvider == null) pluginManager.getPlugin(Auth) else error("The Auth plugin is not available when using a custom access token provider") private suspend fun Auth.tryToGetUser(jwt: String) = try { retrieveUser(jwt) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt index e7151684e..d15dbe590 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt @@ -21,7 +21,11 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse = super.rawRequest(url) { - val jwtToken = jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey + val jwtToken = jwtToken ?: if(supabaseClient.accessTokenProvider != null) { + supabaseClient.accessTokenProvider!!() + } else { + supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() + } ?: supabaseClient.supabaseKey bearerAuth(jwtToken) builder() defaultRequest?.invoke(this) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/providers/Providers.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/providers/Providers.kt index 93df63865..54f19f1bf 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/providers/Providers.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/providers/Providers.kt @@ -55,6 +55,12 @@ data object Slack : OAuthProvider() { } +data object SlackOIDC : OAuthProvider() { + + override val name = "slack_oidc" + +} + data object Twitch : OAuthProvider() { override val name = "twitch" diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt new file mode 100644 index 000000000..3d736a008 --- /dev/null +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt @@ -0,0 +1,14 @@ +package io.github.jan.supabase + +/** + * Optional function for using a third-party authentication system with + * Supabase. The function should return an access token or ID token (JWT) by + * obtaining it from the third-party auth client library. Note that this + * function may be called concurrently and many times. Use memoization and + * locking techniques if this is not supported by the client libraries. + * + * When set, the Auth plugin from `auth-kt` cannot be used. + * Create another client if you wish to use Supabase Auth and third-party + * authentications concurrently in the same application. + */ +typealias AccessTokenProvider = () -> String \ No newline at end of file diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt index e875e0c9a..c89a6668f 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt @@ -39,7 +39,7 @@ sealed interface SupabaseClient { val pluginManager: PluginManager /** - * The http client used to interact with the supabase api + * The http client used to interact with the Supabase api */ val httpClient: KtorSupabaseHttpClient @@ -53,6 +53,9 @@ sealed interface SupabaseClient { */ val defaultSerializer: SupabaseSerializer + @SupabaseInternal + val accessTokenProvider: AccessTokenProvider? + /** * Releases all resources held by the [httpClient] and all plugins the [pluginManager] */ @@ -88,6 +91,7 @@ internal class SupabaseClientImpl( requestTimeout: Long, httpEngine: HttpClientEngine?, override val defaultSerializer: SupabaseSerializer, + override val accessTokenProvider: AccessTokenProvider?, ) : SupabaseClient { init { diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt index 023282acf..282ea8290 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt @@ -66,6 +66,19 @@ class SupabaseClientBuilder @PublishedApi internal constructor(private val supab */ var defaultSerializer: SupabaseSerializer = KotlinXSerializer(Json { ignoreUnknownKeys = true }) + /** + * Optional function for using a third-party authentication system with + * Supabase. The function should return an access token or ID token (JWT) by + * obtaining it from the third-party auth client library. Note that this + * function may be called concurrently and many times. Use memoization and + * locking techniques if this is not supported by the client libraries. + * + * When set, the Auth plugin from `auth-kt` cannot be used. + * Create another client if you wish to use Supabase Auth and third-party + * authentications concurrently in the same application. + */ + var accessToken: AccessTokenProvider? = null + private val httpConfigOverrides = mutableListOf.() -> Unit>() private val plugins = mutableMapOf SupabasePlugin<*>)>() @@ -95,7 +108,8 @@ class SupabaseClientBuilder @PublishedApi internal constructor(private val supab useHTTPS, requestTimeout.inWholeMilliseconds, httpEngine, - defaultSerializer + defaultSerializer, + accessToken ) } From dded345fe8d5daf81391b332fa6eae4bbb0230cb Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 16:30:47 +0200 Subject: [PATCH 02/10] Move error to Auth instance creation --- .../src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt | 2 +- .../commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt index bfd66db4c..3ed62db8b 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/Auth.kt @@ -442,7 +442,7 @@ sealed interface Auth : MainPlugin, CustomSerializationPlugin { * The Auth plugin handles everything related to Supabase's authentication system */ val SupabaseClient.auth: Auth - get() = if(accessTokenProvider == null) pluginManager.getPlugin(Auth) else error("The Auth plugin is not available when using a custom access token provider") + get() = pluginManager.getPlugin(Auth) private suspend fun Auth.tryToGetUser(jwt: String) = try { retrieveUser(jwt) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt index b62c93338..413632477 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt @@ -87,6 +87,7 @@ internal class AuthImpl( get() = Auth.key override fun init() { + if(supabaseClient.accessTokenProvider != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") setupPlatform() if (config.autoLoadFromStorage) { authScope.launch { From 3cf95ce68dad81a210740f2b85dec96d668361ad Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 16:31:27 +0200 Subject: [PATCH 03/10] Use init block --- .../kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt index 413632477..95a527d2b 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt @@ -86,8 +86,11 @@ internal class AuthImpl( override val pluginKey: String get() = Auth.key - override fun init() { + init { if(supabaseClient.accessTokenProvider != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") + } + + override fun init() { setupPlatform() if (config.autoLoadFromStorage) { authScope.launch { From 9ae53235d0c0e60586bebf59d26360fd824bbf81 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 17:32:14 +0200 Subject: [PATCH 04/10] Add a test and make AccessTokenProvider suspending --- .../gotrue/AuthenticatedSupabaseApi.kt | 18 ++++++++------- .../jan/supabase/AccessTokenProvider.kt | 2 +- .../commonTest/kotlin/SupabaseClientTest.kt | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt index d15dbe590..cb6b5fee3 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt @@ -20,15 +20,17 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( private val jwtToken: String? = null // Can be configured plugin-wide. By default, all plugins use the token from the current session ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { - override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse = super.rawRequest(url) { - val jwtToken = jwtToken ?: if(supabaseClient.accessTokenProvider != null) { + override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { + val customAccessToken = if(supabaseClient.accessTokenProvider != null) { supabaseClient.accessTokenProvider!!() - } else { - supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() - } ?: supabaseClient.supabaseKey - bearerAuth(jwtToken) - builder() - defaultRequest?.invoke(this) + } else null + return super.rawRequest(url) { + // 1. A custom token in the plugin config 2. The token from a third party provider 3. The token from the current session (Supabase Auth) 4. The default token from the client + val jwtToken = jwtToken ?: customAccessToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey + bearerAuth(jwtToken) + builder() + defaultRequest?.invoke(this) + } } suspend fun rawRequest(builder: HttpRequestBuilder.() -> Unit): HttpResponse = rawRequest("", builder) diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt index 3d736a008..b35298683 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt @@ -11,4 +11,4 @@ package io.github.jan.supabase * Create another client if you wish to use Supabase Auth and third-party * authentications concurrently in the same application. */ -typealias AccessTokenProvider = () -> String \ No newline at end of file +typealias AccessTokenProvider = suspend () -> String \ No newline at end of file diff --git a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt index aed85d335..25d5447da 100644 --- a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt +++ b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt @@ -30,6 +30,28 @@ class SupabaseClientTest { } } + @Test + fun testAccessTokenProvider() { + runTest { + val client = createMockedSupabaseClient( + requestHandler = { + assertEquals( + "supabase-kt/${BuildConfig.PROJECT_VERSION}", + it.headers["X-Client-Info"], + "X-Client-Info header should be set to 'supabase-kt/${BuildConfig.PROJECT_VERSION}'" + ) + respond("") + }, + configuration = { + accessToken = { + "myToken" + } + } + ) + assertEquals("myToken", client.accessTokenProvider?.invoke()) + } + } + @Test fun testDefaultLogLevel() { createMockedSupabaseClient( From b28502168e9d3db78b3fdaa45df9fedd93b3f35e Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 17:33:17 +0200 Subject: [PATCH 05/10] Remove unused code in test --- test-common/src/commonTest/kotlin/SupabaseClientTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt index 25d5447da..3e142807f 100644 --- a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt +++ b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt @@ -34,14 +34,6 @@ class SupabaseClientTest { fun testAccessTokenProvider() { runTest { val client = createMockedSupabaseClient( - requestHandler = { - assertEquals( - "supabase-kt/${BuildConfig.PROJECT_VERSION}", - it.headers["X-Client-Info"], - "X-Client-Info header should be set to 'supabase-kt/${BuildConfig.PROJECT_VERSION}'" - ) - respond("") - }, configuration = { accessToken = { "myToken" From be4ac3f593556952b21166727ebc697971ee51e2 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 12 Aug 2024 17:41:44 +0200 Subject: [PATCH 06/10] Add test for using Auth with an access token --- GoTrue/src/commonTest/kotlin/AuthTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/GoTrue/src/commonTest/kotlin/AuthTest.kt b/GoTrue/src/commonTest/kotlin/AuthTest.kt index 09415de3c..3b68fbc27 100644 --- a/GoTrue/src/commonTest/kotlin/AuthTest.kt +++ b/GoTrue/src/commonTest/kotlin/AuthTest.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.buildJsonObject import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNull @@ -46,6 +47,22 @@ class AuthTest { } } + @Test + fun testErrorWhenUsingAccessToken() { + runTest { + assertFailsWith { + createMockedSupabaseClient( + configuration = { + accessToken = { + "myToken" + } + install(Auth) + } + ) + } + } + } + @Test fun testSavingSessionToStorage() { runTest { From e6bcb147be8cfc877a86f7ed1c20af6f9f4d51e7 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Aug 2024 12:02:09 +0200 Subject: [PATCH 07/10] Clean up code and add realtime & storage support --- .../github/jan/supabase/gotrue/AccessToken.kt | 36 +++++++++ .../io/github/jan/supabase/gotrue/AuthImpl.kt | 2 +- .../gotrue/AuthenticatedSupabaseApi.kt | 8 +- .../src/commonTest/kotlin/AccessTokenTest.kt | 73 +++++++++++++++++++ .../supabase/realtime/RealtimeChannelImpl.kt | 7 +- .../github/jan/supabase/storage/BucketApi.kt | 6 +- .../io/github/jan/supabase/SupabaseClient.kt | 7 +- .../commonTest/kotlin/SupabaseClientTest.kt | 2 +- 8 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt create mode 100644 GoTrue/src/commonTest/kotlin/AccessTokenTest.kt diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt new file mode 100644 index 000000000..2cfe706c9 --- /dev/null +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt @@ -0,0 +1,36 @@ +package io.github.jan.supabase.gotrue + +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.plugins.MainConfig +import io.github.jan.supabase.plugins.MainPlugin + +/** + * Returns the access token used for requests. The token is resolved in the following order: + * 1. [jwtToken] if not null + * 2. [SupabaseClient.customAccessToken] if not null + * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed + * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true + */ +@SupabaseInternal +suspend fun SupabaseClient.accessToken( + jwtToken: String? = null, + keyAsFallback: Boolean = true +): String? { + val key = if(keyAsFallback) supabaseKey else null + return jwtToken ?: customAccessToken?.invoke() + ?: pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: key +} + +/** + * Returns the access token used for requests. The token is resolved in the following order: + * 1. [jwtToken] if not null + * 2. [SupabaseClient.customAccessToken] if not null + * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed + * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true + */ +@SupabaseInternal +suspend fun SupabaseClient.accessToken( + plugin: MainPlugin, + keyAsFallback: Boolean = true +) = accessToken(plugin.config.jwtToken, keyAsFallback) \ No newline at end of file diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt index 95a527d2b..f85a3a327 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt @@ -87,7 +87,7 @@ internal class AuthImpl( get() = Auth.key init { - if(supabaseClient.accessTokenProvider != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") + if(supabaseClient.customAccessToken != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") } override fun init() { diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt index cb6b5fee3..811b6f943 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt @@ -21,13 +21,9 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { - val customAccessToken = if(supabaseClient.accessTokenProvider != null) { - supabaseClient.accessTokenProvider!!() - } else null + val accessToken = supabaseClient.accessToken(jwtToken) ?: error("No access token available") return super.rawRequest(url) { - // 1. A custom token in the plugin config 2. The token from a third party provider 3. The token from the current session (Supabase Auth) 4. The default token from the client - val jwtToken = jwtToken ?: customAccessToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey - bearerAuth(jwtToken) + bearerAuth(accessToken) builder() defaultRequest?.invoke(this) } diff --git a/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt b/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt new file mode 100644 index 000000000..0628ff935 --- /dev/null +++ b/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt @@ -0,0 +1,73 @@ +import io.github.jan.supabase.gotrue.Auth +import io.github.jan.supabase.gotrue.accessToken +import io.github.jan.supabase.gotrue.auth +import io.github.jan.supabase.gotrue.minimalSettings +import io.github.jan.supabase.testing.createMockedSupabaseClient +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class AccessTokenTest { + + @Test + fun testAccessTokenWithJwtToken() { + runTest { + val client = createMockedSupabaseClient( + configuration = { + install(Auth) { + minimalSettings() + } + } + ) + client.auth.importAuthToken("myAuth") //this should be ignored as per plugin tokens override the used access token + assertEquals("myJwtToken", client.accessToken("myJwtToken")) + } + } + + @Test + fun testAccessTokenWithKeyAsFallback() { + runTest { + val client = createMockedSupabaseClient(supabaseKey = "myKey") + assertEquals("myKey", client.accessToken()) + } + } + + @Test + fun testAccessTokenWithoutKey() { + runTest { + val client = createMockedSupabaseClient() + assertNull(client.accessToken(keyAsFallback = false)) + } + } + + @Test + fun testAccessTokenWithCustomAccessToken() { + runTest { + val client = createMockedSupabaseClient( + configuration = { + accessToken = { + "myCustomToken" + } + } + ) + assertEquals("myCustomToken", client.accessToken()) + } + } + + @Test + fun testAccessTokenWithAuth() { + runTest { + val client = createMockedSupabaseClient( + configuration = { + install(Auth) { + minimalSettings() + } + } + ) + client.auth.importAuthToken("myAuth") + assertEquals("myAuth", client.accessToken()) + } + } + +} \ No newline at end of file diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt index 747bd4cd8..b7851a9d6 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt @@ -3,7 +3,7 @@ package io.github.jan.supabase.realtime import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.collections.AtomicMutableList import io.github.jan.supabase.decodeIfNotEmptyOrDefault -import io.github.jan.supabase.gotrue.Auth +import io.github.jan.supabase.gotrue.accessToken import io.github.jan.supabase.logging.d import io.github.jan.supabase.logging.e import io.github.jan.supabase.logging.w @@ -17,7 +17,6 @@ import io.ktor.http.headers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first -import kotlinx.datetime.Clock import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.buildJsonObject @@ -61,9 +60,7 @@ internal class RealtimeChannelImpl( } _status.value = RealtimeChannel.Status.SUBSCRIBING Realtime.logger.d { "Subscribing to channel $topic" } - val currentJwt = realtimeImpl.config.jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentSessionOrNull()?.let { - if(it.expiresAt > Clock.System.now()) it.accessToken else null - } + val currentJwt = supabaseClient.accessToken(realtimeImpl, keyAsFallback = false) val postgrestChanges = clientChanges.toList() val joinConfig = RealtimeJoinPayload(RealtimeJoinConfig(broadcastJoinConfig, presenceJoinConfig, postgrestChanges, isPrivate)) val joinConfigObject = buildJsonObject { diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt index 7f3553716..b679f8b86 100644 --- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt +++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt @@ -3,7 +3,7 @@ package io.github.jan.supabase.storage import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.exceptions.RestException -import io.github.jan.supabase.gotrue.Auth +import io.github.jan.supabase.gotrue.accessToken import io.github.jan.supabase.storage.resumable.ResumableClient import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.utils.io.ByteReadChannel @@ -314,8 +314,8 @@ sealed interface BucketApi { * **Authentication: Bearer ** * @param path The path to download */ -fun BucketApi.authenticatedRequest(path: String): Pair { +suspend fun BucketApi.authenticatedRequest(path: String): Pair { val url = authenticatedUrl(path) - val token = supabaseClient.storage.config.jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey + val token = supabaseClient.accessToken(supabaseClient.storage) ?: error("No access token available") return token to url } \ No newline at end of file diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt index c89a6668f..44937a3d4 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt @@ -53,8 +53,11 @@ sealed interface SupabaseClient { */ val defaultSerializer: SupabaseSerializer + /** + * The custom access token provider used to provide custom access tokens for requests. Configured within the [SupabaseClientBuilder] + */ @SupabaseInternal - val accessTokenProvider: AccessTokenProvider? + val customAccessToken: AccessTokenProvider? /** * Releases all resources held by the [httpClient] and all plugins the [pluginManager] @@ -91,7 +94,7 @@ internal class SupabaseClientImpl( requestTimeout: Long, httpEngine: HttpClientEngine?, override val defaultSerializer: SupabaseSerializer, - override val accessTokenProvider: AccessTokenProvider?, + override val customAccessToken: AccessTokenProvider?, ) : SupabaseClient { init { diff --git a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt index 3e142807f..617e5af93 100644 --- a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt +++ b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt @@ -40,7 +40,7 @@ class SupabaseClientTest { } } ) - assertEquals("myToken", client.accessTokenProvider?.invoke()) + assertEquals("myToken", client.customAccessToken?.invoke()) } } From 81faa9a52ac995ab63bda78d04d244c16d58ad80 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Aug 2024 12:04:01 +0200 Subject: [PATCH 08/10] Improve docs --- .../kotlin/io/github/jan/supabase/AccessTokenProvider.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt index b35298683..7d0afd14b 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/AccessTokenProvider.kt @@ -6,9 +6,5 @@ package io.github.jan.supabase * obtaining it from the third-party auth client library. Note that this * function may be called concurrently and many times. Use memoization and * locking techniques if this is not supported by the client libraries. - * - * When set, the Auth plugin from `auth-kt` cannot be used. - * Create another client if you wish to use Supabase Auth and third-party - * authentications concurrently in the same application. */ typealias AccessTokenProvider = suspend () -> String \ No newline at end of file From 9a7916fd2ed5278798a368b1201f0b7cc19973cc Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Aug 2024 12:31:10 +0200 Subject: [PATCH 09/10] Rename methods --- .../io/github/jan/supabase/gotrue/AccessToken.kt | 12 ++++++------ .../kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt | 2 +- .../jan/supabase/gotrue/AuthenticatedSupabaseApi.kt | 2 +- GoTrue/src/commonTest/kotlin/AccessTokenTest.kt | 12 ++++++------ .../jan/supabase/realtime/RealtimeChannelImpl.kt | 4 ++-- .../io/github/jan/supabase/storage/BucketApi.kt | 4 ++-- .../kotlin/io/github/jan/supabase/SupabaseClient.kt | 4 ++-- .../src/commonTest/kotlin/SupabaseClientTest.kt | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt index 2cfe706c9..d55c52620 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt @@ -8,29 +8,29 @@ import io.github.jan.supabase.plugins.MainPlugin /** * Returns the access token used for requests. The token is resolved in the following order: * 1. [jwtToken] if not null - * 2. [SupabaseClient.customAccessToken] if not null + * 2. [SupabaseClient.resolveAccessToken] if not null * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true */ @SupabaseInternal -suspend fun SupabaseClient.accessToken( +suspend fun SupabaseClient.resolveAccessToken( jwtToken: String? = null, keyAsFallback: Boolean = true ): String? { val key = if(keyAsFallback) supabaseKey else null - return jwtToken ?: customAccessToken?.invoke() + return jwtToken ?: accessToken?.invoke() ?: pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: key } /** * Returns the access token used for requests. The token is resolved in the following order: * 1. [jwtToken] if not null - * 2. [SupabaseClient.customAccessToken] if not null + * 2. [SupabaseClient.resolveAccessToken] if not null * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true */ @SupabaseInternal -suspend fun SupabaseClient.accessToken( +suspend fun SupabaseClient.resolveAccessToken( plugin: MainPlugin, keyAsFallback: Boolean = true -) = accessToken(plugin.config.jwtToken, keyAsFallback) \ No newline at end of file +) = resolveAccessToken(plugin.config.jwtToken, keyAsFallback) \ No newline at end of file diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt index f85a3a327..71c77e602 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthImpl.kt @@ -87,7 +87,7 @@ internal class AuthImpl( get() = Auth.key init { - if(supabaseClient.customAccessToken != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") + if(supabaseClient.accessToken != null) error("The Auth plugin is not available when using a custom access token provider. Please uninstall the Auth plugin.") } override fun init() { diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt index 811b6f943..424afc341 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AuthenticatedSupabaseApi.kt @@ -21,7 +21,7 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { - val accessToken = supabaseClient.accessToken(jwtToken) ?: error("No access token available") + val accessToken = supabaseClient.resolveAccessToken(jwtToken) ?: error("No access token available") return super.rawRequest(url) { bearerAuth(accessToken) builder() diff --git a/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt b/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt index 0628ff935..2c962fa7f 100644 --- a/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt +++ b/GoTrue/src/commonTest/kotlin/AccessTokenTest.kt @@ -1,7 +1,7 @@ import io.github.jan.supabase.gotrue.Auth -import io.github.jan.supabase.gotrue.accessToken import io.github.jan.supabase.gotrue.auth import io.github.jan.supabase.gotrue.minimalSettings +import io.github.jan.supabase.gotrue.resolveAccessToken import io.github.jan.supabase.testing.createMockedSupabaseClient import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -21,7 +21,7 @@ class AccessTokenTest { } ) client.auth.importAuthToken("myAuth") //this should be ignored as per plugin tokens override the used access token - assertEquals("myJwtToken", client.accessToken("myJwtToken")) + assertEquals("myJwtToken", client.resolveAccessToken("myJwtToken")) } } @@ -29,7 +29,7 @@ class AccessTokenTest { fun testAccessTokenWithKeyAsFallback() { runTest { val client = createMockedSupabaseClient(supabaseKey = "myKey") - assertEquals("myKey", client.accessToken()) + assertEquals("myKey", client.resolveAccessToken()) } } @@ -37,7 +37,7 @@ class AccessTokenTest { fun testAccessTokenWithoutKey() { runTest { val client = createMockedSupabaseClient() - assertNull(client.accessToken(keyAsFallback = false)) + assertNull(client.resolveAccessToken(keyAsFallback = false)) } } @@ -51,7 +51,7 @@ class AccessTokenTest { } } ) - assertEquals("myCustomToken", client.accessToken()) + assertEquals("myCustomToken", client.resolveAccessToken()) } } @@ -66,7 +66,7 @@ class AccessTokenTest { } ) client.auth.importAuthToken("myAuth") - assertEquals("myAuth", client.accessToken()) + assertEquals("myAuth", client.resolveAccessToken()) } } diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt index b7851a9d6..6b1c171e3 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/RealtimeChannelImpl.kt @@ -3,7 +3,7 @@ package io.github.jan.supabase.realtime import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.collections.AtomicMutableList import io.github.jan.supabase.decodeIfNotEmptyOrDefault -import io.github.jan.supabase.gotrue.accessToken +import io.github.jan.supabase.gotrue.resolveAccessToken import io.github.jan.supabase.logging.d import io.github.jan.supabase.logging.e import io.github.jan.supabase.logging.w @@ -60,7 +60,7 @@ internal class RealtimeChannelImpl( } _status.value = RealtimeChannel.Status.SUBSCRIBING Realtime.logger.d { "Subscribing to channel $topic" } - val currentJwt = supabaseClient.accessToken(realtimeImpl, keyAsFallback = false) + val currentJwt = supabaseClient.resolveAccessToken(realtimeImpl, keyAsFallback = false) val postgrestChanges = clientChanges.toList() val joinConfig = RealtimeJoinPayload(RealtimeJoinConfig(broadcastJoinConfig, presenceJoinConfig, postgrestChanges, isPrivate)) val joinConfigObject = buildJsonObject { diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt index b679f8b86..a4c7da748 100644 --- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt +++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt @@ -3,7 +3,7 @@ package io.github.jan.supabase.storage import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.exceptions.RestException -import io.github.jan.supabase.gotrue.accessToken +import io.github.jan.supabase.gotrue.resolveAccessToken import io.github.jan.supabase.storage.resumable.ResumableClient import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.utils.io.ByteReadChannel @@ -316,6 +316,6 @@ sealed interface BucketApi { */ suspend fun BucketApi.authenticatedRequest(path: String): Pair { val url = authenticatedUrl(path) - val token = supabaseClient.accessToken(supabaseClient.storage) ?: error("No access token available") + val token = supabaseClient.resolveAccessToken(supabaseClient.storage) ?: error("No access token available") return token to url } \ No newline at end of file diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt index 44937a3d4..f9bbd39de 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt @@ -57,7 +57,7 @@ sealed interface SupabaseClient { * The custom access token provider used to provide custom access tokens for requests. Configured within the [SupabaseClientBuilder] */ @SupabaseInternal - val customAccessToken: AccessTokenProvider? + val accessToken: AccessTokenProvider? /** * Releases all resources held by the [httpClient] and all plugins the [pluginManager] @@ -94,7 +94,7 @@ internal class SupabaseClientImpl( requestTimeout: Long, httpEngine: HttpClientEngine?, override val defaultSerializer: SupabaseSerializer, - override val customAccessToken: AccessTokenProvider?, + override val accessToken: AccessTokenProvider?, ) : SupabaseClient { init { diff --git a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt index 617e5af93..50a896852 100644 --- a/test-common/src/commonTest/kotlin/SupabaseClientTest.kt +++ b/test-common/src/commonTest/kotlin/SupabaseClientTest.kt @@ -40,7 +40,7 @@ class SupabaseClientTest { } } ) - assertEquals("myToken", client.customAccessToken?.invoke()) + assertEquals("myToken", client.accessToken?.invoke()) } } From 0459c0d9cdac22eb4d9fa2d84d800cd3f37eeea4 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 15 Aug 2024 17:31:29 +0200 Subject: [PATCH 10/10] improve docs --- .../kotlin/io/github/jan/supabase/gotrue/AccessToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt index d55c52620..0a3a4a537 100644 --- a/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt +++ b/GoTrue/src/commonMain/kotlin/io/github/jan/supabase/gotrue/AccessToken.kt @@ -24,7 +24,7 @@ suspend fun SupabaseClient.resolveAccessToken( /** * Returns the access token used for requests. The token is resolved in the following order: - * 1. [jwtToken] if not null + * 1. [MainConfig.jwtToken] if not null * 2. [SupabaseClient.resolveAccessToken] if not null * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true