Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

do not send non-JWTs in Authorization header #787

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -17,7 +17,7 @@ suspend fun SupabaseClient.resolveAccessToken(
jwtToken: String? = null,
keyAsFallback: Boolean = true
): String? {
val key = if(keyAsFallback) supabaseKey else null
val key = if(keyAsFallback && isApiKeyJWT) supabaseKey else null
return jwtToken ?: accessToken?.invoke()
?: pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: key
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor(
): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) {

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)
return super.rawRequest(url) {
bearerAuth(accessToken)
accessToken?.let { bearerAuth(it) }
builder()
defaultRequest?.invoke(this)
}
Expand Down
19 changes: 14 additions & 5 deletions Auth/src/commonTest/kotlin/AccessTokenTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.minimalSettings
import io.github.jan.supabase.auth.resolveAccessToken
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.createMockedSupabaseClient
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
Expand All @@ -20,16 +21,24 @@ class AccessTokenTest {
}
}
)
client.auth.importAuthToken("myAuth") //this should be ignored as per plugin tokens override the used access token
client.auth.importAuthToken(TEST_JWT) //this should be ignored as per plugin tokens override the used access token
assertEquals("myJwtToken", client.resolveAccessToken("myJwtToken"))
}
}

@Test
fun testAccessTokenWithKeyAsFallback() {
runTest {
val client = createMockedSupabaseClient(supabaseKey = "myKey")
assertEquals("myKey", client.resolveAccessToken())
val client = createMockedSupabaseClient(supabaseKey = TEST_JWT)
assertEquals(TEST_JWT, client.resolveAccessToken())
}
}

@Test
fun testAccessTokenWithKeyAsFallbackWithInvalidKey() {
runTest {
val client = createMockedSupabaseClient(supabaseKey = "not_a_jwt")
assertNull(client.resolveAccessToken())
}
}

Expand Down Expand Up @@ -65,8 +74,8 @@ class AccessTokenTest {
}
}
)
client.auth.importAuthToken("myAuth")
assertEquals("myAuth", client.resolveAccessToken())
client.auth.importAuthToken(TEST_JWT)
assertEquals(TEST_JWT, client.resolveAccessToken())
}
}

