supabase / auth-js

An isomorphic Javascript library for Supabase Auth.
MIT License
354 stars 159 forks source link

Enable third party auth from access token and/or code #406

Closed davitykale-zz closed 1 year ago

davitykale-zz commented 3 years ago

Feature request

Ability to use access token or other credential received from OAuth flow to enable third party auth.

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

I am using Expo for my app which takes care a lot of the nuances in handling OAuth flows in a React Native / Expo managed app: https://docs.expo.io/guides/authentication/. Right now, trying to use the built-in provider flows from Supabase JS client is not working.

Describe the solution you'd like

I would like a way to send an access token or other credential received from an OAuth flow to Supabase to facilitate the login.

Describe alternatives you've considered

Not offering third party auth.

kiwicopple commented 3 years ago

Hey @davitykale - we do already have OAuth providers here: https://supabase.io/docs/reference/javascript/auth-signin#sign-in-using-third-party-providers

Is there something that isn't working with Expo?

davitykale-zz commented 3 years ago

@kiwicopple, when I try that flow with Expo, nothing happens -- "user" and "error" in that example are just null

kiwicopple commented 3 years ago

Oh I see - I think perhaps becuase it's trying to open a new window. And actually the redirect wouldn't work 🤔 .

We don't want to build components specifically for Expo, so I wonder if there is some way to "hook into" these existing implementation?

davitykale-zz commented 3 years ago

@kiwicopple that's what I was hoping! Expo enables the OAuth providers individually (and can return an access token or other identifiers). I'm wondering if there's a way I could pass the access token to Supabase instead of having Supabase handle the new window/redirect flow.

kiwicopple commented 3 years ago

OK cool. We're not too familiar with Expo, so for now I will label it as help wanted. Hopefully someone can spec up the implementation for us. Perhaps you have some ideas already @davitykale ?

joelbraga commented 3 years ago

@kiwicopple The solution shouldn’t be around calling /auth/v1/callback (https://github.com/supabase/gotrue#get-callback) directly?

Expo returns all identifiers needed (access_token, refresh_token) so we just need to expose on gotrue-js a function that receives all the identifiers and internally calls the /callback api with the correct query params for the corresponding provider.

Thoughts?

ChronSyn commented 3 years ago

I've been attempting to get this working today, and I made a bit of progress:

The access_token is a JWT, so this needs to be decoded using the key provided in your dashboard - This is something that should be handled by Supabase, as we definitely don't want to be doing that inside the app (as it means hardcoding the key which could be pulled from the react-native bundle).

From there, I tried calling AsyncStorage.setItem("supabase.auth.token", {...}) - I attempted to recreate the same structure used by Supabase (found by signing in with email and password and then pulling the details out of AsyncStorage), and passing that as the 2nd argument.

I chained a .then() onto the end of the setItem call, and tried supabaseClient.auth.refreshSession(). Unfortunately, even though the details were correctly set in AsyncStorage, Supabase still reported the user was not logged in when logging supabaseClient.auth.session. I didn't anticipate it would work, but I hoped I was on the right tracks.

In any case, I believe the solution is to expose a method which allows us to pass a few details such as access_token, refresh_token, etc. in order to auth a user - providing a callback (or async method) which returns the user's details from the Supabase platform once successful seems to be the way to go.

awalias commented 3 years ago

Expo returns all identifiers needed (access_token, refresh_token) so we just need to expose on gotrue-js a function that receives all the identifiers and internally calls the /callback api with the correct query params for the corresponding provider.

this was my initial thought. I think it's worth a shot

The access_token is a JWT, so this needs to be decoded using the key provided in your dashboard - This is something that should be handled by Supabase, as we definitely don't want to be doing that inside the app (as it means hardcoding the key which could be pulled from the react-native bundle).

it's actually possible to decode the contents of the JWT without the secret (you just can't verify that the signature is legit)

The returnUrl I set was exp://my.local.ip:19000/--/auth/callback - this is something that needs further investigation on how we handle this in a deployed app, but that should be covered by the deep linking or auth guides, and is outside the scope of Supabase

