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

onAuthStateChange not triggered when JWT expired

Open KirioXX opened this issue 2 years ago • 57 comments

Describe the bug We had this week quite a lot of users where the JWT expired. There users where not logged out instead they had to close and reopen the app to trigger the logout.

In our logs we have seen a lot of these errors:

data:text/text;charset=utf-8,
PostgrestException(message: JWT expired, code: PGRST301, details: Unauthorized, hint: null)

When the exception was thrown, this was the stack:
0		PostgrestBuilder._parseResponse (package:postgrest/src/postgrest_builder.dart:323:0)
1		PostgrestBuilder._execute (package:postgrest/src/postgrest_builder.dart:198:0)
2		PostgrestBuilder.then (package:postgrest/src/postgrest_builder.dart:400:0)


To Reproduce Steps to reproduce the behavior:

  1. Setup a listener to listen for onAuthStateChange (we use a bloc cubit for that )
  2. Login
  3. Let the JWT expire
  4. Try to make a request

Expected behavior User is getting logged out either when the JWT expires or when the try to make a request.

Screenshots

Version (please complete the following information): On Linux/macOS Please run dart pub deps | grep -E "supabase|gotrue|postgrest|storage_client|realtime_client|functions_client" in your project directory and paste the output here.

│   └── supabase_flutter...
│   └── supabase_flutter...
│   └── supabase_flutter...
│   ├── supabase_flutter...
│   ├── supabase_flutter...
│   └── supabase_flutter...
│   ├── supabase_flutter...
│   └── supabase_flutter...
│   ├── supabase_flutter...
│   └── supabase_flutter...
│   └── supabase_flutter...
│   └── supabase_flutter...
├── supabase_flutter 1.7.0
│   ├── supabase 1.6.3
│   │   ├── functions_client 1.1.1
│   │   ├── gotrue 1.6.0
│   │   ├── postgrest 1.2.3
│   │   ├── realtime_client 1.0.3
│   │   ├── storage_client 1.3.0

On Windows Please run dart pub deps | findstr "supabase gotrue postgrest storage_client realtime_client functions_client" in your project directory and paste the output here.

Additional context Add any other context about the problem here.

KirioXX avatar Apr 25 '23 15:04 KirioXX

gotrue 1.6.0 got just a few minutes ago released. Do you really still experience this issue? What version of gotrue where the production app using? What jwt expiry time have you set?

Vinzent03 avatar Apr 25 '23 16:04 Vinzent03

Thanks for the quick response @Vinzent03. That is a very good point the current production version is still on gotrue 1.4.2:

├── supabase_flutter 1.4.0
│   ├── supabase 1.5.1
│   │   ├── functions_client 1.0.2
│   │   ├── gotrue 1.4.2
│   │   ├── postgrest 1.2.2
│   │   ├── realtime_client 1.0.2
│   │   ├── storage_client 1.2.2

Could that be the issue?

KirioXX avatar Apr 26 '23 08:04 KirioXX

There was a fix in 1.5.6 about jwt expiry margins. This may be the cause.

Vinzent03 avatar Apr 26 '23 08:04 Vinzent03

Thank you very much Vinzent03. We actually have a new release going out to the first users today, which uses 1.5.6. I will keep you posted if the new version resolves it.

KirioXX avatar Apr 26 '23 08:04 KirioXX

It might be important to update to 1.5.7 though, depending on how important the session emitted by the inAuthStateChange stream is.

Vinzent03 avatar Apr 26 '23 08:04 Vinzent03

Sorry I meant we are on 1.5.7 😅

KirioXX avatar Apr 26 '23 08:04 KirioXX

@Vinzent03 we rolled out our new app version to some test device. So far everything looks good. But we got this error yesterday and it seems to be stuck in a loop:

data:text/text;charset=utf-8,
AuthException(message: Invalid Refresh Token: Refresh Token Not Found, statusCode: 400)

