supabase / supabase-js

An isomorphic Javascript client for Supabase. Query your Supabase database, subscribe to realtime events, upload and download files, browse typescript examples, invoke postgres functions via rpc, invoke supabase edge functions, query pgvector.
https://supabase.com
MIT License
3.14k stars 251 forks source link

OAuth API connection is non-refreshable #707

Closed jfeaver closed 1 year ago

jfeaver commented 1 year ago

Bug report

Describe the bug

When a signed in user (with password) wants to connect to a Google calendar they are able to view the calendar for one hour until the session expires. At this time, the app refreshes the session. The refresh seems successful except that the refresh only refreshes the session with GoTrue but does not refresh the provider token.

I might be approaching OAuth API connections in a not-the-supabase-way. Please let me know if this is the case.

Thanks!

To Reproduce

  1. Sign in a user with a password (standard Supabase stuff here)
  2. Sign in again with OAuth and scopes for connecting a Google calendar
  3. Upon receiving the user back after the OAuth redirect, listen for auth state changes and save the provider token to continue using the Google Calendar API
  4. Returns later on when the access token has expired. The access token/refresh token are refreshed but the provider token remains expired and can no longer retrieve data from the Google API.

Step 2:

getSupabase().auth.signInWithOAuth({
    provider: "google",
    options: {
      scopes:
        "https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events",
      redirectTo: url("/oauth/calendar/callback"),
    },
  });

STEP 3:

    getSupabase().auth.onAuthStateChange((event, session) => {
      if (event === "SIGNED_IN") {
        // I can look at the session details here and confirm that they are not stored in the database
        console.log({ anAuthStateChange: session });
        const formData = new FormData();
        formData.append("session", JSON.stringify(session));

        // We're using the database to store the provider token and refresh token (security concerns, possibly)
        submit(formData, { method: "post" });
      }
    });

STEP 4:

  if (isExpiringSoon(session.expires_at)) {
    const newSession = await getSupabase(session.access_token).auth.refreshSession(session);
    // We expect to get back a refreshed provider token here but do not so the next line is only partially successful
    saveRefreshedCalendarSession(newSession);
  }

getSupabase function uses the createClient function under the hood with these settings (inspired by supa-fly-stack). Using persistSession true or false doesn't seem to make a difference for this issue.

  const global = accessToken
    ? {
        global: {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        },
      }
    : {};

  return createClient<Database>(SUPABASE_URL, supabaseKey, {
    auth: {
      autoRefreshToken: false,
      persistSession: false,
    },
    ...global,
  });

Expected behavior

I expect that calling auth.refreshToken will return a refreshed provider token. I don't care as much about the GoTrue session as I do about the Google API session.

Screenshots

N/A

System information

Additional context

None.

jfeaver commented 1 year ago

I've been attempting a workaround where I directly request a refresh from the GoTrue API or Google OAuth API directly but don't get back a new "provider_token" from the GoTrue API (based on the JS client behaviour, I guess I should have expected this) and can't get back a non-error response from Google OAuth API ("invalid_grant" error).

Maybe this is a GoTrue bug?

For now, I think the workaround for me is to use Supabase for authentication and handle OAuth API connections separately from Supabase.

j4w8n commented 1 year ago

Yeah, supabase doesn't handle refreshing the provider tokens.

jfeaver commented 1 year ago

supabase doesn't handle refreshing the provider tokens

Okay, thanks @j4w8n. That's good to know.

That being the case, I wish Supabase made it possible for me to initiate the OAuth with Google and then refresh those tokens on my own. As it is, I can't use Supabase for any step because the refresh tokens are not provided after Google OAuth (maybe this is the real issue). There's a provider_refresh_token property but it's nullish for Google OAuth sessions even though Google's API documentation and OAuth Playground show that they provide refresh tokens.

I want Supabase to help with OAuth for API access but I'm looking at implementing it separately. This isn't hard but it was a bad experience working through the Supabase issues only to figure out that I couldn't use it (another "real issue" could be that Supabase needs documentation stating that they don't handle refreshing provider tokens). I'd like to reuse the OAuth capabilities of Supabase and keep consistency in my codebase rather than implement OAuth separately so if Supabase did handle refreshing provider tokens, that would be lovely.

j4w8n commented 1 year ago

I don't have Google oauth setup, so I can't confirm. You might hit up the discord server and ask there - if it returns a provider token.

jfeaver commented 1 year ago

While implementing OAuth for myself, I found out that Google OAuth requires an access_type=offline URL param to be present in the initial authorization request. So... in order to get back a provider_refresh_token, one must add that to the request. In my case, this means doing something like this in Step 1:

  signInWithOAuth({
    provider: "google",
    options: {
      queryParams: { access_type: "offline" },
      scopes:
        "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.readonly",
      redirectTo: url("/oauth/calendar/callback"),
    },
  });

After getting back the provider_refresh_token, I still have to handle refreshing that token with Google as needed. Time to get back to work. :)

Hopefully this discussion will save someone else the hours of time I spent going through this all.