Skip to content
Open
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -496,8 +497,15 @@ interface Auth : MainPlugin<AuthConfig>, 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)
}
}

}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 [UserSession] in the [Auth] plugin to make any request with this plugin. The [SupabaseClient.supabaseKey] cannot be used as fallback.
*/
var requireValidSession: Boolean

}
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -35,33 +50,73 @@
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)
fun SupabaseClient.authenticatedSupabaseApi(
resolveUrl: (path: String) -> String,
parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null,
config: AuthenticatedApiConfig
) =
AuthenticatedSupabaseApi(resolveUrl, parseErrorResponse, this, config)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.jan.supabase.auth

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.network.NetworkInterceptor
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.HttpHeaders

object SessionNetworkInterceptor: NetworkInterceptor.Before {

override suspend fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) {
val authHeader = builder.headers[HttpHeaders.Authorization]?.replace("Bearer ", "")

}

}
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.github.jan.supabase.auth.exception

//TODO: Add actual message and docs
class TokenExpiredException: Exception("The token has expired")
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -101,7 +102,8 @@ interface Postgrest : MainPlugin<Postgrest.Config>, 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -141,7 +142,8 @@ interface Realtime : MainPlugin<Realtime.Config>, 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -120,8 +121,9 @@ interface Storage : MainPlugin<Storage.Config>, 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -93,7 +98,7 @@ interface SupabaseClient {
}

internal class SupabaseClientImpl(
config: SupabaseClientConfig,
override val config: SupabaseClientConfig,
) : SupabaseClient {

override val accessToken: AccessTokenProvider? = config.accessToken
Expand All @@ -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) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<NetworkInterceptor>()

private val httpConfigOverrides = mutableListOf<HttpConfigOverride>()
private val plugins = mutableMapOf<String, PluginProvider>()

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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

internal data class SupabaseClientConfig(
data class SupabaseClientConfig(
val supabaseUrl: String,
val supabaseKey: String,
val defaultLogLevel: LogLevel,
Expand All @@ -17,9 +18,10 @@ internal data class SupabaseClientConfig(
val osInformation: OSInformation?
)

internal data class SupabaseNetworkConfig(
data class SupabaseNetworkConfig(
val useHTTPS: Boolean,
val httpEngine: HttpClientEngine?,
val httpConfigOverrides: List<HttpConfigOverride>,
val interceptors: List<NetworkInterceptor>,
val requestTimeout: Duration
)
Loading
Loading