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

Enable third party auth from access token and/or code

Open davitykale-zz opened this issue 4 years ago • 33 comments
trafficstars

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.

davitykale-zz avatar Apr 02 '21 02:04 davitykale-zz

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?

kiwicopple avatar Apr 02 '21 03:04 kiwicopple

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

davitykale-zz avatar Apr 02 '21 03:04 davitykale-zz

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?

kiwicopple avatar Apr 02 '21 03:04 kiwicopple

@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.

davitykale-zz avatar Apr 02 '21 03:04 davitykale-zz

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 ?

kiwicopple avatar Apr 02 '21 03:04 kiwicopple

@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?

joelbraga avatar Apr 03 '21 18:04 joelbraga

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

  • Use expo's AuthSession.startAsync pointing towards https://<YOUR_SUPABASE_URL>.supabase.co/auth/v1/authorize?provider=${provider} (where provider is one of the providers supported by Supabase)
  • 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
  • (Possibly optional) Ensure WebBrowser.maybeCompleteAuthSession(); is set - in my case, it was set before my react native function for App() - you could and should put this in a dedicated login screen
  • Hook up a button or other pressable element to call the function you made (the one which calls authSession.startAsync. This will open the browser and allow the user to auth with third-party
  • Assuming that auth succeeds, it'll return things such as access_token, expires_in, refresh_token, etc.

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.

ChronSyn avatar Apr 04 '21 22:04 ChronSyn

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

awalias avatar May 19 '21 05:05 awalias

@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.

He1nr1chK avatar May 19 '21 07:05 He1nr1chK

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

awalias avatar May 19 '21 08:05 awalias

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 😅

jpstrikesback avatar May 28 '21 17:05 jpstrikesback

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 👍

ChronSyn avatar May 28 '21 17:05 ChronSyn

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,
    });
  });

jpstrikesback avatar May 28 '21 17:05 jpstrikesback

@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?

davitykale avatar Jun 09 '21 13:06 davitykale

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

jpstrikesback avatar Jun 09 '21 13:06 jpstrikesback

@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.

heysailor avatar Jul 02 '21 09:07 heysailor

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

He1nr1chK avatar Jul 04 '21 05:07 He1nr1chK

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.

fkhadra avatar Jul 29 '21 22:07 fkhadra

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

ChronSyn avatar Jul 30 '21 01:07 ChronSyn

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

jonathan-pyt avatar Jul 30 '21 05:07 jonathan-pyt

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

eifo avatar Aug 03 '21 16:08 eifo

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?

fkhadra avatar Aug 03 '21 17:08 fkhadra

@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]; };

eifo avatar Aug 03 '21 19:08 eifo

@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.

fkhadra avatar Aug 03 '21 19:08 fkhadra

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

eifo avatar Aug 03 '21 19:08 eifo

@fkhadra thanks.

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

cloudorbush avatar Sep 28 '21 18:09 cloudorbush

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.

fkhadra avatar Sep 28 '21 18:09 fkhadra

@fkhadra thank you.

I think it just works doing

supabase.auth.signIn(
     {
        email,
        password,
      }
    );

cloudorbush avatar Sep 29 '21 00:09 cloudorbush

@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.

meghaboggaram avatar Dec 02 '21 01:12 meghaboggaram

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 :(

dhruvbhatia7 avatar Dec 09 '21 03:12 dhruvbhatia7