deep linking via redirects should now possible by setting the exact links in Additional Redirect URLs in the dashboard (although I haven't personally tested this in a mobile environment)

image

He1nr1chK commented 3 years ago

@awalias, my suggestion would be to just add access_token and refresh_token as parameters, in addition to provider, to the signIn() function. As @ChronSyn suggested gotrue-js should then call the /callback api with the tokens and return user data. Please let me know how I can assist.

awalias commented 3 years ago

yes I think this makes sense @He1nr1chK . feel free to make a PR if you have time 👍

otherwise I will try and get round it it this week

jpstrikesback commented 3 years ago

So after a bit of a wild goose chase around the moving parts here I've rm -rf'd my earlier posts since they are pointless 🤣...and come up with a tiny PR that calls the /token endpoint with a refresh_token obtained using Expo AuthSession.startAsync as @ChronSyn mentioned above to get the session back 😅

ChronSyn commented 3 years ago

So after a bit of a wild goose chase around the moving parts here I've rm -rf'd my earlier posts since they are pointless 🤣...and come up with a tiny PR that calls the /token endpoint with a refresh_token obtained using Expo AuthSession.startAsync as @ChronSyn mentioned above to get the session back 😅

Awesome, can't wait to see it 😄

The brick wall I hit was when trying to find a way to force the Supabase JS lib to accept some state I'd retrieved from AuthSession. If doing a call to /token with a refresh token is the way to go, and it's a method that can be exposed from within Supabase, lib then it sounds like you're on the right track 👍

jpstrikesback commented 3 years ago

Yep, it extends the auth.signIn (gotrue-js client signIn()) method to receive the refresh_token :) here's a quick example of usage:

  startAsync({
    authUrl: `https://MYSUPABASEAPP.supabase.co/auth/v1/authorize?provider=google&redirect_to=${redirectUri}`,
    returnUrl: redirectUri,
  }).then(async (response: any) => {
    if (!response) return;
    const supaResponse = await supabaseClient.auth.signIn({
      refreshToken: response.params?.refresh_token,
    });
  });
davitykale commented 3 years ago

@jpstrikesback thank you so much for the incredible work on this! Works like a charm for Google and Facebook auth 😀

Is it safe to assume that this should work for Apple Auth as well?

jpstrikesback commented 3 years ago

Cheers @davitykale 🙏 it does work for sign in with Apple from my experience

heysailor commented 3 years ago

@davitykale - it works with Expo's AuthSession, which uses a web-based Apple authentication flow. It won't work with Expo's expo-apple-authentication, which uses the native device.

@jpstrikesback did you see any path towards using an authorization code obtained from the native device authentication flow, and passing it with user data to create a new supabase user?

I do know that Auth0 has a special code exchange endpoint in their API just for handling this case. It takes the code and user details - so I imagine a similar endpoint would need to be added to the GoTrue API to enable native flows.

He1nr1chK commented 3 years ago

Hi @heysailor I asked a similar question in Discussions if you would like to follow it there. [https://github.com/supabase/supabase/discussions/2204]()

fkhadra commented 3 years ago

Just tested the approach with github provider + expo go, it worked as well. It took me a bit of time to understand how to glue all the things together to make it work but in the end, it's pretty straightforward. For those who are looking for a full example as I was, here it is 😆

import React from "react";
import { Button } from "@components/button";
import { supabase } from "@lib/supabase";
import { startAsync } from "expo-auth-session";
import * as Linking from "expo-linking";

interface ProviderResponse {
  params?: {
    refresh_token: string;
  };
}

export function Playground() {
  async function handleLogin() {
   // Create a URL that works for the environment the app is currently running in
   // Expo Client (dev): exp://128.0.0.1:19000/--/path
   // Expo Client (prod): exp://exp.host/@yourname/your-app/--/path
    const returnUrl = Linking.makeUrl("/auth/callback");

    const payload = (await startAsync({
      authUrl: `https://yoursupabase.supabase.co/auth/v1/authorize?provider=github&redirect_to=${returnUrl}`,
      returnUrl,
    })) as ProviderResponse;

    const response = await supabase.auth.signIn({
      refreshToken: payload.params?.refresh_token,
    });

    console.log(response)
  }

  return <Button onPress={handleLogin}>Login</Button>;
}

Update your supabase config as well

Screenshot 2021-07-30 at 00 05 32

Hope this help.

ChronSyn commented 3 years ago

Just tested the approach with github provider + expo go, it worked as well. It took me a bit of time to understand how to glue all the things together to make it work but in the end, it's pretty straightforward. For those who are looking for a full example as I was, here it is 😆

snip

Hope this help.

This is definitely a good example to put in the docs

Mistes974 commented 3 years ago

There is also my example here if that helps: https://github.com/supabase/supabase/discussions/2489#discussioncomment-1050095

eifo commented 3 years ago

Is there any way to Prompt.SelectAccount using this approach?

fkhadra commented 3 years ago

Is there any way to Prompt.SelectAccount using this approach?

@eifo I'm not familiar with Prompt.SelectAccount, do you have an example to share maybe? What are you trying to accomplish?

eifo commented 3 years ago

@fkhadra trying to change the google account to sign in when you have multiple accounts, it's connecting with primary account by default without letting you choose.The only way I found it to work is with Expo's useAuthRequest hook but can't get it to work with supabase. This is how it's used...

const [request, response, promptAsync] = useAuthRequest( { clientId: '[GUID].apps.googleusercontent.com', redirectUri, prompt: Prompt.SelectAccount, scopes: ['openid', 'profile'], }, discovery, ); return [request, response, promptAsync]; };

