supabase-kt icon indicating copy to clipboard operation
supabase-kt copied to clipboard

[Question]: JWT keeps expiring in background despite alwaysAutoRefresh=true on Supabase 3.1.4

Open Yahouedeou opened this issue 8 months ago • 18 comments

General info

What is your question?

Problem

I'm experiencing frequent JWT expiration issues in my Android application that primarily runs in the background. Despite configuring alwaysAutoRefresh = true, and enableLifecycleCallbacks = false, sessions aren't consistently refreshed when the app is in background state.

Configuration

I'm using Supabase version 3.1.4 with Kotlin. Here's my client configuration:

val client by lazy {
    createSupabaseClient(
        supabaseUrl = SUPABASE_URL,
        supabaseKey = SUPABASE_KEY
    ) {
        defaultSerializer = KotlinXSerializer(json)
        install(Postgrest)
        install(Realtime)
        install(Auth) {
           alwaysAutoRefresh = true          
           enableLifecycleCallbacks = false
        }
        install(Functions)
    }
}

Questions

  • Is there a recommended approach for handling token refreshing in Android apps that spend significant time in background state?
  • Are there specific configuration options I'm missing to make the built-in refresh mechanism work properly in background?

Relevant log output (optional)

Error updating request status | Error: io.github.jan.supabase.postgrest.exception.PostgrestRestException: JWT expired
URL: https://XXXX.supabase.co/rest/v1/device_health_commands?id=eq.XXXX-XXXX&select=%2A
Headers: [Authorization=[Bearer XXXXXXXXXXXX], Prefer=[return=representation], Content-Profile=[public], apikey=[XXXXXXXXXXXX], X-Client-Info=[supabase-kt/3.1.4], Accept=[application/json], Accept-Charset=[UTF-8]]
Http Method: PATCH

Yahouedeou avatar Apr 18 '25 18:04 Yahouedeou

Can you enable debug logs in the SupabaseClientBuilder and check for any Auth logs while its in background? Or when that exception happens

jan-tennert avatar Apr 18 '25 19:04 jan-tennert

Supabase-Auth

  • Here are all logs related to the Supabase-Auth .
  • Wether I put the app in the background or not, I don't have anything else printed in the logs from Supabase-Auth.
2025-04-19 14:05:44.193  5794-5794  Supabase-Auth           com.sapiensgo.android                D  Initializing Auth plugin...
2025-04-19 14:05:44.193  5794-5794  Supabase-Auth           com.sapiensgo.android                D  Initialized Auth plugin
2025-04-19 14:05:44.194  5794-5829  Supabase-Auth           com.sapiensgo.android                I  Loading session from storage...
2025-04-19 14:05:44.224  5794-5832  Supabase-Auth           com.sapiensgo.android                D  Importing session UserSession(accessToken=XXXXXXXXX, refreshToken=XXXXXXXXX, providerRefreshToken=null, providerToken=null, expiresIn=604800, tokenType=bearer, user=UserInfo(appMetadata={"provider":"email","providers":["email"]}, aud=authenticated, confirmationSentAt=2024-12-13T08:18:52.259882Z, confirmedAt=2024-12-13T08:19:05.729970Z, createdAt=2024-12-13T08:18:52.232731Z, email=XXXXXXXXX, emailConfirmedAt=2024-12-13T08:19:05.729970Z, factors=[], id= XXXXXXXXX, identities=[Identity(id= XXXXXXXXX, identityData={"email":"XXXXXXXXX","email_verified":false,"phone_verified":false,"sub":"XXXXXXXXX"}, identityId=cdaa5549-a095-4133-9984-953f28d4568b, lastSignInAt=2024-12-13T08:18:52.251735Z, updatedAt=2024-12-13T08:18:52.251795Z, createdAt=2024-12-13T08:18:52.251795Z, provider=email, userId=XXXXXXXXX)], lastSignInAt=2025-04-17T09:38:53.203616321Z, phone=, role=authenticated, updatedAt=2025-04-17T09:38:53.206398Z, userMetadata={"email":"XXXXXXXXX","email_verified":false,"phone_verified":false,"sub":"XXXXXXXXX"}, phoneChangeSentAt=null, newPhone=null, emailChangeSentAt=null, newEmail=null, invitedAt=null, recoverySentAt=2025-04-17T09:38:43.439217Z, phoneConfirmedAt=null, actionLink=null), type=, expiresAt=2025-04-24T09:38:52.209139Z) from Storage, auto refresh is set to true.
2025-04-19 14:05:44.231  5794-5832  Supabase-Auth           com.sapiensgo.android                D  Session saved to storage (auto refresh enabled)
2025-04-19 14:05:44.231  5794-5832  Supabase-Auth           com.sapiensgo.android                D  Session imported successfully. Starting auto refresh...
2025-04-19 14:05:44.232  5794-5832  Supabase-Auth           com.sapiensgo.android                D  Auto refresh started.
2025-04-19 14:05:44.232  5794-5829  Supabase-Auth           com.sapiensgo.android                D  Refreshing session in 3d 18h 57m 7.976252s.
2025-04-19 14:05:44.233  5794-5832  Supabase-Auth           com.sapiensgo.android                I  Successfully loaded session from storage!

