Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ", "")

Check warning

Code scanning / detekt

Property is unused and should be removed. Warning

Private property authHeader is unused.

}

}
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")

Check warning

Code scanning / detekt

Public classes, interfaces and objects require documentation. Warning

TokenExpiredException is missing required documentation.
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