supabase / supabase-flutter

Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products.
https://supabase.com/
MIT License
707 stars 166 forks source link

`Supabase.initialize()` blocks when access token has expired #630

Closed rkistner closed 8 months ago

rkistner commented 1 year ago

Describe the bug

There are a couple of issues with session management that effectively prevents users from using a Supabase/Flutter-based app while offline, even if all the required data is cached in the app.

  1. When launching the app with an expired access token, Supabase.initialize() blocks until the session is refreshed. In the best case, this adds a delay to the app startup. If the user is offline, the app hangs completely while launching.
  2. The above assuming the default of calling await Supabase.initialize() from main(). We can't just remove the await - there is no way to check whether or not the user is already logged in until that completes.
  3. ~If the user is offline for a while in the app, the access token refresh will fail and is retried every 5 minutes or so. If the user comes online again, the user has to wait for that automatic retry, and all other calls will fail with a 401 error until it completes.~ There is no way to explicitly trigger a retry.

To Reproduce Steps to reproduce the behavior:

  1. Set JWT expiry limit to 90 seconds (unrealistically short, but makes this issue easier to reproduce).
  2. Place the device in offline mode.
  3. Wait 2 minutes.
  4. Kill and restart the app.

Expected behavior

  1. Supabase.initialize() should not block on any network calls. At most, Supabase.initialize() should block until the session could be loaded from local storage, even if the access token has expired.
  2. ~Calls to Postgrest should automatically refresh the access token if required, instead of waiting for the automatic retry (also see https://github.com/supabase/supabase-flutter/issues/452#issuecomment-1584661212).~
  3. An API to explicitly refresh the access token if expired should be added.

Version (please complete the following information):

└── supabase_flutter 1.10.16
    ├── supabase 1.11.3
    │   ├── functions_client 1.3.2
    │   ├── gotrue 1.12.1
    │   ├── postgrest 1.5.0
    │   ├── realtime_client 1.2.1
    │   ├── storage_client 1.5.2

Workaroud

As a workaround, setting the JWT expiry limit to the max (1 week) reduces the impact of this issue (limits the occurrence to once a week). But that is quite bad for security - there is then effectively no way to sign users out remotely.

Ideally I'd like to use 5 minute expiry limits, but even with the default of 1 hour this issue comes up quite often.

Vinzent03 commented 1 year ago

Calls to Postgrest should automatically refresh the access token if required, instead of waiting for the automatic retry

that should already be the case since supabase v1.9.8.

An API to explicitly refresh the access token if expired should be added.

Use Supabase.instance.client.auth.refreshSession();

rkistner commented 1 year ago

that should already be the case since supabase v1.9.8.

Thanks, I missed that that part is already implemented, calling refreshSession() automatically. I'll see if I can still reproduce the 401 errors I saw before, but either way it would still be affected by issues with refreshSession() below:

Use Supabase.instance.client.auth.refreshSession();

The issue with refreshSession() is that it does not initiate a new call if there was already an existing one - it waits for the existing call to complete, which includes waiting for retries with exponential back-off.

So what happens is that the user was offline, and now _callRefreshToken() is only attempted around once every 5 minutes. So even though the user is online again, it could take up to 5 minutes before the token is actually refreshed, and every call to refreshSession() waits for that. I could not find any way to trigger it immediately.

dshukertjr commented 1 year ago

@rkistner The supabase_flutter client should fetch an new access token if needed on every request that is sent out, so we might not need to refresh the session in the Supabase.initialize() method. However, I feel like we might run into some race conditions on the realtime side. Will investigate and see what we could do.

The issue with refreshSession() is that it does not initiate a new call if there was already an existing one - it waits for the existing call to complete, which includes waiting for retries with exponential back-off. So what happens is that the user was offline, and now _callRefreshToken() is only attempted around once every 5 minutes. So even though the user is online again, it could take up to 5 minutes before the token is actually refreshed, and every call to refreshSession() waits for that. I could not find any way to trigger it immediately.

That is a good point. We could probably modify the library to immediately attempt to refresh the token when refreshSession() is called.

rkistner commented 1 year ago

Thanks for the quick response! I'll test out the refreshSession() fix - that should only leave the Supabase.initialize() issue.

dshukertjr commented 8 months ago

Closing this issue as with supabase_flutter v2, Supabase.initialize() no longer needs network connection to complete.

gyannickange commented 2 months ago

Hello, I'm using next auth access token in the Supabase.initialize() like this

await Supabase.initialize(
   url: Constants.supabaseUrl,
   anonKey: Constants.supabaseAnonKey,
   headers: {
     'Authorization': 'Bearer $accessToken',
   },
);

But if the user is not authenticated at the application's startup, the access token is null, so how do I update my supabase client when the user will sign-in?

Vinzent03 commented 2 months ago

@gyannickange This issue is already closed for 6 months and is not really related to your issue. Please open a new issue next time. I think the recommended way is to update headers via the client.headers setter.

gyannickange commented 2 months ago

@gyannickange This issue is already closed for 6 months and is not really related to your issue. Please open a new issue next time. I think the recommended way is to update headers via the client.headers setter.

Thanks. This solution works for me.