auth-js icon indicating copy to clipboard operation
auth-js copied to clipboard

Add Provider Refresh Token to Supabase Session Data

Open fredguest opened this issue 3 years ago • 12 comments

Feature request

Is your feature request related to a problem? Please describe.

Currently, the data returned from the supabase.auth.session() method, or the supabase.auth.onAuthStateChange() callback contain the following properties: access_token, refresh_token, provider_token.

The access_token and refresh_token both apply to the Supabase API, so that your application can maintain access to Supabase without repeatedly requesting the user's credentials and permission.

The provider_token applies to an external auth provider, such as Discord for example, so that you can access the Discord API, but there is no provider_refresh, so it is impossible to maintain access to an external provider's API without repeatedly requesting the user's credentials and permission.

Most users expect that after they entered their credentials, reviewed the scopes that a third party application is requesting, and granted access to those scopes once, that they should not have to do it repeatedly. Because the provider refresh token is currently being omitted from the Supabase session data, despite being granted by the user and available in the external provider's OAuth2 flow, the experience for the end user is atypical and suboptimal.

Describe the solution you'd like

Most external auth providers that have implemented an OAuth2 flow return a refresh token in the response, such as Discord for example https://discord.com/developers/docs/topics/oauth2#authorization-code-grant-access-token-response so this feature could be implemented by simply passing that value along to the Supabase session data. This would make the Supabase session data symmetrical when using an external auth provider:

Supabase Access Token -> access_token Supabase Refresh Token -> refresh_token Provider Access Token -> provider_token Provider Refresh Token -> provider_refresh (this is the proposed addition)

Describe alternatives you've considered

It would be possible to implement an external OAuth2 flow without using the supabase.auth module, and obtain the provider's access token and refresh token directly. The downside of this solution is that in bypassing the supabase.auth module, you would no longer be creating user records in the Supabase auth.users table. I'm not certain if it would still be possible to implement RLS without the auth.users table, but it seems unlikely that it would be advisable. For serverless applications, RLS, or a similar non-client means to restrict access to the DB, is generally of high importance.

fredguest avatar Aug 30 '21 20:08 fredguest

For anyone running into this, apparently you can bypass the supabase.auth module entirely and still implement RLS, which is actually not a bad solution https://github.com/supabase/supabase/discussions/1849

fredguest avatar Sep 03 '21 00:09 fredguest

Can we get an update if this solution is being proposed or if there is a workaround? @fredguest it does not appear your solution gives you a refresh token.

handlerda avatar Sep 09 '21 17:09 handlerda

What I linked to is a solution for implementing RLS in the Supabase DB without using the supabase.auth module at all. It's not meant to be a solution for obtaining a provider refresh token from Supabase, it presumes that you have already obtained provider access and refresh tokens on your own.

In other words, I'm using Supabase purely for the Postgres DB, nothing else. I'm using vanilla HTTP requests to obtain access and refresh tokens directly from Discord, and then I can also use vanilla HTTP requests for token rotation when necessary, because I have the Discord refresh token.

If you want to continue using the supabase.auth module, the solution I linked to will not work for you. You would need the feature I originally proposed in this ticket, but there's been no response so I don't know if they plan to implement it.

fredguest avatar Sep 09 '21 18:09 fredguest

Hello! I'm looking for this feature, too. I'm building an application that needs an offline access_type for Google Drive API and with the current implementation, Supabase is not helping me as I expected.

Looking for news 😇

vladutilie avatar Nov 04 '21 18:11 vladutilie

@fredguest thanks for reporting in detail the issue. I'm creating an app using the new Spotify provider and facing the same obstacles. Auth integration experience has been great so far! Alas, having to keep signing the user in periodically is a deal breaker.

Sigmus avatar Nov 11 '21 11:11 Sigmus

I noticed a related issue that may also contain an alternative solution. When supabase.auth.onAuthStateChange() calls the /token?grant_type=refresh_token endpoint, the returned payload does not contain provider_token.

This means you cannot simply override the local session object entirely when getting the results from the refresh endpoint. Instead, the app will have to maintain a separate copy of the provider_token elsewhere. Or perform a new signIn to get a new provider_token, which will reload the page.

If /token?grant_type=refresh_token returned a provider_token in the response, it would be possible to always have a "fresh" token. We wouldn't need a provider_refresh token at all if the JWT Expiry authentication setting is lower than the expiration time of the provider.

If this is possible and makes sense it will probably be something that needs to be updated on the server because the full response is already set by this function:

  /**
   * Generates a new JWT.
   * @param refreshToken A valid refresh token that was returned on login.
   */
  async refreshAccessToken(
    refreshToken: string
  ): Promise<{ data: Session | null; error: ApiError | null }> {
    try {
      const data: any = await post(
        this.fetch,
        `${this.url}/token?grant_type=refresh_token`,
        { refresh_token: refreshToken },
        { headers: this.headers }
      )
      const session = { ...data }
      if (session.expires_in) session.expires_at = expiresAt(data.expires_in)
      return { data: session, error: null }
    } catch (e) {
      return { data: null, error: e as ApiError }
    }
  }

Maybe @awalias has some feedback on the ideas outlined in this issue?

Sigmus avatar Nov 12 '21 14:11 Sigmus

Same thing. I'm looking for this feature. Thats a deal breaker for me too.

shevenionov avatar Dec 05 '21 15:12 shevenionov

I haven't checked this lately. Does anyone know if any updates have sorted out this issue?

Sigmus avatar Mar 23 '22 13:03 Sigmus

Also wondering if this has been fixed recently.

laznic avatar Apr 02 '22 17:04 laznic

Same here. I have been looking for a good workaround for react native. BTW, it is the only feature holding back from moving my app to production.

tanifort avatar Apr 03 '22 17:04 tanifort

I would appreciate this too. 2 birds 1 stone. I have an app that uses supaclient.auth.signIn provider = Google. Then I use Google oauth client to get a refresh token with scopes for google services, so the app can do google api things for the user whenever. = 2x sign in UX (when initially using the app).

twofingerrightclick avatar May 12 '22 05:05 twofingerrightclick

Same issue here. provider_token disappears after the first refreshSession() call and makes it impossible to consume OAuth provider apis which are crucial in my app.

lauri865 avatar Jul 27 '22 06:07 lauri865

Hey, we've started returning the provider_refresh_token in the session now thanks to @msonnberger's PR #451. We'll also be adding it to the rc branch soon for those trying out supabase-js/v2. Will close this issue for now! Huge thanks to everyone for the patience and feedback!

kangmingtay avatar Sep 30 '22 04:09 kangmingtay

@kangmingtay, is there a way to provide queryParams to google oauth provider though? Trying to pass options.queryParams.access_type = "offline" to the provider w/ SignInWithOAuthCredentials, but it doesn't seem to get passed through to the provider on supabase-js/v2. Hence there's no way to prompt the OAuth provider to return the refresh_token in the first place and provider_refresh_token is empty + provider_token disappears from the session after the first refresh.

lauri865 avatar Oct 13 '22 10:10 lauri865

@kangmingtay, is there a way to provide queryParams to google oauth provider though? Trying to pass options.queryParams.access_type = "offline" to the provider w/ SignInWithOAuthCredentials, but it doesn't seem to get passed through to the provider on supabase-js/v2. Hence there's no way to prompt the OAuth provider to return the refresh_token in the first place and provider_refresh_token is empty + provider_token disappears from the session after the first refresh.

I observe the same behavior. However, Github seems to automatically send the provider_refresh_token

younesbenallal avatar Oct 17 '22 23:10 younesbenallal

Thanks for raising this up @lauri865 and @younesbenallal! currently, you can't pass query params to most providers because gotrue doesn't support that. However, since some oauth providers only return the provider refresh token if an explicit query param is passed, we've added support for passing any query params here (https://github.com/supabase/gotrue/pull/757)

It'll take some time before this is available to all Supabase projects but do keep a lookout in the next week or so!

kangmingtay avatar Oct 19 '22 05:10 kangmingtay

Hi @kangmingtay thanks for fixing this! I just started a new Supabase project where I need this behavior options.queryParams.access_type = "offline" to get a refresh token from Google.

When can I expect https://github.com/supabase/gotrue/pull/757 to be available? I'm on the latest JS library. For the context, the API call looks like

	const { data, error } = await supabaseClient.auth.signInWithOAuth({
		provider: 'google',
		options: {
			queryParams: { access_type: 'offline' },
			scopes:
				'https://www.googleapis.com/auth/gmail.modify'
		}
	});

devstein avatar Nov 07 '22 03:11 devstein

hey @devstein, probably this week

kangmingtay avatar Nov 07 '22 19:11 kangmingtay

@kangmingtay Amazing! What's the best way to follow so I know when it's available?

devstein avatar Nov 07 '22 19:11 devstein

@devstein you can watch the new releases by going on the repo and watching custom events : ezgif com-gif-maker

younesbenallal avatar Nov 08 '22 09:11 younesbenallal

@younesbenallal Thanks. I was referring to when the version of GoTrue is released to my Supabase instance.

For others that stumble upon this thread, I found the root cause is because supabase start is running GoTrue v2.15.3, but the fix is only in version GoTrue v2.19.1

Created a PR: https://github.com/supabase/cli/pull/587

devstein avatar Nov 08 '22 14:11 devstein

@kangmingtay, is there a way to provide queryParams to google oauth provider though? Trying to pass options.queryParams.access_type = "offline" to the provider w/ SignInWithOAuthCredentials, but it doesn't seem to get passed through to the provider on supabase-js/v2. Hence there's no way to prompt the OAuth provider to return the refresh_token in the first place and provider_refresh_token is empty + provider_token disappears from the session after the first refresh.