JWT Event Logs

  • Here are some breadcrumb logs from Sentry when we have an event of JWT expired
Timber
error
05:49:47.059 PM
Error getting phone number for SIM slot: 2 | Error: io.github.jan.supabase.postgrest.exception.PostgrestRestException: JWT expired
URL: https://XXXXXXXXX.supabase.co/rest/v1/phone_numbers?device_id=eq.XXXXXXXXX&sim_slot=eq.2&is_active=eq.true&select=%2A
Headers: [Authorization=[Bearer XXXXXXXXX], Content-Type=[application/json], Prefer=[], Accept-Profile=[public], apikey=[XXXXXXXXX], X-Client-Info=[supabase-kt/3.1.4], Accept=[application/json], Accept-Charset=[UTF-8]]
Http Method: GET

---------------------
---------------------
HTTP
info
05:49:45.606 PM
GET: https://XXXXXXXXX.supabase.co/rest/v1/phone_numbers [401]

{
host: wpvjenmnmvrueqieadzs.supabase.co,
http.end_timestamp: 1744998586790,
http.query: device_id=eq.XXXXXXXXX&sim_slot=eq.2&is_active=eq.true&select=%2A,
http.start_timestamp: 1744998585606,
path: /rest/v1/phone_numbers,
protocol: HTTP_2,
response_content_length: 88
}
---------------------
---------------------

SMSReceiver
debug
05:49:45.491 PM
Incoming SMS received on SIM slot: 2 | Subscription ID: 3
---------------------
---------------------
SMSReceiver
info
05:49:45.406 PM
Incoming SMS received | Starting processing
---------------------
---------------------
Device Event
info
05:49:40.234 PM

{
action: SCREEN_OFF,

extras: {
reason: 2,
why: 3
}
}
---------------------
---------------------
Device Event
info
05:49:39.754 PM

{
action: DREAMING_STARTED
}
---------------------
---------------------
Device Event
info
05:49:13.092 PM

{
action: SCREEN_ON,

extras: {
why: 2
}
}
---------------------
---------------------
Network Event
info
05:49:12.543 PM

{
action: NETWORK_CAPABILITIES_CHANGED,
download_bandwidth: 687,
network_type: wifi,
signal_strength: -54,
upload_bandwidth: 47025,
vpn_active: false
}
---------------------
---------------------
Device Event
info
05:49:12.478 PM

{
action: DREAMING_STOPPED
}
---------------------
---------------------

Yahouedeou avatar Apr 19 '25 05:04 Yahouedeou

I had this happen to me today too. Unfortunately, I wasn't logging the session status.

Error message:

Received message without event: RealtimeMessage(topic=realtime:db-changes, event=system, payload={"message":"Token has expired 0 seconds ago","status":"error","extension":"system","channel":"db-changes"}, ref=null)

The app was in the foreground for 7 minutes when this happened. Otherwise, I don't see anything relevant in my logs.

I'm still working out issues with subscriptions, so I can't say if this is a regression.

sproctor avatar Apr 20 '25 03:04 sproctor

