Open KirioXX opened 1 year ago
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
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
auth.getSession()
getSession()
first checks if the current session is valid, and if it's not, it refreshes the sessionWith 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?
I actually was wrong with the assumption that the occurrences went down, we just don't have that many devices on the new app version that is using dedicated users per device. But I can see that the exception have not changed between the app version with dedicated device user and without dedicated device user.
@dshukertjr is there a good documentation on how we could handle Postgrest having the JWT expired error for flutter based applications? I can't seem to find a solution that would allow me to check if the jwt has expired and refresh it accordingly. Also, will handling jwt expiration for postgrest be applicable to other services such as storage & rpc calls? Thanks in advance !
@KirioXX Okay, that is unfortunate, but thanks for letting us know.
I'm assuming no, but are there any chance that the system clock on the devices that are experiencing this issue are off by a minute or more?
@jmsandiegoo Are you also experiencing JWT expired error from supabase_flutter? The SDK is meant to handle access token refresh, but for some cases the refresh is not happening, and we haven't been able to reproduce it consistently. If you have any additional information that is not here in this issue that might help us reproduce this issue, it would be greatly appreciated!
I can't seem to find a solution that would allow me to check if the jwt has expired and refresh it accordingly.
You can call supabase.auth.refreshSession();
to refresh the session and get a new token this will refresh the token for all storage, rpc, and other services! To check if the access token has expired or not, you should be able to compare supabase.auth.currentSession.expiresAt
with the current timestamp.
@dshukertjr i see! and yes unfortunately the JWT expired error happens occasionally. I'll look into implementing refreshSession as suggested for now. Also, I was wondering are there documentation about the kind of Exceptions each services would throw when being called in flutter? So far i am aware that auth will throw an AuthException and storage would throw StorageException respectively.
@jmsandiegoo
Postgrest, our API features, will throw PostgrestException
. More details about it can be found here.
https://postgrest.org/en/stable/references/errors.html
@dshukertjr I checked with implementation and this is what they came back with:
The device time will be set automatically by Apple from an NTP server, so I'd say it's unlikely the time is off on the device. If the time was set manually on first starting up a device, I'd say there's a high chance of it being off by a minute or more, but we don't ever manually set or change the time.
@KirioXX Thanks. That was my assumption, but good to confirm.
Im experiencing something similar, maybe. Using Supabase_Flutter We have switched to SupaBase from cognito (where we did not experience this issue). When we close our app and the jwt expires and open it again, it wont refresh the token. Closing the app fully and opening again does work without any new login.
We dont use any other parts og Supabase other than authentication. In our api we can see that it receives an expired token. Lifetimevalidation failed. Token expired at x, time is now y.
We have tried implementing something similar suggested in another comment where we try calling refreshsession when expiresAt is lower than datetime.now. As this is only happening on mobile im having a hard time testing my code. But the below code seems to work. But now i think it is calling refreshsession every time. As it only happens on mobile im having a hard time testing it.
Future<String> getSignedInUserToken() async {
if (Supabase.instance.client.auth.currentSession != null) {
if (Supabase.instance.client.auth.currentSession!.expiresAt! < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
var result = await Supabase.instance.client.auth.refreshSession();
return result.session!.accessToken;
}
return Supabase.instance.client.auth.currentSession!.accessToken;
}
return "Error getting token";
}
I will try an test some more. Maybe someone can use my fix or maybe improve on it.
@larsbloch I can't reproduce your issue. On android, it always refreshes the token correctly for me. Are you doing some calls immediately after reopening so that it maybe didn't have the time to refresh it? Are you using a custom local storage implementation?
Hello @Vinzent03, Thanks for responding. We are not using any custom local storage. It does not refresh the page immediately. But i do force it to reload some data after a second. When i fully close the app and open it again, then tokens are working.
i also need to mention that it is not an app through the appstore but saves as an app from an url. I am also using android.
How does token refresh work. Does the supabase_flutter package check expiry each time i try to get the token using Supabase.instance.client.auth.currentSession!.accessToken
Or does it have some other internal logic?
I am using the token to query an api, so Supabase wont know if the token generates an error.
i also need to mention that it is not an app through the appstore but saves as an app from an url.
@larsbloch What exactly do you mean? Are you using a flutter webapp or you are just installing an android app not via the play store?
We are using didChangeAppLifecycleState()
to check when the app gets to foreground again.
@Vinzent03 Yes it is a flutter web app. We open the app in chrome and choose to add it to the desktop. It is not a "real" app.
Googling didChangeAppLifecycleState(), then it looks like it does not work on flutter web. Since it is a "webapp" this might not be sufficient to refresh the token https://github.com/flutter/flutter/issues/53107
Some even has a workaround https://stackoverflow.com/questions/68367780/flutter-web-how-to-detect-applifecyclestate-changes
Could this be causing the issue we are seing ?
We will try to test it using debugging from pc on phone on monday to get som more info on how the tokens and refreshsession works for us.
That could indeed behave differently. Will try to test this today.
@KirioXX @jmsandiegoo Are you using webapps as well, or are you using native android/iOS applications?
Hey Vinzent03, we only have a native iOS app.
Would be awesome if we knew what caused it. I will await your test. Let me know if you need more information.
@KirioXX @Vinzent03 has shipped an update that makes sure that the SDK always refreshes the access token before sending out the request if the token is expired, but would you be able to update supabase_flutter to the latest version and see if the error you are facing is mitigated?
@dshukertjr @Vinzent03 Im not sure if this would be something that would fix anything for the issue i am facing. But i have tried updating to 1.10.10 and i still get the same errors. Have you tested my issue ?
@larsbloch Hmm which exact errors do you get again?
@Vinzent03 Yes ofcource. Let me summarize the previous posts.
We have website in flutter web we want users to use as a flutter webapp. This works completely fine untill i close the app and open it up after the token has expired.
We use this piece of code to get the token
Future<String> getSignedInUserToken() async {
if (Supabase.instance.client.auth.currentSession != null) {
return Supabase.instance.client.auth.currentSession!.accessToken;
}
return "Error getting token";
}
But for some reason the web app keeps using the expired token (as per logs of the backend). The token does not get refreshed. If i keep making requests in my app it eventually refresh the token. Sometimes it works a couple of seconds after i get the error. Other times i have to completely logout. I have testet it throughout the day with a 1 minute expiration on tokens.
You told me you used something called didchangeapplifecyclestate Googling didChangeAppLifecycleState(), then it looks like it does not work on flutter web. Since it is a "webapp" this might not be sufficient to refresh the token https://github.com/flutter/flutter/issues/53107
Some even has a workaround https://stackoverflow.com/questions/68367780/flutter-web-how-to-detect-applifecyclestate-changes
I have also tried refreshing the session like this as a hack, but it doesnt seem to work
Future<String> getSignedInUserToken() async {
if (Supabase.instance.client.auth.currentSession != null) {
if (Supabase.instance.client.auth.currentSession!.expiresAt! < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
await Supabase.instance.client.auth.refreshSession();
}
return Supabase.instance.client.auth.currentSession!.accessToken;
}
return "Error getting token";
}
So you are using the token for some custom api? The supabase client should take care of the refreshing for its own requests. When you use your own refreshing, what exactly doesn't work? Is the returned access token still expired, or does the token refresh fail.
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:
To Reproduce Steps to reproduce the behavior:
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.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.