fkhadra commented 3 years ago

@eifo weird I don't have this issue, I'm able to select which account to use.

https://user-images.githubusercontent.com/5574267/128074015-5c565ac8-8269-4934-aae2-63f4ab014740.MP4

I'm not using the useAuthRequest hook, don't know if this makes any difference.

eifo commented 3 years ago

@fkhadra thanks for sharing! So weird, I can't understand why I'm not being prompted for my account, something is off.

vbylen commented 2 years ago

@fkhadra thanks.

How would the authUrl look when using a simple email or phone signin?

fkhadra commented 2 years ago

Hey @10000multiplier, the example I provided above is what I use for third party login(github, google, etc...). For email authentication, I simply use the supabase client as follow:

supabase.auth.signIn(
      password
        ? {
            email,
            password,
          }
        // password less login
        : { email }
    );

Haven't tried the phone signin yet.

vbylen commented 2 years ago

@fkhadra thank you.

I think it just works doing

supabase.auth.signIn(
     {
        email,
        password,
      }
    );
meghaboggaram commented 2 years ago

@fkhadra thanks for sharing your example.. I'm using expo go + google and facebook. everything works great except the redirect. my app's default page is opening after authentication, instead of the path specified. I have configured deep links in my app and they work correctly. I have added it to the list of redirect url as well in supabase any help here is appreciated.

dhruvbhatia7 commented 2 years ago

Just tested the approach with github provider + expo go, it worked as well. It took me a bit of time to understand how to glue all the things together to make it work but in the end, it's pretty straightforward. For those who are looking for a full example as I was, here it is 😆

import React from "react";
import { Button } from "@components/button";
import { supabase } from "@lib/supabase";
import { startAsync } from "expo-auth-session";
import * as Linking from "expo-linking";

interface ProviderResponse {
  params?: {
    refresh_token: string;
  };
}

export function Playground() {
  async function handleLogin() {
   // Create a URL that works for the environment the app is currently running in
   // Expo Client (dev): exp://128.0.0.1:19000/--/path
   // Expo Client (prod): exp://exp.host/@yourname/your-app/--/path
    const returnUrl = Linking.makeUrl("/auth/callback");

    const payload = (await startAsync({
      authUrl: `https://yoursupabase.supabase.co/auth/v1/authorize?provider=github&redirect_to=${returnUrl}`,
      returnUrl,
    })) as ProviderResponse;

    const response = await supabase.auth.signIn({
      refreshToken: payload.params?.refresh_token,
    });

    console.log(response)
  }

  return <Button onPress={handleLogin}>Login</Button>;
}

Update your supabase config as well Screenshot 2021-07-30 at 00 05 32

Hope this help.

Screen Shot 2021-12-08 at 7 01 53 PM

Sign in based on refresh token will just work once. How can I auto-refresh the token? I have a use-case where I have to pass a refresh token to a subdomain and this could be any number of different subdomains. How do I ensure I can keep signing in with the refresh token for every new subdomain created?

One possible solution would be to use the access_token with setAuth