Could this bug be related to the previously fixed issue in v2.4.3 – Major bug fix? Specifically the one described as:

“Fix a major bug causing sessions to be refreshed later than they should be”

Yahouedeou avatar Apr 22 '25 15:04 Yahouedeou

No. The session expires sometime in the background and the problem is hard to pin point without logs around the time of expiration. Looking into it.

jan-tennert avatar Apr 22 '25 17:04 jan-tennert

@Yahouedeou Do you have a reproducer? Or how is your app run in the background?

jan-tennert avatar Apr 23 '25 12:04 jan-tennert

@jan-tennert

Reproducer

  • I don't have a simple reproducer for this issue. Our app uses standard Android background services with WakeLock to keep running when the app isn't in the foreground. We are not using an external library for background processes. I could share with you the GitRepo if needed.

More context around the JWT issue

  • At the beginning we used the default JWT Access token expiry time settings and had some issues, so recently increased the JWT Access token expiry time to 604800s but still we are experiencing issues with background sessions not refreshing.

  • Yesterday I did some tests, and by setting a very short JWT Access token expiry (120s), the session refresh works perfectly when the app is in the background. We can see the refresh operations happening in our logs.

  • This makes me wonder if there's something about how Supabase handles longer lived tokens, or if we need to implement something specific to better handles tasks in the background.

  • Our application is running 99% of the time in background mode with our users. The app has to be active/reacheable in order to receive FCM messages from our Backend.

Yahouedeou avatar Apr 24 '25 18:04 Yahouedeou

What happens when the refresh fails? It looks like there's no retry, but I'm not confident on that. A lot of devices shut down the wifi after a period of inactivity. If that happens and auth tries to refresh the token, what happens?

sproctor avatar Apr 25 '25 11:04 sproctor

It will retry on every exception except on rest exceptions (so on network fails and on internal server issues) https://github.com/supabase-community/supabase-kt/blob/4fd0c66812f55c6d093282a64cea615e3a35e8ff/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt#L460-L476

jan-tennert avatar Apr 25 '25 11:04 jan-tennert

  • From all the bugs we had, I never saw a retry.
  • To give you an example, we just had a bug 5min ago on a device from a user, on the homepage of the application we are fetching data, every time the page loads. The data showed was empty, doesn't matter how much we reloaded the page. We had to completely close the app and launch it again to get the session refreshed.
  • And here is a sentry logs regarding another case when calling a edge function, but with the same sessions with the JWT expired.

Image

Image

Yahouedeou avatar Apr 25 '25 16:04 Yahouedeou

From all the bugs we had, I never saw a retry.

Well a retry is not necessarily common, if there is never a server issue or the internet is always on there won't be a retry ever.

To give you an example, we just had a bug 5min ago on a device from a user, on the homepage of the application we are fetching data, every time the page loads. The data showed was empty, doesn't matter how much we reloaded the page. We had to completely close the app and launch it again to get the session refreshed.

Well logs on that would be helpful

And here is a sentry logs regarding another case when calling a edge function, but with the same sessions with the JWT expired.

But the logs show a different error: invalid jwt?

In any case what we can do is add more checks to the session in generel. For example:

  • Check if the session is expired when it's accessed and if it is, then log some data like if the session job is still running etc, then force a refresh before making a request

jan-tennert avatar Apr 26 '25 13:04 jan-tennert

@sproctor This should also prevent your issue for now and we gain more information about why the auto refresh might be stopped

jan-tennert avatar Apr 26 '25 13:04 jan-tennert

Afaik, supabase doesn't have an option to allow for leeway in tokens. If you do a check before a request, make sure they have some time, like 10 - 30 seconds, before they expire.

sproctor avatar Apr 26 '25 13:04 sproctor

Afaik, supabase doesn't have an option to allow for leeway in tokens. If you do a check before a request, make sure they have some time, like 10 - 30 seconds, before they expire.

Yea I'd use a similar threshold to the auto refresh timer, but a little bit less obviously so its not clashing

jan-tennert avatar Apr 26 '25 14:04 jan-tennert

Experiencing the same issue, after some time in background, the jwt is invalid/expired and not refreshed automatically. closing the app and relaunching seems the only way to refresh it after it occurs.

