Skip to content

auth-js: signOut() throws before clearing local session when offline (all scopes, including 'local') #2501

Description

@alluisvinas

Bug report

Package: @supabase/auth-js (via @supabase/supabase-js)
Version observed: 2.110.0
File: packages/core/auth-js/src/GoTrueClient.ts, method _signOut

What happens

GoTraClient._signOut({ scope }) always calls the network first:

async _signOut({ scope } = { scope: 'global' }) {
  return await this._useSession(async (result) => {
    ...
    const accessToken = data.session?.access_token;
    if (accessToken) {
      const { error } = await this.admin.signOut(accessToken, scope); // <-- network call
      if (error) {
        if (!(/* 404/401/403/session-missing */)) {
          return this._returnResult({ error }); // returns, but at least reaches here
        }
      }
    }
    if (scope !== 'others') {
      await this._removeSession(); // <-- local cleanup, only reached if the above didn't throw
      await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
    }
    return this._returnResult({ error: null });
  });
}

If the client is offline, this.admin.signOut(accessToken, scope) performs a fetch that throws (network error), rather than resolving with { error }. That exception propagates out of _signOut() uncaught, so _removeSession() is never reached — the local session/token is never removed from storage, regardless of scope (including scope: 'local', which a caller would reasonably expect to never touch the network at all).

Impact

An app calling supabase.auth.signOut() (or { scope: 'local' }) while offline gets a rejected promise, but the user is still authenticated locally (valid session token still in storage) after the call — even though the app has already navigated away as if logged out. Reloading the page, or any code path that reads the persisted session, will find the user still "logged in" with no indication anything went wrong, unless the caller wraps every signOut() in a try/catch AND manually clears the storage key itself as a fallback (which defeats the purpose of having a public signOut() API).

Expected behavior

  • scope: 'local' should never perform a network call at all (it's documented as "sign out only on this device/browser"), or
  • at minimum, _removeSession() should run in a finally (or before attempting the network call, followed by a best-effort network revoke) so that local session state is guaranteed to be cleared even if the network request fails/throws.

Repro

// Simulate offline (e.g. via navigator.onLine / fetch mock)
const { error } = await supabase.auth.signOut(); // rejects (or resolves with error) while offline
// Session token is STILL present in the configured storage (e.g. localStorage[storageKey])

Workaround we're using

We manually remove the storage key(s) ourselves when signOut() throws or returns an error, as a forced fallback, before treating the local logout as complete. Happy to open a PR if a fix direction is agreed (e.g. moving _removeSession() into a finally, or making scope: 'local' skip the network call entirely).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions