From d355a4000883cf866e532e091f894199e0756d55 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Mon, 6 Oct 2025 21:52:20 +0200 Subject: [PATCH 1/5] Initial commit --- .../io/github/jan/supabase/auth/Auth.kt | 8 ++++ .../io/github/jan/supabase/auth/AuthConfig.kt | 8 ++++ .../auth/SessionNetworkInterceptor.kt | 29 ++++++++++++++ .../jan/supabase/SupabaseClientBuilder.kt | 10 ++++- .../jan/supabase/SupabaseClientConfig.kt | 2 + .../network/KtorSupabaseHttpClient.kt | 38 ++++++++++++++----- .../supabase/network/NetworkInterceptor.kt | 23 +++++++++++ 7 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt create mode 100644 Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt index 0b4d5ce44..09ebc0732 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt @@ -1,6 +1,7 @@ package io.github.jan.supabase.auth import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.SupabaseClientBuilder import io.github.jan.supabase.annotations.SupabaseExperimental import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.admin.AdminApi @@ -496,8 +497,15 @@ interface Auth : MainPlugin, CustomSerializationPlugin { const val API_VERSION = 1 override fun createConfig(init: AuthConfig.() -> Unit) = AuthConfig().apply(init) + override fun create(supabaseClient: SupabaseClient, config: AuthConfig): Auth = AuthImpl(supabaseClient, config) + override fun setup(builder: SupabaseClientBuilder, config: AuthConfig) { + if(config.checkSessionOnRequest) { + builder.networkInterceptors.add(SessionNetworkInterceptor) + } + } + } } diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index 1b4d5a738..594ba5911 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -103,6 +103,14 @@ open class AuthConfigDefaults : MainConfig() { @SupabaseExperimental var urlLauncher: UrlLauncher = UrlLauncher.DEFAULT + /** + * Whether to check if the current session is expired on an authenticated request and possibly try to refresh it. + * + * **Note: This option is experimental and is a fail-safe for when the auto refresh fails. This option may be removed without notice.** + */ + @SupabaseExperimental + var checkSessionOnRequest: Boolean = true + } /** diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt new file mode 100644 index 000000000..3574a4b02 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt @@ -0,0 +1,29 @@ +package io.github.jan.supabase.auth + +import io.github.jan.supabase.OSInformation +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.logging.e +import io.github.jan.supabase.network.NetworkInterceptor +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.http.HttpHeaders +import kotlin.time.Clock + +object SessionNetworkInterceptor: NetworkInterceptor.Before { + + override fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) { + val authHeader = builder.headers[HttpHeaders.Authorization]?.replace("Bearer ", "") + val currentSession = supabase.auth.currentSessionOrNull() + val sessionExistsAndExpired = authHeader == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < Clock.System.now() + val autoRefreshEnabled = supabase.auth.config.alwaysAutoRefresh + if(sessionExistsAndExpired && autoRefreshEnabled) { + val autoRefreshRunning = supabase.auth.isAutoRefreshRunning + Auth.logger.e { """ + Authenticated request attempted with expired access token. This should not happen. Please report this issue. Trying to refresh session before... + Auto refresh running: $autoRefreshRunning + OS: ${OSInformation.CURRENT} + Session: $currentSession + """.trimIndent() } + } + } + +} \ No newline at end of file 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 fa2efedd1..9cb9c746d 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt @@ -3,6 +3,7 @@ package io.github.jan.supabase import io.github.jan.supabase.annotations.SupabaseDsl import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.logging.LogLevel +import io.github.jan.supabase.network.NetworkInterceptor import io.github.jan.supabase.plugins.PluginManager import io.github.jan.supabase.plugins.SupabasePlugin import io.github.jan.supabase.plugins.SupabasePluginProvider @@ -95,6 +96,12 @@ class SupabaseClientBuilder @PublishedApi internal constructor(private val supab */ var osInformation: OSInformation? = OSInformation.CURRENT + /** + * A list of [NetworkInterceptor]s. Used for modifying requests or handling responses. + */ + @SupabaseInternal + var networkInterceptors = mutableListOf() + private val httpConfigOverrides = mutableListOf() private val plugins = mutableMapOf() @@ -124,7 +131,8 @@ class SupabaseClientBuilder @PublishedApi internal constructor(private val supab useHTTPS = useHTTPS, httpEngine = httpEngine, httpConfigOverrides = httpConfigOverrides, - requestTimeout = requestTimeout + requestTimeout = requestTimeout, + interceptors = networkInterceptors ), defaultSerializer = defaultSerializer, coroutineDispatcher = coroutineDispatcher, diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt index 3e6983ef1..d6e60ba1a 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt @@ -1,6 +1,7 @@ package io.github.jan.supabase import io.github.jan.supabase.logging.LogLevel +import io.github.jan.supabase.network.NetworkInterceptor import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher import kotlin.time.Duration @@ -21,5 +22,6 @@ internal data class SupabaseNetworkConfig( val useHTTPS: Boolean, val httpEngine: HttpClientEngine?, val httpConfigOverrides: List, + val interceptors: List, val requestTimeout: Duration ) diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt index 2db17767e..07a7e3fc6 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt @@ -4,6 +4,7 @@ package io.github.jan.supabase.network import io.github.jan.supabase.BuildConfig import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.SupabaseNetworkConfig import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.logging.d @@ -11,7 +12,6 @@ import io.github.jan.supabase.logging.e import io.github.jan.supabase.supabaseJson import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.client.plugins.HttpTimeout @@ -39,16 +39,21 @@ typealias HttpRequestOverride = HttpRequestBuilder.() -> Unit * A [SupabaseHttpClient] that uses ktor to send requests */ @OptIn(SupabaseInternal::class) -class KtorSupabaseHttpClient @SupabaseInternal constructor( +internal class KtorSupabaseHttpClient @SupabaseInternal constructor( private val supabaseKey: String, - modifiers: List.() -> Unit> = listOf(), - private val requestTimeout: Long, - engine: HttpClientEngine? = null, + private val networkConfig: SupabaseNetworkConfig, private val osInformation: OSInformation? ): SupabaseHttpClient() { + val requestTimeout = networkConfig.requestTimeout + val engine = networkConfig.httpEngine + val modifiers = networkConfig.httpConfigOverrides + + val beforeInterceptors = networkConfig.interceptors.filter { it is NetworkInterceptor.Before }.toTypedArray() + val afterInterceptors = networkConfig.interceptors.filter { it is NetworkInterceptor.After }.toTypedArray() + init { - SupabaseClient.LOGGER.d { "Creating KtorSupabaseHttpClient with request timeout $requestTimeout ms, HttpClientEngine: $engine" } + SupabaseClient.LOGGER.d { "Creating KtorSupabaseHttpClient with request timeout $requestTimeout, HttpClientEngine: $engine" } } @SupabaseInternal @@ -63,11 +68,11 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( } val endPoint = request.url.encodedPath SupabaseClient.LOGGER.d { "Starting ${request.method.value} request to endpoint $endPoint" } - + callBeforeInterceptors(request) val response = try { httpClient.request(url, builder) } catch(e: HttpRequestTimeoutException) { - SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint timed out after $requestTimeout ms" } + SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint timed out after $requestTimeout" } throw e } catch(e: CancellationException) { SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint was cancelled"} @@ -76,6 +81,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( SupabaseClient.LOGGER.e(e) { "${request.method.value} request to endpoint $endPoint failed with exception ${e.message}" } throw HttpRequestException(e.message ?: "", request) } + callAfterInterceptors(response) val responseTime = (response.responseTime.timestamp - response.requestTime.timestamp).milliseconds SupabaseClient.LOGGER.d { "${request.method.value} request to endpoint $endPoint successfully finished in $responseTime" } return response @@ -92,7 +98,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( val response = try { httpClient.prepareRequest(url, builder) } catch(e: HttpRequestTimeoutException) { - SupabaseClient.LOGGER.e { "Request timed out after $requestTimeout ms on url $url" } + SupabaseClient.LOGGER.e { "Request timed out after $requestTimeout on url $url" } throw e } catch(e: CancellationException) { SupabaseClient.LOGGER.e { "Request was cancelled on url $url" } @@ -106,6 +112,18 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( fun close() = httpClient.close() + private fun callBeforeInterceptors(requestBuilder: HttpRequestBuilder) { + beforeInterceptors.forEach { + it.call(requestBuilder, supabase) + } + } + + private fun callAfterInterceptors(response: HttpResponse) { + interceptors.forEach { + it.afterRequest(response) + } + } + private fun HttpClientConfig<*>.applyDefaultConfiguration(modifiers: List.() -> Unit>) { install(DefaultRequest) { headers { @@ -124,7 +142,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( json(supabaseJson) } install(HttpTimeout) { - requestTimeoutMillis = requestTimeout + requestTimeoutMillis = requestTimeout.inWholeMilliseconds } modifiers.forEach { it.invoke(this) } } diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt new file mode 100644 index 000000000..9ba30304b --- /dev/null +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt @@ -0,0 +1,23 @@ +package io.github.jan.supabase.network + +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.annotations.SupabaseInternal +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.statement.HttpResponse + +@SupabaseInternal +sealed interface NetworkInterceptor { + + fun interface Before: NetworkInterceptor { + + fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) + + } + + fun interface After: NetworkInterceptor { + + fun call(response: HttpResponse, supabase: SupabaseClient) + + } + +} \ No newline at end of file From d54902a411d8a68555e125272e2f66cb59bc1896 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Sat, 11 Oct 2025 23:49:31 +0200 Subject: [PATCH 2/5] add exception and improve http client --- .../auth/SessionNetworkInterceptor.kt | 10 ++++++- .../auth/exception/TokenExpiredException.kt | 4 +++ .../io/github/jan/supabase/SupabaseClient.kt | 13 +++++----- .../jan/supabase/SupabaseClientConfig.kt | 4 +-- .../network/KtorSupabaseHttpClient.kt | 26 +++++++++---------- .../supabase/network/NetworkInterceptor.kt | 4 +-- 6 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt index 3574a4b02..efdabcf08 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt @@ -2,6 +2,7 @@ package io.github.jan.supabase.auth import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.exceptions.RestException import io.github.jan.supabase.logging.e import io.github.jan.supabase.network.NetworkInterceptor import io.ktor.client.request.HttpRequestBuilder @@ -10,7 +11,7 @@ import kotlin.time.Clock object SessionNetworkInterceptor: NetworkInterceptor.Before { - override fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) { + override suspend fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) { val authHeader = builder.headers[HttpHeaders.Authorization]?.replace("Bearer ", "") val currentSession = supabase.auth.currentSessionOrNull() val sessionExistsAndExpired = authHeader == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < Clock.System.now() @@ -23,6 +24,13 @@ object SessionNetworkInterceptor: NetworkInterceptor.Before { OS: ${OSInformation.CURRENT} Session: $currentSession """.trimIndent() } + + //TODO: Exception logic + try { + supabase.auth.refreshCurrentSession() + } catch(e: RestException) { + Auth.logger.e(e) { "Failed to refresh session" } + } } } diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt new file mode 100644 index 000000000..2e6b99564 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt @@ -0,0 +1,4 @@ +package io.github.jan.supabase.auth.exception + +//TODO: Add actual message and docs +class TokenExpiredException: Exception("The token has expired") \ 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 061c6c156..c97d1e6bb 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt @@ -18,6 +18,11 @@ import kotlinx.coroutines.CoroutineDispatcher */ interface SupabaseClient { + /** + * The configuration for the Supabase Client. + */ + val config: SupabaseClientConfig + /** * The supabase url with either a http or https scheme. */ @@ -93,7 +98,7 @@ interface SupabaseClient { } internal class SupabaseClientImpl( - config: SupabaseClientConfig, + override val config: SupabaseClientConfig, ) : SupabaseClient { override val accessToken: AccessTokenProvider? = config.accessToken @@ -117,11 +122,7 @@ internal class SupabaseClientImpl( @OptIn(SupabaseInternal::class) override val httpClient = KtorSupabaseHttpClient( - supabaseKey, - config.networkConfig.httpConfigOverrides, - config.networkConfig.requestTimeout.inWholeMilliseconds, - config.networkConfig.httpEngine, - config.osInformation + this ) override val pluginManager = PluginManager(config.plugins.toList().associate { (key, value) -> diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt index d6e60ba1a..f5de8ff15 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt @@ -6,7 +6,7 @@ import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher import kotlin.time.Duration -internal data class SupabaseClientConfig( +data class SupabaseClientConfig( val supabaseUrl: String, val supabaseKey: String, val defaultLogLevel: LogLevel, @@ -18,7 +18,7 @@ internal data class SupabaseClientConfig( val osInformation: OSInformation? ) -internal data class SupabaseNetworkConfig( +data class SupabaseNetworkConfig( val useHTTPS: Boolean, val httpEngine: HttpClientEngine?, val httpConfigOverrides: List, diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt index 07a7e3fc6..be0f5f523 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt @@ -2,9 +2,7 @@ package io.github.jan.supabase.network import io.github.jan.supabase.BuildConfig -import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient -import io.github.jan.supabase.SupabaseNetworkConfig import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.logging.d @@ -39,18 +37,20 @@ typealias HttpRequestOverride = HttpRequestBuilder.() -> Unit * A [SupabaseHttpClient] that uses ktor to send requests */ @OptIn(SupabaseInternal::class) -internal class KtorSupabaseHttpClient @SupabaseInternal constructor( - private val supabaseKey: String, - private val networkConfig: SupabaseNetworkConfig, - private val osInformation: OSInformation? +class KtorSupabaseHttpClient @SupabaseInternal constructor( + private val supabase: SupabaseClient ): SupabaseHttpClient() { - val requestTimeout = networkConfig.requestTimeout - val engine = networkConfig.httpEngine - val modifiers = networkConfig.httpConfigOverrides + private val supabaseKey = supabase.supabaseKey + private val osInformation = supabase.config.osInformation - val beforeInterceptors = networkConfig.interceptors.filter { it is NetworkInterceptor.Before }.toTypedArray() - val afterInterceptors = networkConfig.interceptors.filter { it is NetworkInterceptor.After }.toTypedArray() + private val networkConfig = supabase.config.networkConfig + private val requestTimeout = networkConfig.requestTimeout + private val engine = networkConfig.httpEngine + private val modifiers = networkConfig.httpConfigOverrides + + private val beforeInterceptors = networkConfig.interceptors.filterIsInstance().toTypedArray() + private val afterInterceptors = networkConfig.interceptors.filterIsInstance().toTypedArray() init { SupabaseClient.LOGGER.d { "Creating KtorSupabaseHttpClient with request timeout $requestTimeout, HttpClientEngine: $engine" } @@ -119,8 +119,8 @@ internal class KtorSupabaseHttpClient @SupabaseInternal constructor( } private fun callAfterInterceptors(response: HttpResponse) { - interceptors.forEach { - it.afterRequest(response) + afterInterceptors.forEach { + it.call(response, supabase) } } diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt index 9ba30304b..0ef5ee258 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt @@ -10,13 +10,13 @@ sealed interface NetworkInterceptor { fun interface Before: NetworkInterceptor { - fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) + suspend fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) } fun interface After: NetworkInterceptor { - fun call(response: HttpResponse, supabase: SupabaseClient) + suspend fun call(response: HttpResponse, supabase: SupabaseClient) } From 10e756acc2816117cb3de328d0c893f960742137 Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Sun, 12 Oct 2025 00:01:15 +0200 Subject: [PATCH 3/5] Initial commit --- .../jan/supabase/auth/AuthDependentPluginConfig.kt | 13 +++++++++++++ .../io/github/jan/supabase/postgrest/Postgrest.kt | 4 +++- .../io/github/jan/supabase/realtime/Realtime.kt | 4 +++- .../io/github/jan/supabase/storage/Storage.kt | 6 ++++-- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt new file mode 100644 index 000000000..96dfc1467 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt @@ -0,0 +1,13 @@ +package io.github.jan.supabase.auth + +/** + * TODO + */ +interface AuthDependentPluginConfig { + + /** + * Whether to require a valid [io.github.jan.supabase.auth.user.UserSession] in the [Auth] plugin to make any request with this plugin. + */ + var requireValidSession: Boolean + +} \ No newline at end of file diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt index 960ef4e75..8f60628a7 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt @@ -2,6 +2,7 @@ package io.github.jan.supabase.postgrest import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseSerializer +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.logging.SupabaseLogger import io.github.jan.supabase.plugins.CustomSerializationConfig @@ -101,7 +102,8 @@ interface Postgrest : MainPlugin, CustomSerializationPlugin { data class Config( var defaultSchema: String = "public", var propertyConversionMethod: PropertyConversionMethod = PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE, - ): MainConfig(), CustomSerializationConfig { + override var requireValidSession: Boolean = false, + ): MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { override var serializer: SupabaseSerializer? = null diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt index cb2a0cb50..eb13755c1 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt @@ -4,6 +4,7 @@ import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseClientBuilder import io.github.jan.supabase.SupabaseSerializer import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.auth.resolveAccessToken import io.github.jan.supabase.logging.SupabaseLogger import io.github.jan.supabase.logging.w @@ -141,7 +142,8 @@ interface Realtime : MainPlugin, CustomSerializationPlugin { var connectOnSubscribe: Boolean = true, @property:SupabaseInternal var websocketFactory: RealtimeWebsocketFactory? = null, var disconnectOnNoSubscriptions: Boolean = true, - ): MainConfig(), CustomSerializationConfig { + override var requireValidSession: Boolean = false, + ): MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { /** * A custom access token provider. If this is set, the [SupabaseClient] will not be used to resolve the access token. diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt index 6774a80e7..9c9136ca8 100644 --- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt +++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt @@ -3,6 +3,7 @@ package io.github.jan.supabase.storage import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseSerializer import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.auth.authenticatedSupabaseApi import io.github.jan.supabase.bodyOrNull import io.github.jan.supabase.collections.AtomicMutableMap @@ -120,8 +121,9 @@ interface Storage : MainPlugin, CustomSerializationPlugin { data class Config( var transferTimeout: Duration = 120.seconds, @PublishedApi internal var resumable: Resumable = Resumable(), - override var serializer: SupabaseSerializer? = null - ) : MainConfig(), CustomSerializationConfig { + override var serializer: SupabaseSerializer? = null, + override var requireValidSession: Boolean = false, + ) : MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { /** * @param cache the cache for caching resumable upload urls From 96d034f649e8b443f1b92e7ee073aee24717c5ee Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Sun, 12 Oct 2025 02:06:09 +0200 Subject: [PATCH 4/5] add option to disable key as fallback --- .../auth/AuthDependentPluginConfig.kt | 5 +- .../supabase/auth/AuthenticatedSupabaseApi.kt | 71 ++++++++++++++++--- .../auth/SessionNetworkInterceptor.kt | 22 ------ .../exception/SessionRequiredException.kt | 6 ++ 4 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt index 96dfc1467..56158a005 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt @@ -1,12 +1,15 @@ package io.github.jan.supabase.auth +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.auth.user.UserSession + /** * TODO */ interface AuthDependentPluginConfig { /** - * Whether to require a valid [io.github.jan.supabase.auth.user.UserSession] in the [Auth] plugin to make any request with this plugin. + * Whether to require a valid [UserSession] in the [Auth] plugin to make any request with this plugin. The [SupabaseClient.supabaseKey] cannot be used as fallback. */ var requireValidSession: Boolean diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt index aca2fd113..9bdd4b56b 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt @@ -1,27 +1,42 @@ @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") package io.github.jan.supabase.auth +import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.exception.SessionRequiredException import io.github.jan.supabase.exceptions.RestException +import io.github.jan.supabase.logging.e import io.github.jan.supabase.network.SupabaseApi import io.github.jan.supabase.plugins.MainPlugin import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.bearerAuth import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpStatement +import kotlin.time.Clock + +data class AuthenticatedApiConfig( + val jwtToken: String? = null, + val defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, + val requireSession: Boolean +) @OptIn(SupabaseInternal::class) class AuthenticatedSupabaseApi @SupabaseInternal constructor( resolveUrl: (path: String) -> String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, - private val defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, supabaseClient: SupabaseClient, - private val jwtToken: String? = null // Can be configured plugin-wide. By default, all plugins use the token from the current session + config: AuthenticatedApiConfig ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { + private val defaultRequest = config.defaultRequest + private val jwtToken = config.jwtToken + private val requireSession = config.requireSession + override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { - val accessToken = supabaseClient.resolveAccessToken(jwtToken) ?: error("No access token available") + val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException() + checkAccessToken(accessToken) return super.rawRequest(url) { bearerAuth(accessToken) builder() @@ -35,33 +50,73 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( url: String, builder: HttpRequestBuilder.() -> Unit ): HttpStatement { + val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException() + checkAccessToken(accessToken) return super.prepareRequest(url) { - val jwtToken = jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey - bearerAuth(jwtToken) + bearerAuth(accessToken) builder() defaultRequest?.invoke(this) } } + private suspend fun checkAccessToken(token: String?) { + val currentSession = supabaseClient.auth.currentSessionOrNull() + val now = Clock.System.now() + val sessionExistsAndExpired = token == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < now + val autoRefreshEnabled = supabaseClient.auth.config.alwaysAutoRefresh + if(sessionExistsAndExpired && autoRefreshEnabled) { + val autoRefreshRunning = supabaseClient.auth.isAutoRefreshRunning + Auth.logger.e { """ + Authenticated request attempted with expired access token. This should not happen. Please report this issue. Trying to refresh session before... + Auto refresh running: $autoRefreshRunning + OS: ${OSInformation.CURRENT} + Session: $currentSession + """.trimIndent() } + + //TODO: Exception logic + try { + supabaseClient.auth.refreshCurrentSession() + } catch(e: RestException) { + Auth.logger.e(e) { "Failed to refresh session" } + } + } + } + } +//TODO: Fix + /** * Creates a [AuthenticatedSupabaseApi] with the given [baseUrl]. Requires [Auth] to authenticate requests * All requests will be resolved relative to this url */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(baseUrl: String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null) = authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse) +fun SupabaseClient.authenticatedSupabaseApi( + baseUrl: String, + parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null +) = + authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse) /** * Creates a [AuthenticatedSupabaseApi] for the given [plugin]. Requires [Auth] to authenticate requests * All requests will be resolved using the [MainPlugin.resolveUrl] function */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(plugin: MainPlugin<*>, defaultRequest: (HttpRequestBuilder.() -> Unit)? = null) = authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, defaultRequest, plugin.config.jwtToken) +fun SupabaseClient.authenticatedSupabaseApi( + plugin: MainPlugin<*>, + defaultRequest: (HttpRequestBuilder.() -> Unit)? = null +) = + authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, defaultRequest, plugin.config.jwtToken) /** * Creates a [AuthenticatedSupabaseApi] with the given [resolveUrl] function. Requires [Auth] to authenticate requests * All requests will be resolved using this function */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(resolveUrl: (path: String) -> String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, jwtToken: String? = null) = AuthenticatedSupabaseApi(resolveUrl, parseErrorResponse, defaultRequest, this, jwtToken) \ No newline at end of file +fun SupabaseClient.authenticatedSupabaseApi( + resolveUrl: (path: String) -> String, + parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, + config: AuthenticatedApiConfig +) = + AuthenticatedSupabaseApi(resolveUrl, parseErrorResponse, this, config) \ No newline at end of file diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt index efdabcf08..bb9b1a120 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/SessionNetworkInterceptor.kt @@ -1,37 +1,15 @@ package io.github.jan.supabase.auth -import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient -import io.github.jan.supabase.exceptions.RestException -import io.github.jan.supabase.logging.e import io.github.jan.supabase.network.NetworkInterceptor import io.ktor.client.request.HttpRequestBuilder import io.ktor.http.HttpHeaders -import kotlin.time.Clock object SessionNetworkInterceptor: NetworkInterceptor.Before { override suspend fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) { val authHeader = builder.headers[HttpHeaders.Authorization]?.replace("Bearer ", "") - val currentSession = supabase.auth.currentSessionOrNull() - val sessionExistsAndExpired = authHeader == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < Clock.System.now() - val autoRefreshEnabled = supabase.auth.config.alwaysAutoRefresh - if(sessionExistsAndExpired && autoRefreshEnabled) { - val autoRefreshRunning = supabase.auth.isAutoRefreshRunning - Auth.logger.e { """ - Authenticated request attempted with expired access token. This should not happen. Please report this issue. Trying to refresh session before... - Auto refresh running: $autoRefreshRunning - OS: ${OSInformation.CURRENT} - Session: $currentSession - """.trimIndent() } - //TODO: Exception logic - try { - supabase.auth.refreshCurrentSession() - } catch(e: RestException) { - Auth.logger.e(e) { "Failed to refresh session" } - } - } } } \ No newline at end of file diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt new file mode 100644 index 000000000..4529dbc9c --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt @@ -0,0 +1,6 @@ +package io.github.jan.supabase.auth.exception + +/** + * An exception thrown when trying to perform a request that requires a valid session while no user is logged in. + */ +class SessionRequiredException: Exception("You need to be logged in to perform this request") \ No newline at end of file From ed9951a91de35765b16556aeba1fdb3eb2144e7c Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Mon, 13 Oct 2025 00:49:41 +0200 Subject: [PATCH 5/5] separate methods which are not used authenticated --- .../io/github/jan/supabase/auth/AuthConfig.kt | 4 +- .../auth/AuthDependentPluginConfig.kt | 2 +- .../io/github/jan/supabase/auth/AuthImpl.kt | 36 +++++++++-------- .../supabase/auth/AuthenticatedSupabaseApi.kt | 39 +++++++++++-------- .../jan/supabase/auth/admin/AdminApi.kt | 6 +-- .../io/github/jan/supabase/auth/mfa/MfaApi.kt | 2 +- .../providers/builtin/DefaultAuthProvider.kt | 4 +- .../supabase/auth/providers/builtin/OTP.kt | 2 +- 8 files changed, 52 insertions(+), 43 deletions(-) diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index 594ba5911..7a18957d7 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -12,7 +12,7 @@ import kotlin.time.Duration.Companion.seconds /** * The configuration for [Auth] */ -expect class AuthConfig() : CustomSerializationConfig, AuthConfigDefaults +expect class AuthConfig() : CustomSerializationConfig, AuthConfigDefaults, AuthDependentPluginConfig /** * The default values for the [AuthConfig] @@ -111,6 +111,8 @@ open class AuthConfigDefaults : MainConfig() { @SupabaseExperimental var checkSessionOnRequest: Boolean = true + var requireValidSession: Boolean = false + } /** diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt index 56158a005..3807edc86 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt @@ -13,4 +13,4 @@ interface AuthDependentPluginConfig { */ var requireValidSession: Boolean -} \ No newline at end of file +} diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt index d4168377f..7105d65ac 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt @@ -87,8 +87,9 @@ internal class AuthImpl( override val codeVerifierCache = config.codeVerifierCache ?: createDefaultCodeVerifierCache() @OptIn(SupabaseInternal::class) - internal val api = supabaseClient.authenticatedSupabaseApi(this) - override val admin: AdminApi = AdminApiImpl(this) + internal val userApi = supabaseClient.authenticatedSupabaseApi(this) + internal val publicApi = supabaseClient.authenticatedSupabaseApi(this, requireSession = false) + override val admin: AdminApi = AdminApiImpl(publicApi) override val mfa: MfaApi = MfaApiImpl(this) var sessionJob: Job? = null override val isAutoRefreshRunning: Boolean @@ -142,7 +143,7 @@ internal class AuthImpl( }, redirectUrl, config) override suspend fun signInAnonymously(data: JsonObject?, captchaToken: String?) { - val response = api.postJson("signup", buildJsonObject { + val response = publicApi.postJson("signup", buildJsonObject { data?.let { put("data", it) } captchaToken?.let(::putCaptchaToken) }) @@ -166,7 +167,7 @@ internal class AuthImpl( val automaticallyOpen = ExternalAuthConfigDefaults().apply(config).automaticallyOpenUrl val fetchUrl: suspend (String?) -> String = { redirectTo: String? -> val url = getOAuthUrl(provider, redirectTo, "user/identities/authorize", config) - val response = api.rawRequest(url) { + val response = userApi.rawRequest(url) { method = HttpMethod.Get parameter("skip_http_redirect", true) } @@ -193,12 +194,12 @@ internal class AuthImpl( config: (IDToken.Config) -> Unit ) { val body = IDToken.Config(idToken = idToken, provider = provider, linkIdentity = true).apply(config) - val result = api.postJson("token?grant_type=id_token", body) + val result = userApi.postJson("token?grant_type=id_token", body) importSession(result.safeBody(), source = SessionSource.UserIdentitiesChanged(result.safeBody())) } override suspend fun unlinkIdentity(identityId: String, updateLocalUser: Boolean) { - api.delete("user/identities/$identityId") + userApi.delete("user/identities/$identityId") if (updateLocalUser) { val session = currentSessionOrNull() ?: return val newUser = session.user?.copy(identities = session.user.identities?.filter { it.identityId != identityId }) @@ -222,7 +223,7 @@ internal class AuthImpl( } val codeChallenge: String? = preparePKCEIfEnabled() - return api.postJson("sso", buildJsonObject { + return publicApi.postJson("sso", buildJsonObject { redirectUrl?.let { put("redirect_to", it) } createdConfig.captchaToken?.let(::putCaptchaToken) codeChallenge?.let(::putCodeChallenge) @@ -232,7 +233,8 @@ internal class AuthImpl( createdConfig.providerId?.let { put("provider_id", it) } - }).body() + }) + .body() } override suspend fun updateUser( @@ -246,7 +248,7 @@ internal class AuthImpl( putJsonObject(supabaseJson.encodeToJsonElement(updateBuilder).jsonObject) codeChallenge?.let(::putCodeChallenge) }.toString() - val response = api.putJson("user", body) { + val response = userApi.putJson("user", body) { redirectUrl?.let { url.parameters.append("redirect_to", it) } } val userInfo = response.safeBody() @@ -262,7 +264,7 @@ internal class AuthImpl( } private suspend fun resend(type: String, body: JsonObjectBuilder.() -> Unit) { - api.postJson("resend", buildJsonObject { + userApi.postJson("resend", buildJsonObject { put("type", type) putJsonObject(buildJsonObject(body)) }) @@ -297,19 +299,19 @@ internal class AuthImpl( captchaToken?.let(::putCaptchaToken) codeChallenge?.let(::putCodeChallenge) }.toString() - api.postJson("recover", body) { + publicApi.postJson("recover", body) { redirectUrl?.let { url.encodedParameters.append("redirect_to", it) } } } override suspend fun reauthenticate() { - api.get("reauthenticate") + userApi.get("reauthenticate") } override suspend fun signOut(scope: SignOutScope) { if (currentSessionOrNull() != null) { try { - api.post("logout") { + userApi.post("logout") { parameter("scope", scope.name.lowercase()) } } catch(e: RestException) { @@ -339,7 +341,7 @@ internal class AuthImpl( captchaToken?.let(::putCaptchaToken) additionalData() } - val response = api.postJson("verify", body) + val response = publicApi.postJson("verify", body) val session = response.body() importSession(session, source = SessionSource.SignIn(OTP)) } @@ -371,7 +373,7 @@ internal class AuthImpl( } override suspend fun retrieveUser(jwt: String): UserInfo { - val response = api.get("user") { + val response = userApi.get("user") { headers["Authorization"] = "Bearer $jwt" } val body = response.bodyAsText() @@ -394,7 +396,7 @@ internal class AuthImpl( require(codeVerifier != null) { "No code verifier stored. Make sure to use `getOAuthUrl` for the OAuth Url to prepare the PKCE flow." } - val session = api.postJson("token?grant_type=pkce", buildJsonObject { + val session = publicApi.postJson("token?grant_type=pkce", buildJsonObject { put("auth_code", code) put("code_verifier", codeVerifier) }) { @@ -414,7 +416,7 @@ internal class AuthImpl( val body = buildJsonObject { put("refresh_token", refreshToken) } - val response = api.postJson("token?grant_type=refresh_token", body) { + val response = publicApi.postJson("token?grant_type=refresh_token", body) { headers.remove("Authorization") } return response.safeBody("Auth#refreshSession") diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt index 9bdd4b56b..7a685f3c5 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt @@ -5,9 +5,11 @@ import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.exception.SessionRequiredException +import io.github.jan.supabase.auth.exception.TokenExpiredException import io.github.jan.supabase.exceptions.RestException import io.github.jan.supabase.logging.e import io.github.jan.supabase.network.SupabaseApi +import io.github.jan.supabase.plugins.MainConfig import io.github.jan.supabase.plugins.MainPlugin import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.bearerAuth @@ -15,6 +17,7 @@ import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpStatement import kotlin.time.Clock +@SupabaseInternal data class AuthenticatedApiConfig( val jwtToken: String? = null, val defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, @@ -34,13 +37,14 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( private val requireSession = config.requireSession override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { + val builder = HttpRequestBuilder().apply(builder) val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) ?: throw SessionRequiredException() checkAccessToken(accessToken) return super.rawRequest(url) { bearerAuth(accessToken) - builder() defaultRequest?.invoke(this) + this } } @@ -63,30 +67,31 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( private suspend fun checkAccessToken(token: String?) { val currentSession = supabaseClient.auth.currentSessionOrNull() val now = Clock.System.now() - val sessionExistsAndExpired = token == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < now + val sessionExistsAndExpired = + token == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < now val autoRefreshEnabled = supabaseClient.auth.config.alwaysAutoRefresh - if(sessionExistsAndExpired && autoRefreshEnabled) { + if (sessionExistsAndExpired && autoRefreshEnabled) { val autoRefreshRunning = supabaseClient.auth.isAutoRefreshRunning - Auth.logger.e { """ + Auth.logger.e { + """ Authenticated request attempted with expired access token. This should not happen. Please report this issue. Trying to refresh session before... Auto refresh running: $autoRefreshRunning OS: ${OSInformation.CURRENT} Session: $currentSession - """.trimIndent() } + """.trimIndent() + } - //TODO: Exception logic try { supabaseClient.auth.refreshCurrentSession() - } catch(e: RestException) { + } catch (e: Exception) { Auth.logger.e(e) { "Failed to refresh session" } + throw TokenExpiredException() } } } } -//TODO: Fix - /** * Creates a [AuthenticatedSupabaseApi] with the given [baseUrl]. Requires [Auth] to authenticate requests * All requests will be resolved relative to this url @@ -94,20 +99,22 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( @SupabaseInternal fun SupabaseClient.authenticatedSupabaseApi( baseUrl: String, - parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null + parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, + config: AuthenticatedApiConfig ) = - authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse) + authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse, config) /** * Creates a [AuthenticatedSupabaseApi] for the given [plugin]. Requires [Auth] to authenticate requests * All requests will be resolved using the [MainPlugin.resolveUrl] function */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi( - plugin: MainPlugin<*>, - defaultRequest: (HttpRequestBuilder.() -> Unit)? = null -) = - authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, defaultRequest, plugin.config.jwtToken) +fun SupabaseClient.authenticatedSupabaseApi( + plugin: MainPlugin, + defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, + requireSession: Boolean = plugin.config.requireValidSession +): AuthenticatedSupabaseApi where C : MainConfig, C : AuthDependentPluginConfig = + authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, AuthenticatedApiConfig(defaultRequest = defaultRequest, requireSession = requireSession)) /** * Creates a [AuthenticatedSupabaseApi] with the given [resolveUrl] function. Requires [Auth] to authenticate requests diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt index bca33394c..546a1173d 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt @@ -2,7 +2,7 @@ package io.github.jan.supabase.auth.admin import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.auth.AuthImpl +import io.github.jan.supabase.auth.AuthenticatedSupabaseApi import io.github.jan.supabase.auth.SignOutScope import io.github.jan.supabase.auth.user.UserInfo import io.github.jan.supabase.auth.user.UserMfaFactor @@ -99,9 +99,7 @@ interface AdminApi { } @PublishedApi -internal class AdminApiImpl(val gotrue: Auth) : AdminApi { - - val api = (gotrue as AuthImpl).api +internal class AdminApiImpl(val api: AuthenticatedSupabaseApi) : AdminApi { override suspend fun signOut(jwt: String, scope: SignOutScope) { api.post("logout") { diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt index fe9259a4a..a89bc931d 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt @@ -128,7 +128,7 @@ internal class MfaApiImpl( override val verifiedFactors: List get() = auth.currentUserOrNull()?.factors?.filter(UserMfaFactor::isVerified) ?: emptyList() - val api = auth.api + val api = auth.userApi override suspend fun enroll(factorType: FactorType, friendlyName: String?, config: Config.() -> Unit): MfaFactor { val result = api.postJson("factors", buildJsonObject { diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt index 3cfcfa2b0..13fce1827 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt @@ -59,7 +59,7 @@ sealed interface DefaultAuthProvider : AuthProvider { val encodedCredentials = encodeCredentials(config) val gotrue = supabaseClient.auth as AuthImpl val url = "token?grant_type=$grantType" - val response = gotrue.api.postJson(url, encodedCredentials) { + val response = gotrue.publicApi.postJson(url, encodedCredentials) { redirectUrl?.let { redirectTo(it) } } response.body().also { @@ -87,7 +87,7 @@ sealed interface DefaultAuthProvider : AuthProvider { Phone -> "signup" IDToken -> "token?grant_type=id_token" } - val response = gotrue.api.postJson(url, buildJsonObject { + val response = gotrue.publicApi.postJson(url, buildJsonObject { putJsonObject(body) if (codeChallenge != null) { putCodeChallenge(codeChallenge) diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt index 3875f0d62..b1d6e1b86 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt @@ -82,7 +82,7 @@ data object OTP: AuthProvider { supabaseClient.auth.codeVerifierCache.saveCodeVerifier(codeVerifier) codeChallenge = generateCodeChallenge(codeVerifier) } - (supabaseClient.auth as AuthImpl).api.postJson("otp", buildJsonObject { + (supabaseClient.auth as AuthImpl).publicApi.postJson("otp", buildJsonObject { putJsonObject(body) codeChallenge?.let { put("code_challenge", it)