const { user, error } = supabase.auth.setAuth(access_token)

but this doesn't work :(

rodbs commented 2 years ago

@fkhadra trying to change the google account to sign in when you have multiple accounts, it's connecting with primary account by default without letting you choose.The only way I found it to work is with Expo's useAuthRequest hook but can't get it to work with supabase. This is how it's used...

const [request, response, promptAsync] = useAuthRequest( { clientId: '[GUID].apps.googleusercontent.com', redirectUri, prompt: Prompt.SelectAccount, scopes: ['openid', 'profile'], }, discovery, ); return [request, response, promptAsync]; };

@fkhadra Do you know if it's possible to use useAuthRequest to login in via supabase/provider. I see that in your example with startAsync you pass supabase's auth url. But how do you do it in useAuthRequest? I've tried to pass the url in discovery and it seems to work but I don't get any response ( I get 'dismiss')

The problem I'm facing with startAsync is that works like you say, but I cannot make it work in react-native-web (with NextJS). On opening the pop-up it loads a page localhost:3000/start' instead of theauth.exp.io.../start` proxy. Apart that with startAsync it seems you cannot disable the proxy in production, right? Thx

vikrvm commented 2 years ago

Just tested the approach with github provider + expo go, it worked as well. It took me a bit of time to understand how to glue all the things together to make it work but in the end, it's pretty straightforward. For those who are looking for a full example as I was, here it is 😆

import React from "react";
import { Button } from "@components/button";
import { supabase } from "@lib/supabase";
import { startAsync } from "expo-auth-session";
import * as Linking from "expo-linking";

interface ProviderResponse {
  params?: {
    refresh_token: string;
  };
}

export function Playground() {
  async function handleLogin() {
   // Create a URL that works for the environment the app is currently running in
   // Expo Client (dev): exp://128.0.0.1:19000/--/path
   // Expo Client (prod): exp://exp.host/@yourname/your-app/--/path
    const returnUrl = Linking.makeUrl("/auth/callback");

    const payload = (await startAsync({
      authUrl: `https://yoursupabase.supabase.co/auth/v1/authorize?provider=github&redirect_to=${returnUrl}`,
      returnUrl,
    })) as ProviderResponse;

    const response = await supabase.auth.signIn({
      refreshToken: payload.params?.refresh_token,
    });

    console.log(response)
  }

  return <Button onPress={handleLogin}>Login</Button>;
}

Update your supabase config as well Screenshot 2021-07-30 at 00 05 32

Hope this help.

Just want to say thank you for this. Helped me out almost a year later :)

fedorish commented 1 year ago

Is the refreshToken approach going to work in with supabase-js 2.0 when signIn is replaced?

pixtron commented 1 year ago

Is the refreshToken approach going to work in with supabase-js 2.0 when signIn is replaced?

You could try this in v2 instead of singIn

if (payload.params?.refresh_token) {
  const { data: { session, user}, error } = await supabase.auth.setSession(payload.params.refresh_token);
}
beppek commented 1 year ago

I'm having some issues with supabase.auth.setSession in 2.0.0-rc.10 where calling that method seems to set the session, it returns the user and a new refresh_token. But calling supabase.auth.getUser or supabase.auth.getSession returns null for user and session respectively.

Maybe worth noting that I'm using this in a server context.

Edit any attempts to update rows protected by RLS tied to the user id fail.

alrightsure commented 1 year ago

supabase.auth.setSession doesn't seem to work for this. The docs (and pixtron's example) seems to show that setSession takes a refresh token as its args, but according to its type defnition, it requires an entire session object:

"Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session. If the refresh token in the current session is invalid and the current session has expired, an error will be thrown. If the current session does not contain at expires_at field, setSession will use the exp claim defined in the access token.

@param currentSession — The current session that minimally contains an access token, refresh token and a user."

pixtron commented 1 year ago

supabase.auth.setSession doesn't seem to work for this. The docs (and pixtron's example) seems to show that setSession takes a refresh token as its args, but according to its type defnition, it requires an entire session object:

@alrightsure Your right, the signature of the method has changed in #467 shortly after i left the comment. Now it would be:

const {access_token, refresh_token} = payload.params;
if (access_token && refresh_token) {
  const { data: { session, user}, error } = await supabase.auth.setSession({access_token, refresh_token});
}
tri2820 commented 1 year ago