When the exception was thrown, this was the stack:
0		GotrueFetch.request (package:gotrue/src/fetch.dart:99:0)
1		GoTrueClient._callRefreshToken (package:gotrue/src/gotrue_client.dart:669:0)
2		GoTrueClient.recoverSession (package:gotrue/src/gotrue_client.dart:549:0)
3		SupabaseAuth.initialize (package:supabase_flutter/src/supabase_auth.dart:104:0)
4		Supabase.initialize (package:supabase_flutter/src/supabase.dart:91:0)
5		_initSupabase (package:tusks/main.dart:67:0)
6		main (package:tusks/main.dart:24:0)
7		main (package:tusks/main_production.dart:18:0)

Can that be related to the JWT issue?

KirioXX avatar May 05 '23 08:05 KirioXX

You should get logged out when this error comes up. You get this error for example when you sign out on another device.

Vinzent03 avatar May 05 '23 19:05 Vinzent03

Hi @Vinzent03, we had yesterday again a user that didn't get log out even though the request returned 401 codes. Is there anything that we could miss when we listen to onAuthStateChange?

KirioXX avatar May 12 '23 08:05 KirioXX

We currently only sign out on the specific error message Invalid Refresh Token: Refresh Token Not Found. I don't know what the reason for a 401 response is. We emit the error via addError to the onAuthDtateChange stream, where you could react to that.

Vinzent03 avatar May 12 '23 08:05 Vinzent03

Thanks for the quick response @Vinzent03. If it helps, this is full response:

Response
  Headers:
   access-control-allow-origin: "*"
   content-type: "application/json; charset=utf-8"
   x-kong-upstream-latency: "0"
   alt-svc: "h3=":443"; ma=86400, h3-29=":443"; ma=86400"
   via: "kong/2.8.1"
   server: "cloudflare"
   sb-gateway-version: "1"
   transfer-encoding: "chunked"
   cf-cache-status: "DYNAMIC"
   date: "Thu, 11 May 2023 21:51:43 GMT"
   x-kong-proxy-latency: "1"
   strict-transport-security: "max-age=2592000; includeSubDomains"
   connection: "keep-alive"
   cf-ray: "7c5da514af510fd3-LAX"
   vary: "Accept-Encoding"
   www-authenticate: "Bearer error="invalid_token", error_description="JWT expired""
  Body:
   code: "PGRST301"
   message: "JWT expired"
   details: null
   hint: null

The logs also say that there was 1 slow request before the failing once, could it be that the JWT was invalidated because the refresh request was to slow? Sadly I miss a couple network request logs.

KirioXX avatar May 12 '23 09:05 KirioXX

That seems to be the response of a call to postgrest which failed, because the call to refresh the jwt didn't work.

could it be that the JWT was invalidated because the refresh request was to slow? Definitely not, because jwts can't be manually invalidated. Only after the expiry time.

Can you see how slow exactly the previous request was and where it went? Maybe the call to the token endpoint to refresh the jwt hasn't finished yet?

Vinzent03 avatar May 12 '23 09:05 Vinzent03

I can see the 100 failed requests and they all tried to fetch the same resource:

{
  "status": 401,
  "response_time": 198.431,
  "method": "GET",
  "response_headers": {
    "access-control-allow-origin": "*",
    "content-type": "application/json; charset=utf-8",
    "x-kong-upstream-latency": "0",
    "alt-svc": "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400",
    "via": "kong/2.8.1",
    "server": "cloudflare",
    "sb-gateway-version": "1",
    "transfer-encoding": "chunked",
    "cf-cache-status": "DYNAMIC",
    "date": "Thu, 11 May 2023 21:49:44 GMT",
    "x-kong-proxy-latency": "1",
    "strict-transport-security": "max-age=2592000; includeSubDomains",
    "connection": "keep-alive",
    "cf-ray": "7c5da231ee9d0fd3-LAX",
    "vary": "Accept-Encoding",
    "www-authenticate": "Bearer error=\"invalid_token\", error_description=\"JWT expired\""
  },
  "date": 1683841784800,
  "request": "",
  "headers": {
    "Accept-Profile": "public",
    "apikey": "XXXX",
    "X-Client-Info": "supabase-flutter/1.7.0",
    "Authorization": "Bearer XXXX"
  },
  "response": {
    "code": "PGRST301",
    "message": "JWT expired",
    "details": null,
    "hint": null
  },
  "url": "https://XXXX/rest/v1/job_cards?select=%2A&job_card_id=eq.ae44c164-a38b-413a-9e61-ab89512a4bb7&limit=1"
}

