Skip to content

initialize_from_storage removes the session right after refreshing it #1646

Description

@hsusul

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    authbugSomething isn't workingpythonPull requests that update Python code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions