supabase / auth-js

An isomorphic Javascript library for Supabase Auth.
MIT License
351 stars 157 forks source link

autoRefreshToken doesn't work #620

Open nuurcodes opened 3 years ago

nuurcodes commented 3 years ago

Bug report

Describe the bug

After initialising supabase client with

createClient(supabaseUrl, supabaseKey, {
  localStorage: AsyncStorage as any,
  autoRefreshToken: true
});

and setting the JWT expiry to 60 (seconds) on the supabase dashboard, the token does not refresh after 60 seconds have elapsed. Oddly enough, setting the JWT expiry to anything less than 60 triggers the token refresh but it constantly updates without waiting for expiry

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

  1. Setup RN project (I used expo bare)
  2. Set JWT expiry to 60
  3. Add supabase auth state listener
  4. Sign in
  5. Wait for refresh token to update (doesn't update)

Expected behavior

The token should refresh after the expiry time set on the supabase dashboard

System information

awalias commented 3 years ago

hey @nuurcodes thanks for the issue, could you please test if you can reproduce with the latest supabase-js version v1.13.1

nuurcodes commented 3 years ago

@awalias Setting the JWT expiry to anything less than 60 still causes the accessToken to update in a loop on v1.13.1

When I set the JWT expiry to 60, I was expecting the accessToken to automatically update every 60 seconds but it doesn't. I've tried logging supabase.auth.session() and also listening to onAuthStateChange

churichard commented 3 years ago

Having the same issue here. I'm constantly needing to log back into my app, even though I've set the expiration date to be a week long and have autoRefreshToken set to true. It seems like I need to re-enter my credentials even if the token is not actually expired (i.e. I need to re-enter my credentials even though a week has not passed since the last time I did it). This happens both on localhost and on my production website.

If autoRefreshToken is true, I would expect the token to be automatically refreshed without needing to enter my credentials again. In other words, if I've logged in once, I should stay logged in unless I've explicitly logged out or cleared my cookies/cache.

heauton commented 3 years ago

Shouldn't this be on by default? https://github.com/supabase/supabase-js/blob/master/src/SupabaseClient.ts#L11-L12

I still see this issue on 1.15.0.

kiwicopple commented 3 years ago

:tada: This issue has been resolved in version 1.15.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

b2m9 commented 2 years ago

Sorry to dig out an old issue, but are we sure this is fixed? Despite initializing the Supabase client with autoRefreshToken: true I have to double check session.expires_at and initiate supabase.auth.refreshSession() manually throughout my app. (on version 1.28.2)

There isn't much documentation on how autoRefreshToken is supposed to work. But judging from the questions in the discussions section, I'm not the only one who is having trouble letting Supabase refresh expired JWTs.

bangdragon commented 2 years ago

Sorry to dig out an old issue, but are we sure this is fixed? Despite initializing the Supabase client with autoRefreshToken: true I have to double check session.expires_at and initiate supabase.auth.refreshSession() manually throughout my app. (on version 1.28.2)

There isn't much documentation on how autoRefreshToken is supposed to work. But judging from the questions in the discussions section, I'm not the only one who is having trouble letting Supabase refresh expired JWTs.

How you catch session expires and and initiate supabase.auth.refreshSession() manually. Thanks

ARMATAV commented 2 years ago

Still broken. autoRefreshToken setting does nothing to actually trigger a refresh and you will keep setting the error in perpetuity until you manually reload the page.

image
edgarsilva commented 1 year ago

Yeah this seems to still be happening, at least when running Supabase locally for development, working on a React Native app using Expo ->

LOG  SB Res --> {"data": {"session": null}, "error": [AuthApiError: Invalid Refresh Token]}

Trying to figure out how long before it expires, and supabase.auth.refreshToken() also throws an error once it has expired.

erickreutz commented 1 year ago

Can confirm this is also happening for me.

kangmingtay commented 1 year ago

Hey everyone, we're investigating this issue and it would be great if yall can provide the supabase-js / gotrue-js versions that you experience this error on.

uze commented 1 year ago

Hey everyone, we're investigating this issue and it would be great if yall can provide the supabase-js / gotrue-js versions that you experience this error on.

Version: "@supabase/supabase-js": "^2.1.3"

optinforce commented 1 year ago

Experiencing this with these versions:

supabase-js: 2.10.0 gotrue-js: 2.13.0

UPDATE: For my case, I have found that the problem actually comes from nuxt-supabase: https://github.com/nuxt-modules/supabase/issues/137#issuecomment-1466514716

shimi-chans-tree commented 1 year ago

When I left the app running on the iOS simulator for a while and reloaded Metro, I encountered the error Invalid Refresh Token: Already Used

@supabase/supabase-js: "^2.22.0" expo: "^48.0.10" (bare workflow)

huksley commented 1 year ago

I am also getting "Invalid Refresh Token: Already Used" @supabase/supabase-js: "^2.21.0"

I don't see previous invocations which might have used the token. And btw, isn't the refresh token supposed to be constant and used only to reissue access tokens as needed?

lewisd1996 commented 1 year ago

Also getting this in my RN project, constantly being signed out in a matter of minutes, anything we can do here?

vbylen commented 1 year ago

Have you tried setting the jwt_expiry in config.toml to one week (the maximum allowed):

jwt_expiry = 604800
kangmingtay commented 1 year ago

Hey everyone, i'm attempt to reproduce this issue using the expo user management example (https://github.com/supabase/supabase/tree/master/examples/user-management/expo-user-management) but i haven't been able to reproduce this. It would be great if someone has a repo to share or can outline the reproducible steps.

I'm running on "@supabase/supabase-js": "^2.24.0", and this is how i'm initialising the supabase client:

export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
  auth: {
    storage: ExpoSecureStoreAdapter as any,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
});
claycoleman commented 1 year ago

Yeah this seems to still be happening, at least when running Supabase locally for development, working on a React Native app using Expo ->

LOG  SB Res --> {"data": {"session": null}, "error": [AuthApiError: Invalid Refresh Token]}

Trying to figure out how long before it expires, and supabase.auth.refreshToken() also throws an error once it has expired.

This response describes the issue pretty similarly to me – I'm running supabase locally using npx supabase start, my client is initialized just like yours is @kangmingtay but with local supabase url and anon key.

I have a supabase.auth.onAuthStateChange which runs on app init and whenever the auth state changes. typically the repro case is logging in then waiting until the token expires, usually this happens while the app simulator / the app is in the background on my mac. in this case, my onAuthStateChange fires and receives a session with a jwt and refresh token. This triggers a fetch from my profiles table, but that request fails with {"code": "PGRST301", "details": null, "hint": null, "message": "JWT expired"}. subsequent reloads of the app also fire onAuthStateChange with what looks like a valid session but the same error occurs when trying to fetch data from supabase. my guess is that this session is coming from the secure store.

cc @lewisd1996 @b2m9 @edgarsilva @uze for your repro cases?

claycoleman commented 1 year ago

fwiw, the refresh token that is the session with the expired jwt is still valid according to my local db. i looked this up using the auth.refresh_tokens table. so my issue is slightly different from some of the above responses where even calling refreshToken() errors out. my client is being inited with autoRefreshToken: true though, so also confused why supabase isn't refreshing it automatically! supabase version 2.25.0

swyxio commented 1 year ago

getting AuthApiError: Invalid Refresh Token: Refresh Token Not Found right now as well; not sure if this is the same issue

image

MilesV64 commented 1 year ago

We're also getting the Refresh Token: Refresh Token Not Found issue every time we try to refresh.

JeremyMees commented 1 year ago

The issue was resolved after I updated the Supabase Nuxt package to ^0.3.1. Additionally, I had to clear my existing Supabase cookies, which allowed the functionality to work as intended.

lorsk commented 11 months ago

Only way I am able to resolve this issue is uninstalling the app and installing it again. This occurs in local development and not in production.

Using React native with @supabase/supabase-js: ^2.37.0

sebastiangrebe commented 4 months ago

Same happening for me. "@supabase/supabase-js": "^2.42.7"

Here my supabase client setup:

import { Database } from '../../../../supabase/types'
import { createClient } from '@supabase/supabase-js'
import * as SecureStore from 'expo-secure-store'
import AsyncStorage from "@react-native-async-storage/async-storage";

import * as aesjs from 'aes-js';
import 'react-native-get-random-values';

import { replaceLocalhost } from '../getLocalhost.native'

if (!process.env.EXPO_PUBLIC_SUPABASE_URL) {
  throw new Error(
    `EXPO_PUBLIC_SUPABASE_URL is not set. Please update the root .env.local and restart the server.`
  )
}

if (!process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY) {
  throw new Error(
    `EXPO_PUBLIC_SUPABASE_ANON_KEY is not set. Please update the root .env.local and restart the server.`
  )
}

const supabaseUrl = replaceLocalhost(process.env.EXPO_PUBLIC_SUPABASE_URL)

// As Expo's SecureStore does not support values larger than 2048
// bytes, an AES-256 key is generated and stored in SecureStore, while
// it is used to encrypt/decrypt values stored in AsyncStorage.
class LargeSecureStore {
  private async _encrypt(key: string, value: string) {
    const encryptionKey = crypto.getRandomValues(new Uint8Array(256 / 8));

    const cipher = new aesjs.ModeOfOperation.ctr(encryptionKey, new aesjs.Counter(1));
    const encryptedBytes = cipher.encrypt(aesjs.utils.utf8.toBytes(value));

    await SecureStore.setItemAsync(key, aesjs.utils.hex.fromBytes(encryptionKey));

    return aesjs.utils.hex.fromBytes(encryptedBytes);
  }

  private async _decrypt(key: string, value: string) {
    const encryptionKeyHex = await SecureStore.getItemAsync(key);
    if (!encryptionKeyHex) {
      return encryptionKeyHex;
    }

    const cipher = new aesjs.ModeOfOperation.ctr(aesjs.utils.hex.toBytes(encryptionKeyHex), new aesjs.Counter(1));
    const decryptedBytes = cipher.decrypt(aesjs.utils.hex.toBytes(value));

    return aesjs.utils.utf8.fromBytes(decryptedBytes);
  }

  async getItem(key: string) {
    const encrypted = await AsyncStorage.getItem(key);
    if (!encrypted) { return encrypted; }

    return await this._decrypt(key, encrypted);
  }

  async removeItem(key: string) {
    await AsyncStorage.removeItem(key);
    await SecureStore.deleteItemAsync(key);
  }

  async setItem(key: string, value: string) {
    const encrypted = await this._encrypt(key, value);

    await AsyncStorage.setItem(key, encrypted);
  }
}

export const supabase = createClient<Database>(
  supabaseUrl,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,
  {
    auth: {
      storage: new LargeSecureStore(),
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  }
)

Plus this one for handling foreground / background handling but even with or without no difference:

AppState.addEventListener('change', (state) => {
  if (state === 'active') {
    supabase.auth.startAutoRefresh()
  } else {
    supabase.auth.stopAutoRefresh()
  }
})

Majorly I dont see any events or tries of refreshing the token.

Moreover default 1 hour of course stays longer logged in but setting 50, 60 or 120 seconds mostly logs me out after good 20 seconds. Can someone official have a look into this as it seems to be a big UX blocker?

xl0 commented 1 month ago

I'm getting the same thing. To reproduce more easily, I set the token to 10 seconds. I'm running with supabase/ssr server-only with SvelteKit, a single supabase client in locals that is used throughout the request:

export const handle: Handle = async ({ event, resolve }) => {
    event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
        auth: { debug: true },
        cookies: {
            get: (key) => {
                const value = event.cookies.get(key);
                // debug('cookies get', { key, value });
                return value;
            },
            set: (key, value, options) => {
                // debug('cookies set', { key, value, options });
                event.cookies.set(key, value, { ...options, path: options.path ?? '/' });
            },
            remove: (key, options) => {
                // debug('cookies remove', { key, options });
                event.cookies.delete(key, { ...options, path: options.path ?? '/' });
            }
        }
    });

    event.locals.safeGetSession = async () => {
        const {
            data: { session }
        } = await event.locals.supabase.auth.getSession();
        if (!session) {
            debug('No session found');
            return { session: null, user: null };
        }

        const {
            data: { user },
            error
        } = await event.locals.supabase.auth.getUser();
        if (error) {
            debug('Error getting user: ', error);
            // JWT validation has failed
            return { session: null, user: null };
        }

        return { session, user };
    };

    const { session, user } = await event.locals.safeGetSession();
    event.locals.session = session ?? undefined;
    event.locals.user = user ?? undefined;

        [ ... ]

Here is the traceback:

  app:hooks:supabase Error getting user: AuthApiError: invalid JWT: unable to parse or verify signature, token has invalid claims: token is expired
    at handleError (/home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/lib/fetch.js:63:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async _handleRequest (/home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/lib/fetch.js:108:9)
    at async _request (/home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/lib/fetch.js:89:18)
    at async /home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/GoTrueClient.js:874:24
    at async SupabaseAuthClient._useSession (/home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/GoTrueClient.js:774:20)
    at async SupabaseAuthClient._getUser (/home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/GoTrueClient.js:864:20)
    at async /home/xl0/work/projects/llms/assistant/node_modules/@supabase/auth-js/dist/main/GoTrueClient.js:851:20 {
  __isAuthError: true,
  status: 403,
  code: 'bad_jwt'
} +8m

I turned on auth: {debug: true}, will see if I can catch another one.

xl0 commented 1 month ago

Ok, I think I know what's going on, at least in my case. There is a race condition between the check for the session expiration and when the session is used to get the user: node_modules/@supabase/auth-js/src/GoTrueClient.ts:

      const hasExpired = currentSession.expires_at
        ? currentSession.expires_at <= Date.now() / 1000
        : false

      this._debug(
        '#__loadSession()',
        `session has${hasExpired ? '' : ' not'} expired`,
        'expires_at',
        currentSession.expires_at
      )

      if (!hasExpired) {
              [ return the session ]
      [ refresh the session ]

If the session is expiring in the new milliseconds, this causes the exception. I think an easy workaround would be to add an arbitrary or configurable offset to the session expiration check and refresh a session that expires very soon.

@kiwicopple @awalias