KirioXX avatar May 12 '23 09:05 KirioXX

Hi @Vinzent03, I just wanted to check if there are any updates on this issue? If that is nothing new could you advice use how we could get around this issue? Thank you

KirioXX avatar May 15 '23 14:05 KirioXX

Hey, we are still experience that the JWT expires and the auth state is not changing what causes our automated login flow not to be called. We also got now a stack trace for a failed request if that is of any help:

data:text/text;charset=utf-8,
{message: PostgrestException(message: JWT expired, code: PGRST301, details: Unauthorized, hint: null)}

When the exception was thrown, this was the stack:
0		DeviceCubit._mapFailureToMessage.<fn> (package:xxx/application/device/device_cubit.dart:224:0)
1		_$Unexpected.mapOrNull (package:xxx/domain/work_order/work_order_failure.freezed.dart:457:0)
2		DeviceCubit._mapFailureToMessage (package:xxx/application/device/device_cubit.dart:220:0)
3		DeviceCubit.handleUnsetDeviceJob.<fn> (package:xxx/application/device/device_cubit.dart:186:0)
4		Left.fold (package:dartz/src/either.dart:191:0)
5		DeviceCubit.handleUnsetDeviceJob (package:xxx/application/device/device_cubit.dart:183:0)

About how the JWT could have expired, we have quite a lot of users with a low attention span because of there work environment. Can it be that when one request takes to long and the user triggers more and more requests that the SDK or the server invalidates the JWT to prevent any kind of attack? Thank you!

KirioXX avatar May 31 '23 10:05 KirioXX

It's technically not possible to invalidate a jwt. It's only invalid after its expiry time. Your error shows again that the refreshing of the jwt somehow didn't work. What is your JWT expiry limit in the supabase dashboard?

Vinzent03 avatar May 31 '23 20:05 Vinzent03

Thanks for the response Vinzent03. At the moment the expiry limit is 1 week but the device was definitly active in that week.

KirioXX avatar May 31 '23 20:05 KirioXX

Hmm 1 week is definitely long enough. We currently try to refresh the jwt 60 seconds before expiry. I'm wondering why that sometimes doesn't work in your case.

Vinzent03 avatar May 31 '23 20:05 Vinzent03

Could it be a timezone issue? Because it is pretty consistent for our users in the US and our instance is in the UK.

KirioXX avatar Jun 01 '23 08:06 KirioXX

It seems like there are two separate issues here

  • Postgrest making request with an invalid JWT
  • Gotrue sometimes not refreshing the JWT

The cause of the Gotrue one is unknown. Thanks for the info on the timezone, but nothing pops up immediately as a possible issue.

For Postgrest making request with an invalid JWT, we can probably do what supabase-js is doing. Here is a brief steps of how supabase-js is making API requests to Postgrest

  1. When postgrest method is called, call auth.getSession()
  2. getSession() first checks if the current session is valid, and if it's not, it refreshes the session
  3. after the new session is obtained, an API request to postgrest is made.

With this method, we should at least get rid of the first problem.

dshukertjr avatar Jun 09 '23 14:06 dshukertjr

Thanks @dshukertjr for the update. I just have one question what will happen when it can't refresh the session will this log the user out?

Is there maybe something that we can do to get around the Gotrue error for now? Thank you.

KirioXX avatar Jun 12 '23 18:06 KirioXX

I just have one question what will happen when it can't refresh the session will this log the user out?

If the SDK did attempt to refresh the access token, but failed due to invalid refresh token, the user will be signed out. Otherwise onAuthStateChanged will throw an error, but the user will not be signed out, and they in theory should be able to resume their session if they close and reopen their app.