Is there any way we can provide social scopes as params to startAsync?

hf commented 1 year ago

Hey everyone, this issue has been open for a while but I believe most of the underlying issues have been addressed.

Please switch to v2 of the client library to get all of the goodies. I'll close the issue for now.

mchennupati commented 1 year ago

supabase.auth.setSession doesn't seem to work for this. The docs (and pixtron's example) seems to show that setSession takes a refresh token as its args, but according to its type defnition, it requires an entire session object:

@alrightsure Your right, the signature of the method has changed in #467 shortly after i left the comment. Now it would be:

const {access_token, refresh_token} = payload.params;
if (access_token && refresh_token) {
  const { data: { session, user}, error } = await supabase.auth.setSession({access_token, refresh_token});
}

When I try to set the session object using access and refresh tokens from an OAuth provider as above, I get this error :) [Unhandled promise rejection: ReferenceError: Can't find variable: Buffer] at node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:125:25 in decodeBase64URL

fedorish commented 1 year ago

supabase.auth.setSession doesn't seem to work for this. The docs (and pixtron's example) seems to show that setSession takes a refresh token as its args, but according to its type defnition, it requires an entire session object:

@alrightsure Your right, the signature of the method has changed in #467 shortly after i left the comment. Now it would be:

const {access_token, refresh_token} = payload.params;
if (access_token && refresh_token) {
  const { data: { session, user}, error } = await supabase.auth.setSession({access_token, refresh_token});
}

When I try to set the session object using access and refresh tokens from an OAuth provider as above, I get this error :) [Unhandled promise rejection: ReferenceError: Can't find variable: Buffer] at node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:125:25 in decodeBase64URL

Try installing buffer via your package manager and then adding this to your App.tsx:

import { Buffer } from 'buffer';
global.Buffer = Buffer;
mchennupati commented 1 year ago

That worked for me, thanks ! setting the session, somehow still doesn't trigger the state change as it is not recognized as a SIGN IN I guess. But we can work round that using the data from the session object. supabase.auth.onAuthStateChange((_event, session) => { console.log(_event, session) })

Mjelstad91 commented 1 year ago

With expo SDK 48 iOS is not working with redirect_to. I'm getting: Validation Error: 'redirect_to' is not allowed. Anyone else experiencing this?

Mathias2860DK commented 1 year ago

With expo SDK 48 iOS is not working with redirect_to. I'm getting: Validation Error: 'redirect_to' is not allowed. Anyone else experiencing this?

Did you find a solution?

Palsson123 commented 1 year ago

With expo SDK 48 iOS is not working with redirect_to. I'm getting: Validation Error: 'redirect_to' is not allowed. Anyone else experiencing this?

I also experience this issue. Is there some other workflow for iOS or is this a bug in the supabase package?

paule89123 commented 1 year ago

I get the following warning when setting the session with my access token and refresh token (using expo-secure-store):

await supabase.auth.setSession({ access_token: authResponse.params.access_token, refresh_token: authResponse.params.refresh_token })

Warning: "Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35."

Anyone else encountered this?

ChronSyn commented 1 year ago

I get the following warning when setting the session with my access token and refresh token (using expo-secure-store):

await supabase.auth.setSession({ access_token: authResponse.params.access_token, refresh_token: authResponse.params.refresh_token })

Warning: "Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35."

Anyone else encountered this?

This isn't related to Supabase - instead, it's related to Expo securestore (https://docs.expo.dev/versions/latest/sdk/securestore/). It's not something to worry about unless you're also storing significantly more pieces of data in securestore (hint: You shouldn't be - anything non-sensitive should be in AsyncStorage, and only essential private information should be in SecureStore).

You may be able to use MMKV (https://github.com/mrousavy/react-native-mmkv) with an encrypted store to achieve the same without the warning - though you'd need to create a mapping between the asynstorage / localstorage spec, and MMKV functions.

paule89123 commented 1 year ago

Many thanks @ChronSyn. I wasn't sure if maybe supabase was storing more than it should be.

I won't be storing anything besides the session info in secure-store. Reassuring to hear this won't be a problem. However, expo-secure-store says that it will throw an error in future SDK's, so I'm a little worried about this.