Bug report
When the client starts with a persisted session whose access token has expired, initialize_from_storage() refreshes the session successfully and then immediately deletes it. After restarting an app once the access token has expired (1 hour by default), the user is signed out, even though the refresh token was valid and the refresh succeeded.
https://github.com/supabase/supabase-py/blob/main/src/auth/src/supabase_auth/_async/gotrue_client.py#L1049-L1072
if expires_at and expires_at < time_now + EXPIRY_MARGIN:
refresh_token = current_session.refresh_token
if self._auto_refresh_token and refresh_token:
self._network_retries += 1
try:
await self._call_refresh_token(refresh_token) # saves the new session
self._network_retries = 0
except Exception as e:
if isinstance(e, AuthRetryableError) and self._network_retries < MAX_RETRIES:
... # schedule retry
return
await self._remove_session() # also reached after a successful refresh
return
The success path doesn't return, so control falls through to _remove_session(). That wipes the refreshed session from storage (or from memory when persist_session=False) and cancels the auto-refresh timer _save_session just started. TOKEN_REFRESHED has already been emitted, so listeners are told there's a fresh session that no longer exists. SyncGoTrueClient has the same code.
auth-js's _recoverAndRefresh doesn't do this: it only drops the session when _callRefreshToken returns an error.
Reproduction
import asyncio, time
from httpx import AsyncClient, MockTransport, Response
from supabase_auth import AsyncGoTrueClient
from supabase_auth.types import Session, User
USER = {"id": "11111111-1111-1111-1111-111111111111", "aud": "authenticated",
"app_metadata": {}, "user_metadata": {}, "created_at": "2024-01-01T00:00:00Z"}
def token_endpoint(request):
return Response(200, json={
"access_token": "new-access", "refresh_token": "new-refresh", "token_type": "bearer",
"expires_in": 3600, "expires_at": int(time.time()) + 3600, "user": USER})
async def main():
client = AsyncGoTrueClient(url="http://auth.test",
http_client=AsyncClient(transport=MockTransport(token_endpoint)))
expired = Session(access_token="old-access", refresh_token="old-refresh", token_type="bearer",
expires_in=3600, expires_at=int(time.time()) - 60,
user=User.model_validate(USER))
await client._storage.set_item(client._storage_key, expired.model_dump_json()) # app restart
client.on_auth_state_change(lambda event, _: print("event:", event))
await client.initialize_from_storage()
print("stored:", await client._storage.get_item(client._storage_key))
print("refresh timer:", client._refresh_token_timer)
asyncio.run(main())
Expected behavior
event: TOKEN_REFRESHED
stored: {"access_token":"new-access", ...}
refresh timer: <supabase_auth.timer.Timer object at ...>
Actual behavior
event: TOKEN_REFRESHED
stored: None
refresh timer: None
A later get_session() returns None, so the user has to sign in again.
System information
- supabase-py:
main @ 8c1a914
- Package:
supabase_auth (async and sync)
Bug report
When the client starts with a persisted session whose access token has expired,
initialize_from_storage()refreshes the session successfully and then immediately deletes it. After restarting an app once the access token has expired (1 hour by default), the user is signed out, even though the refresh token was valid and the refresh succeeded.https://github.com/supabase/supabase-py/blob/main/src/auth/src/supabase_auth/_async/gotrue_client.py#L1049-L1072
The success path doesn't return, so control falls through to
_remove_session(). That wipes the refreshed session from storage (or from memory whenpersist_session=False) and cancels the auto-refresh timer_save_sessionjust started.TOKEN_REFRESHEDhas already been emitted, so listeners are told there's a fresh session that no longer exists.SyncGoTrueClienthas the same code.auth-js's
_recoverAndRefreshdoesn't do this: it only drops the session when_callRefreshTokenreturns an error.Reproduction
Expected behavior
Actual behavior
A later
get_session()returnsNone, so the user has to sign in again.System information
main@ 8c1a914supabase_auth(async and sync)