fethij avatar Jul 08 '25 09:07 fethij

@Yahouedeou did you find any workarounds or have gathered more info?

fethij avatar Jul 08 '25 09:07 fethij

The background refresh still didn't work properly in my case, especially when the app is in the background for a long time @fethij

  • The solution I implemented is to force the check of the session expiry, every time before calling the DB / Edge functions. Meaning that I check if the expiry date is already past or due soon.

  • When refreshing session, you have to make sure to also you also import the new refreshed session, otherwise this new session will not be used in your app.

val refreshToken = currentSession.refreshToken

// Use the explicit refresh token method
val refreshedSession = SupabaseClient.client.auth.refreshSession(refreshToken = refreshToken)
                        
// Explicitly import the refreshed session to ensure it's used as the current session
SupabaseClient.client.auth.importSession(refreshedSession)

For Supabase improvements, a few things could be automated/implemented, or optional configurations:

  • When a session is refreshed, automatically import it.
  • When a session has JWT expired, try to refresh the current session. cc @jan-tennert

Yahouedeou avatar Jul 08 '25 13:07 Yahouedeou

When a session is refreshed, automatically import it.

You can use refreshCurrentSession() for that

When a session has JWT expired, try to refresh the current session.

Yea that should be happening automatically, other solutions like checking the session before a request or refreshing upon an error response both do not seem ideal especially because they just fix an issue without actually fixing the issue which is the automatic refresh mechanism

jan-tennert avatar Jul 08 '25 15:07 jan-tennert

I'm also experiencing this issue with JWT expiration errors on both Android and iOS.

Errors I'm seeing:

io.github.jan.supabase.postgrest.exception.PostgrestRestException: JWT expired
Code: PGRST301
Hint: null
Details: null
URL: https://api.pushscroll.com/rest/v1/billing?user_id=eq.cd375b03-e1c6-40f0-bfa0-84742e83d92e&limit=1&select=%2A
Headers: [Authorization=[Bearer..], Content-Type=[application/json], Prefer=[], Accept-Profile=[public], apikey=[..-s], X-Client-Info=[supabase-kt/3.2.5], X-Supabase-Client-Platform=[iOS], X-Supabase-Client-Platform-Version=[18.6.2], Accept=[application/json], Accept-Charset=[UTF-8]]
Http Method: GET
  • io.github.jan.supabase.postgrest.exception.PostgrestRestException: JWT expired Code: PGRST301
  • BadRequestRestException: JWT expired Code: PGRST301

Debug Information:

I added this debug function to help troubleshoot:

internal fun debugInfo(): Map<String, Any> {
    val sessionStatus = when (val status = supabase.auth.sessionStatus.value) {
        is SessionStatus.Authenticated -> {
            "Authenticated(isNew=${status.isNew}, source=${status.source}, emptyAccessToken=${status.session.accessToken.isEmpty()}, type=${status.session.type}, expiresIn=${status.session.expiresIn}, userIsNull=${status.session.user == null})"
        }

        is SessionStatus.NotAuthenticated,
        is SessionStatus.Initializing,
        is SessionStatus.RefreshFailure -> {
            status.toString()
        }
    }
    val sessionIsNull = supabase.auth.currentSessionOrNull() == null
    val userIsNull = supabase.auth.currentUserOrNull() == null

    return mapOf(
        "sessionStatus" to sessionStatus,
        "sessionIsNull" to sessionIsNull,
        "userIsNull" to userIsNull,
    )
}

Data from Sentry for sessionStatus:

Image Image

sessionIsNull and userIsNull are always false

Additional Context:

  • This doesn't only happen at app startup
  • Looking at Sentry breadcrumbs, other requests from different SDKs succeeded in the past
  • Affects both Android and iOS platforms
  • Happy to share Sentry link privately if that helps with debugging

marioortizmanero avatar Nov 07 '25 20:11 marioortizmanero

I don't have more details, but I also encounter this lost session issue recently. Was on v3.0.3 though, not sure if it matters or not.

Thomas-Valloo avatar Nov 28 '25 19:11 Thomas-Valloo