In your case through, it seems like the SDK for some reason didn't attempt to refresh the access token. In this case, I would guess nothing happens, but can't say for sure since the root cause is still uncertain.

Is there maybe something that we can do to get around the Gotrue error for now?

It might not be a pretty work around, but one thing that comes to my mind as a workaround is to manually call refreshSession periodically.

await supabase.auth.refreshSession();

dshukertjr avatar Jun 13 '23 08:06 dshukertjr

Otherwise onAuthStateChanged will throw an error, but the user will not be signed out, and they in theory should be able to resume their session if they close and reopen their app.

Is there a way to refresh the session without closing and reopening the app? For example to refresh the client when onAuthStateChenged throws?

It might not be a pretty work around, but one thing that comes to my mind as a workaround is to manually call refreshSession periodically.

Thanks dshukertjr, that works for now. I'm not sure if this can be related but we have also seen a lot of these errors:

data:text/text;charset=utf-8,
AuthException(message: Invalid Refresh Token: Refresh Token Not Found, statusCode: 400)

When the exception was thrown, this was the stack:
0		GotrueFetch.request (package:gotrue/src/fetch.dart:99:0)
1		GoTrueClient._callRefreshToken (package:gotrue/src/gotrue_client.dart:669:0)
2		GoTrueClient.recoverSession (package:gotrue/src/gotrue_client.dart:549:0)
3		SupabaseAuth.initialize (package:supabase_flutter/src/supabase_auth.dart:104:0)
4		Supabase.initialize (package:supabase_flutter/src/supabase.dart:91:0)
5		_initSupabase (package:xxxx/main.dart:67:0)
6		main (package:xxxx/main.dart:24:0)
7		main (package:xxxx/main_production.dart:18:0)

For context _initSupabase is initialising the supabase client.

KirioXX avatar Jun 13 '23 08:06 KirioXX

Is there a way to refresh the session without closing and reopening the app? For example to refresh the client when onAuthStateChenged throws?

Again, it's not a pretty workaround, and this, in theory, should never happen, but you should be able to attach onError on onAuthStateChanged and call refreshSession like this to retry it.

supabase.auth.onAuthStateChanged
.listen((data) {})
.onError((error) {
  supabase.auth.refreshSession();  
});

I'm not sure if this can be related but we have also seen a lot of these errors:

Thanks for sharing this. This is very helpful. Interesting that the refresh token is missing.

I have been spending some time trying to find the cause of this, but haven't got anything yet. I will keep this issue posted if I do manage to find anything.

dshukertjr avatar Jun 13 '23 09:06 dshukertjr

Invalid Refresh Token: Refresh Token Not Found

I encountered this, if I sign out on another device or session, because Supabase invalidates all refresh tokens on sign out call.

Vinzent03 avatar Jun 13 '23 10:06 Vinzent03

@Vinzent03 Ah, nice catch! @KirioXX Currently are you sharing any login credentials between any of the devices that are experiencing this issue?

dshukertjr avatar Jun 13 '23 13:06 dshukertjr

I reported that here long ago. From what I see, invalidating all refresh tokens in the only effect of calling the logout endpoint. I don't like that behavior, so @dshukertjr what do you think about adding an option to signOut to NOT revoke all refresh tokens? Another method would be possible as well.

Vinzent03 avatar Jun 13 '23 14:06 Vinzent03

@Vinzent03 The auth team is aware of the request, and they are discussing a solution where the developer can choose to just revoke a single session or the entire session for the user. Until then, I would like to stay away from the Flutter library to deviate on a unique solution from js library on this one.

dshukertjr avatar Jun 13 '23 15:06 dshukertjr

OK that makes sense. The number of those exceptions went down quite a bit down since we changed how we authenticate our operators devices. Each device has now it's own user and they should not be logged out any where else then on that one device. But we still have them from time to time.

KirioXX avatar Jun 13 '23 15:06 KirioXX

@KirioXX Thanks for confirming.

You have reported few different types of errors occurring within this issue, but did any of them stop occurring or start occurring after you migrated to using one user per device?

dshukertjr avatar Jun 14 '23 01:06 dshukertjr