supabase-flutter
supabase-flutter copied to clipboard
onAuthStateChange not triggered when JWT expired
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:
- Setup a listener to listen for onAuthStateChange (we use a bloc cubit for that )
- Login
- Let the JWT expire
- 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.
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?
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?
There was a fix in 1.5.6 about jwt expiry margins. This may be the cause.
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.
It might be important to update to 1.5.7 though, depending on how important the session emitted by the inAuthStateChange stream is.
Sorry I meant we are on 1.5.7 😅
@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?
You should get logged out when this error comes up. You get this error for example when you sign out on another device.
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?
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.
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.
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?
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"
}
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
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!
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?
Thanks for the response Vinzent03. At the moment the expiry limit is 1 week but the device was definitly active in that week.
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.
Could it be a timezone issue? Because it is pretty consistent for our users in the US and our instance is in the UK.
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
- When postgrest method is called, call
auth.getSession() getSession()first checks if the current session is valid, and if it's not, it refreshes the session- 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.
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.
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();
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.
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.
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 Ah, nice catch! @KirioXX Currently are you sharing any login credentials between any of the devices that are experiencing this issue?
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 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.
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 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?