Expand Down
3 changes: 2 additions & 1 deletion Auth/src/commonTest/kotlin/AdminApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.minimalSettings
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserMfaFactor
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.assertMethodIs
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.createMockedSupabaseClient
Expand Down Expand Up @@ -39,7 +40,7 @@ class AdminApiTest {
@Test
fun testSignOut() {
runTest {
val jwt = "jwt"
val jwt = TEST_JWT
val scope = SignOutScope.LOCAL
val client = createMockedSupabaseClient(
configuration = configuration
Expand Down
5 changes: 3 additions & 2 deletions Auth/src/commonTest/kotlin/AuthApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.github.jan.supabase.auth.providers.builtin.IDToken
import io.github.jan.supabase.auth.providers.builtin.OTP
import io.github.jan.supabase.auth.providers.builtin.Phone
import io.github.jan.supabase.auth.status.SessionSource
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.assertMethodIs
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.createMockedSupabaseClient
Expand Down Expand Up @@ -615,7 +616,7 @@ class AuthRequestTest {
@Test
fun testRetrieveUser() {
runTest {
val expectedJWT = "token"
val expectedJWT = TEST_JWT
val client = createMockedSupabaseClient(configuration = configuration) {
assertMethodIs(HttpMethod.Get, it.method)
assertPathIs("/user", it.url.pathAfterVersion())
Expand Down Expand Up @@ -677,7 +678,7 @@ class AuthRequestTest {

private fun sampleUserSession() = """
{
"access_token": "token",
"access_token": "$TEST_JWT",
"refresh_token": "refresh",
"token_type": "bearer",
"expires_in": 3600
Expand Down
5 changes: 3 additions & 2 deletions Auth/src/commonTest/kotlin/AuthTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.Identity
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.createMockedSupabaseClient
import io.github.jan.supabase.testing.pathAfterVersion
import io.github.jan.supabase.testing.respondJson
Expand Down Expand Up @@ -185,7 +186,7 @@ class AuthTest {
identities = expectedIdentities
)
val session = UserSession(
accessToken = "accessToken",
accessToken = TEST_JWT,
refreshToken = "refreshToken",
expiresIn = 3600,
tokenType = "Bearer",
Expand Down Expand Up @@ -216,7 +217,7 @@ class AuthTest {

}

fun userSession(customToken: String = "accessToken", expiresIn: Long = 3600, user: UserInfo? = null) = UserSession(
fun userSession(customToken: String = TEST_JWT, expiresIn: Long = 3600, user: UserInfo? = null) = UserSession(
accessToken = customToken,
refreshToken = "refreshToken",
expiresIn = expiresIn,
Expand Down
27 changes: 27 additions & 0 deletions Auth/src/commonTest/kotlin/AuthenticatedSupabaseApiTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import io.github.jan.supabase.auth.AuthenticatedSupabaseApi
import io.github.jan.supabase.testing.createMockedSupabaseClient
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertFailsWith

class AuthenticatedSupabaseApiTest {

@Test
fun testFailureIfInvalidAuthorizationHeader() {
val supabase = createMockedSupabaseClient(
supabaseKey = "myKey",
supabaseUrl = "https://example.com"
)
val api = AuthenticatedSupabaseApi(
resolveUrl = { "https://example.com/$it" },
supabaseClient = supabase,
jwtToken = "myToken"
)
assertFailsWith<IllegalStateException> {
runTest {
api.get("test")
}
}
}

}
9 changes: 6 additions & 3 deletions Auth/src/commonTest/kotlin/MfaApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.github.jan.supabase.auth.providers.builtin.Phone
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserMfaFactor
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.assertMethodIs
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.createMockedSupabaseClient
Expand Down Expand Up @@ -165,7 +166,7 @@ class MfaApiTest {
) {
respondJson(UserInfo(id = "id", aud = "aud", factors = listOf(expectedFactor)))
}
client.auth.importAuthToken("token")
client.auth.importAuthToken(TEST_JWT)
val factors = client.auth.mfa.retrieveFactorsForCurrentUser()
assertEquals(1, factors.size)
assertEquals(expectedFactor, factors.first())
Expand Down Expand Up @@ -212,7 +213,8 @@ class MfaApiTest {
val data = buildJsonObject {
put("aal", currentAAL.name.lowercase())
}
val token = "ignore.${data.toString().encodeBase64()}"
val encoded = data.toString().encodeBase64()
val token = "$encoded.$encoded.$encoded"
val client = createMockedSupabaseClient(
configuration = configuration
) {
Expand All @@ -230,7 +232,8 @@ class MfaApiTest {
val data = buildJsonObject {
put("aal", current.name.lowercase())
}
val token = "ignore.${data.toString().encodeBase64()}"
val encoded = data.toString().encodeBase64()
val token = "$encoded.$encoded.$encoded"
val client = createMockedSupabaseClient(
configuration = configuration
) {
Expand Down
5 changes: 3 additions & 2 deletions Functions/src/commonTest/kotlin/FunctionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.jan.supabase.auth.minimalSettings
import io.github.jan.supabase.functions.FunctionRegion
import io.github.jan.supabase.functions.Functions
import io.github.jan.supabase.functions.functions
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.createMockedSupabaseClient
import io.github.jan.supabase.testing.pathAfterVersion
import io.github.jan.supabase.testing.toJsonElement
Expand All @@ -28,7 +29,7 @@ class FunctionsTest {
@Test
fun testAuthorizationHeaderAuth() {
runTest {
val expectedJWT = "jwt"
val expectedJWT = TEST_JWT
val supabase = createMockedSupabaseClient(
configuration ={
install(Auth) {
Expand All @@ -51,7 +52,7 @@ class FunctionsTest {
@Test
fun testAuthorizationHeaderCustomToken() {
runTest {
val expectedJWT = "jwt"
val expectedJWT = TEST_JWT
val supabase = createMockedSupabaseClient(
configuration = {
install(Functions) {
Expand Down
3 changes: 2 additions & 1 deletion Realtime/src/commonTest/kotlin/RealtimeChannelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.github.jan.supabase.realtime.broadcastFlow
import io.github.jan.supabase.realtime.channel
import io.github.jan.supabase.realtime.postgresChangeFlow
import io.github.jan.supabase.realtime.realtime
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.pathAfterVersion
import io.github.jan.supabase.testing.toJsonElement
Expand Down Expand Up @@ -142,7 +143,7 @@ class RealtimeChannelTest {

@Test
fun testSendingPayloadWithAuthJWT() {
val expectedAuthToken = "authToken"
val expectedAuthToken = TEST_JWT
runTest {
createTestClient(
wsHandler = { i, _ ->
Expand Down
3 changes: 2 additions & 1 deletion Storage/src/commonTest/kotlin/StorageTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.jan.supabase.auth.minimalSettings
import io.github.jan.supabase.storage.Storage
import io.github.jan.supabase.storage.resumable.MemoryResumableCache
import io.github.jan.supabase.storage.storage
import io.github.jan.supabase.testing.TEST_JWT
import io.github.jan.supabase.testing.assertMethodIs
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.createMockedSupabaseClient
Expand Down Expand Up @@ -193,7 +194,7 @@ class StorageTest {
@Test
fun testAuthHeaderWhenAuthInstalled() {
runTest {
val key = "test-key"
val key = TEST_JWT
val client = createMockedSupabaseClient(
configuration = {
install(Storage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ import io.ktor.client.engine.HttpClientEngine
sealed interface SupabaseClient {

/**
* The supabase url with either a http or https scheme.
* The Supabase url with either an HTTP or HTTPs scheme.
*/
val supabaseHttpUrl: String

/**
* The base supabase url without any scheme
* The base Supabase url without any scheme
*/
val supabaseUrl: String

/**
* The api key for interacting with the supabase api
* The api key for interacting with the Supabase API
*/
val supabaseKey: String

Expand All @@ -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

Expand All @@ -59,6 +59,12 @@ sealed interface SupabaseClient {
@SupabaseInternal
val accessToken: AccessTokenProvider?

/**
* Whether the api key is a JWT token. This property only exists to not re-check the api key type every time it is needed.
*/
@SupabaseInternal
val isApiKeyJWT: Boolean

/**
* Releases all resources held by the [httpClient] and all plugins the [pluginManager]
*/
Expand Down Expand Up @@ -110,6 +116,16 @@ internal class SupabaseClientImpl(
"http://$supabaseUrl"
}

override val isApiKeyJWT: Boolean = isJwt(supabaseKey)

init {
if (!isApiKeyJWT) {
SupabaseClient.LOGGER.i {
"The Supabase API key is not a JWT token. It will not be used for authentication."
}
}
}

// override val coroutineContext = Dispatchers.Default + SupervisorJob()

@OptIn(SupabaseInternal::class)
Expand Down
17 changes: 17 additions & 0 deletions Supabase/src/commonMain/kotlin/io/github/jan/supabase/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,21 @@ suspend inline fun <reified T> HttpResponse.bodyOrNull(): T? {
} catch(e: Exception) {
null
}
}

val BASE64URL_REGEX = "^([a-z0-9_-]{4})*(\$|[a-z0-9_-]{3}\$|[a-z0-9_-]{2}\$)".toRegex(RegexOption.IGNORE_CASE)

@SupabaseInternal
fun isJwt(value: String): Boolean {
val value = if(value.startsWith("Bearer ")) value.substring("Bearer ".length).trim() else value.trim()
if(value.isEmpty()) return false

val parts = value.split(".")

if(parts.size != 3) return false

for(part in parts) {
if(part.length < 4 || !BASE64URL_REGEX.matches(part)) return false
}
return true
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.jan.supabase.BuildConfig
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.exceptions.HttpRequestException
import io.github.jan.supabase.isJwt
import io.github.jan.supabase.logging.d
import io.github.jan.supabase.logging.e
import io.github.jan.supabase.supabaseJson
Expand Down Expand Up @@ -55,6 +56,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor(
url(url)
builder()
}
checkAuthorizationHeader(request)
val endPoint = request.url.encodedPath
SupabaseClient.LOGGER.d { "Starting ${request.method.value} request to endpoint $endPoint" }

Expand Down Expand Up @@ -100,6 +102,13 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor(

fun close() = httpClient.close()

private fun checkAuthorizationHeader(requestBuilder: HttpRequestBuilder) {
val authHeader = requestBuilder.headers["Authorization"] ?: return
if(!isJwt(authHeader.substringAfter("Bearer "))) {
error("The Authorization header must be a JWT token")
}
}

private fun HttpClientConfig<*>.applyDefaultConfiguration(modifiers: List<HttpClientConfig<*>.() -> Unit>) {
install(DefaultRequest) {
headers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ internal class ApolloHttpInterceptor(private val supabaseClient: SupabaseClient,
chain: HttpInterceptorChain
): ApolloHttpResponse {
GraphQL.logger.d { "Intercepting Apollo request with url ${request.url}" }
val accessToken = supabaseClient.resolveAccessToken(config.jwtToken) ?: error("Access token should not be null")
val accessToken = supabaseClient.resolveAccessToken(config.jwtToken)
val newRequest = request.newBuilder().apply {
addHeader(HttpHeaders.Authorization, "Bearer $accessToken")
accessToken?.let {
addHeader(HttpHeaders.Authorization, "Bearer $it")
}
}
return chain.proceed(newRequest.build())
}
Expand Down
Loading