It does the samething for me too, I have been getting the provider_token as well as provider_refresh_token from the session object but after sometime, both gets null as I need it for the Google APIs to work. Is there any workaround this? @kangmingtay

Pratikkapadia7 avatar Feb 23 '23 03:02 Pratikkapadia7

hey @Pratikkapadia7, based on the google oauth docs

If you are not using a client library, you need to set the access_type HTTP query parameter to offline when redirecting the user to Google's OAuth 2.0 server. In that case, Google's authorization server returns a refresh token when you exchange an authorization code for an access token. Then, if the access token expires (or at any other time), you can use a refresh token to obtain a new access token.

it seems like you need to manage refreshing the provider_token on your own by using the provider_refresh_token returned initially.

kangmingtay avatar Feb 27 '23 02:02 kangmingtay

hey @Pratikkapadia7, based on the google oauth docs

If you are not using a client library, you need to set the access_type HTTP query parameter to offline when redirecting the user to Google's OAuth 2.0 server. In that case, Google's authorization server returns a refresh token when you exchange an authorization code for an access token. Then, if the access token expires (or at any other time), you can use a refresh token to obtain a new access token.

it seems like you need to manage refreshing the provider_token on your own by using the provider_refresh_token returned initially.

Thanks for the further information @kangmingtay . But the thing I am facing is initially I am only receiving provider_token and the provider_refresh_token is null, so is it something Supabase handles or should I look into the google docs for that too?

Pratikkapadia7 avatar Feb 27 '23 02:02 Pratikkapadia7

Hey @Pratikkapadia7, seems like you also need to pass the query param prompt: consent in order for google to return the provider_refresh_token. Based on this stackoverflow thread,

The refresh_token is only provided on the first authorization from the user. Subsequent authorizations, such as the kind you make while testing an OAuth2 integration, will not return the refresh_token again.

You either need to remove the authorized app from your google account or add the query param prompt: consent as well.

kangmingtay avatar Feb 27 '23 02:02 kangmingtay

cc @J0

kangmingtay avatar Feb 27 '23 06:02 kangmingtay

Hi! If anyone needs it, I kinda solved the problem of using Supabase Google Auth offline.

Here are the steps:

  1. I prompt signInWithOAuth() with additional queryParams for access_type and prompt:
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    scopes: 'https://www.googleapis.com/auth/drive.file',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent', //<- Optional. The refresh-token gets returned
                             //only immediately after consent. It will not be 
                             //re-issued on sessionRefresh or Login.        
                             //Therefore, for test purposes I
                             //"force" consent every time.
    },
  },
})
  1. After login, I fetch the provider_refresh_token from existing session:
const { data: session, error } = await supabase.auth.getSession()
if (error) throw error
)
return session.session.provider_refresh_token
}```
3. And then I save it to my database:
```const { data, error } = await supabase
  .from('gdrive')
  .insert({
    user_id: session.user.id,
    provider_token: provider_token,
    provider_refresh_token: provider_refresh_token,
  })
  .select()
  .single()```

4. So once I have the refresh token saved for each user, I can edit the Google's "GetClient" util:
/utlis/drive.js:

```const { google } = require('googleapis')

const getClient = (accessToken) => {
  const oAuth2Client = new google.auth.OAuth2()
  // oAuth2Client.setCredentials({ access_token: authToken })
  oAuth2Client.setCredentials({ access_token: accessToken })

  return google.drive({
    version: 'v3',
    auth: oAuth2Client,
  })
}

const getRefreshClient = (refreshToken) => {
  const oAuth2Client = new google.auth.OAuth2(
    process.env.GOOGLE_ID,
    process.env.GOOGLE_CLIENT_SECRET
  )
  // oAuth2Client.setCredentials({ access_token: authToken })
  oAuth2Client.setCredentials({ refresh_token: refreshToken })

  return google.drive({
    version: 'v3',
    auth: oAuth2Client,
  })
}

module.exports = {
  getClient,
  getRefreshClient,
}
  1. So, in places where the active provider token is available for me, I use "getClient" with normal access token. However, for offline use, I call "getRefreshClient" module with credentials and refresh token. It's enough to provide only refresh token, Google ID and Google Secret. Google automatically will change it to provider token and authenticate the client.

I'm not sure whether that's the most optimal way to do it, but it works for now so I'm good with it.

ky-zo avatar May 28 '23 18:05 ky-zo

When user is authenticated with 'github' provider, provider_refresh_token is null. Is this a bug?

(provider_token is set to a correct, valid token).

Warchant avatar Oct 23 '23 07:10 Warchant

@Warchant it is not a bug, github oauth API doesn't return a refresh token (https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow)

kangmingtay avatar Oct 23 '23 17:10 kangmingtay

@kangmingtay thanks for the reply. For future readers I've composed a small post with an instruction to solve provider_refresh_token is null problem.

https://warchantua.hashnode.dev/supabase-and-github-app-authentication-done-right

Warchant avatar Oct 25 '23 12:10 Warchant