Giter VIP home page Giter VIP logo

Comments (26)

JMaylor avatar JMaylor commented on July 17, 2024 12

Would be great if this could be implemented. As others have said, it's really quite cumbersome to bootstrap an SPA right now with this. I'll use Vue as an example.

In main.ts we have the following which creates the app and mounts it.

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

When using supabase I'm doing something like this:

import { createApp } from "vue";
import App from "./App.vue";
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseAnonKey);

createApp(App)
  .provide("supabase", supabase)
  .mount("#app");

What happens when I'm using 3rd party auth supabase feature is, I get redirected off my app (no option to open in separate window I don't think?), the auth flow happens, then I want to redirect to myapp.com/protectedRoute.

My app is recreated and tries to go straight to /protectedRoute. However, the auth state is null to start with and so there's no way to know if the user is allowed in or not.

The best solution in this case would be for the supabase-js client to return a promise which is resolved once the auth state is known. Then we can do a familiar pattern (similar to other 3rd party auth) like:

import { createApp } from "vue";
import App from "./App.vue";
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

async function initApp() {
  const supabase = await createClient(supabaseUrl, supabaseAnonKey);
  // At this point supabase is initialised and we know for sure if we have an authenticated user or not.
  // So any route guards will work properly.
  createApp(App)
    .provide("supabase", supabase)
    .mount("#app");
}

initApp();

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024 5

Yeah, no worries whatsoever :)

I think the simplest would be for onAuthStateChange to report all state changes. i.e. it should fire the first time the state is known and anytime after that. It works great in firebase and gives you the ability to differentiate between "no user, because they are not signed in" and "no user (yet?), because I'm still checking" scenarios.

If you had that, you could reliably transform it into a DOM event or promise, or wrap it in the framework state library, whatever make sense for your app.

from gotrue-js.

silasabbott avatar silasabbott commented on July 17, 2024 3

Another solution to this would be for supabase.auth.session() and supabase.auth.user() to return Promises.

It's been a massive headache trying to block and check the authentication for my application before rendering or redirecting and this would be a lifesaver

from gotrue-js.

madeleineostoja avatar madeleineostoja commented on July 17, 2024 2

I'm getting ready to push a development product into beta, and fixing the janky auth guards I currently have (as reported here, my login redirects flash while supabase waits to rehydrate a user session) is on my list. I figured I was just missing something about a ready state or pattern in the docs, but I'm quite shocked that this isn't accounted for in any way by Supabase.

As it stands, as I understand it, there is no way outside of a dodgy Promise race / timeout to account for a pending auth state. This is fundamental for any clientside auth solution. Can someone from the Supabase team weigh in on whether this is a high priority, or even being looked into?

I've run into quite a few rough edges on Supabase's auth layer and I'm having serious second thoughts about continuing with it, unlike the rest of the platform which I've found pretty solid even in beta. It would be wonderful to get some kind of roadmap to prod readiness for the auth layer.

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024 1

Haha no worries - I was writing most of that half asleep so hopefully didn't come off abrupt.

If there truly isn't a way to tell if the state is still pending that would be pretty crap

Yes, this is the fundamental issue - there's (presently) no way to monitor if the state is pending.

The only dimensions available are a boolean isSignedIn and the onAuthState() events, which means you can't reliably reason about the true (deferred) initial state (which may or may not be in async communication with the server).

You introduce an interesting second option here though, which is arguably better than an event (as events, by design, have no guarantee of delivery, due to race conditions when attaching a listener).

If the library were to present a watchable isReady (or similar) property set when the restoration attempt is complete, that would guarantee knowledge of the true state when deciding when to unblock initial navigation.

from gotrue-js.

silasabbott avatar silasabbott commented on July 17, 2024 1

So, from what it sounds like, the main issue here might be lack of documentation more than lack of function.

At this time, for supabase.auth.user(), these are the only two sentences in the docs

  • Inside a browser context, user() will return the user data, if there is a logged in user.
  • This method gets the user object from memory.

And for supabase.auth.onAuthStateChange() it's only this:

  • Receive a notification every time an auth event happens.
  • callback required (object)

There is virtually nothing about when events fire, arguments provided, or what is returned from these methods.


Asking on the Discord server, here's what other's have generally said:

  • An AuthChangeEvent does actually fire every single time when createClient() finishes initializing and has determined if the user is authorized or not. So, if the user was previously logged in and visits the page, onAuthStateChange() will fire the callback soon after with the event "SIGNED_IN".
  • user() and session() don't make any network requests or get any values directly from localStorage. They simply grab the related object and return it. If you call user() before createClient() has not finished initializing, the user object returned won't have any values regardless of whether the user is logged in or not.

Who here is familiar with these methods? Would anyone be up for the task of getting the docs updated with more details?

Edit: After writing this, I found the TypeDoc that does describe each reference/class/interface/etc. which is awesome, but there is still nothing in the docs on when an AuthChangeEvent occurs and the onAuthStateChange() callback is triggered.

from gotrue-js.

xylish7 avatar xylish7 commented on July 17, 2024 1

@madeleineostoja Maybe #335 will help ? At least it helped in my case.

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024

Additional context on Alternative 1 for anyone looking for a possible solution:

const AUTH_EVENTS: AuthChangeEvents = {
  SIGNED_IN: "SIGNED_IN",
  SIGNED_OUT: "SIGNED_OUT"
}

const publicRouteNames: ComputedRef<string[]> = computed<string[]>(() =>
  router
    .getRoutes()
    .filter((route) => !!route.meta.isPublic)
    .map((route) => `${route.name}`)
)

const handleAuthError = async (error: Error): Promise<void> => {
  console.error(error)
  notyf.error(`Failed to load your account.`)
  await router.push({ name: "auth.login" })
}

supabase.auth.onAuthStateChange(
  async (event: AuthChangeEvent): Promise<void> => {
    if (event === AUTH_EVENTS.SIGNED_IN) {
      try {
        const { error, data } = await supabase
          .rpc<AccountRecord>("get_authenticated_account")
          .single()
        if (error) {
          await handleAuthError(error)
        } else if (data) {
          await auth.setAccount(data)
          if (publicRouteNames.value.includes(router.currentRoute.value.name)) {
            await router.replace({ name: "home" })
          }
        }
      } catch (error) {
        await handleAuthError(error)
      }
    }
  }
)

from gotrue-js.

MistaPidaus avatar MistaPidaus commented on July 17, 2024

I think my issue related to this as well. I have a handleAuth to check whether the user is logged in or not and redirect appropriately. checking if user or session is null or not is not helping cuz react will wait for side effect to see if init state will change or not and onAuthStateChange only runs if user logged in. the case if the user offline or lost connection, when reload the browser, it will redirect to login and on 2nd reload it will finally redirect appropriately.

having some kind of loading state or some kind of event that tells the status would be helpful.

from gotrue-js.

JMaylor avatar JMaylor commented on July 17, 2024

I'm coming back a few months on having revisited this, and found a way to do this in a Vue 3 app I'm writing, which works quite nicely I think. Hopefully this is useful to other frameworks as well.

Okay so I have my auth setup to redirect back to url.com/callback after it's gone off to my 3rd party auth provider (I'm using GitHub) and come back to my app:

// github sign in function
const { user, error } = await supabase.auth.signIn(
  { provider: "github" },
  {
    redirectTo: `${window.location.origin}/callback`,
  }
);

The url it comes back to, including hash looks like:

url.com/callback#access_token=foobar&expires_in=3600&provider_token=carrotcake&refresh_token=cookies&token_type=delicious

So the first thing I implement is a route guard in my /callback route, which prevents access to this route unless all of the expected hash keys are present:

// router.ts
{
  path: "/callback",
  name: "callback",
  component: () => import("@/views/auth/AuthCallback.vue"),
  beforeEnter: (to) => {
    /* Parse the route hash into a dictionary */
    const hashDictionary = {} as any;
    // first remove the actual '#' character
    const hash = to.hash.replace("#", "");
    // split hash into key-value pairs
    hash.split("&").forEach((item) => {
      // split 'key=value' into [key, value]
      const [key, value] = item.split("=");
      // add to results
      hashDictionary[key] = value;
    });

    if (
      [
        "access_token",
        "expires_in",
        "provider_token",
        "refresh_token",
        "token_type",
      ].some((key) => !(key in hashDictionary))
    )
      return "/";
  },
},

So, at this point I now have a situation where someone can login with GitHub and be correctly returned to the app. The issue I'd run into before is that sometimes supabase.auth.user() would be initialised before my vue app is created, and sometimes it was the other way round.

We need to listen to the supabase SIGNED_IN event, check to see if we're currently at the callback page, and navigate accordingly. This handles the case where the vue app is created before supabase.auth.user().

supabase.auth.onAuthStateChange((event) => {
  const routeName = router.currentRoute.value.name;
  if (event == "SIGNED_IN" && routeName == "callback") router.push({ name: "home" });
});

Additionally, to handle the case where supabase user is initialised before the app is created, we can do the following in my AuthCallback.vue (the /callback component):

<script setup>
import { supabase } from "@/services/supabase";
import { useRouter } from "vue-router";

const router = useRouter();
onMounted(() => {
  if (supabase.auth.user())  router.push({ name: "home" });
});
</script>

Hope this helps someone at least. I have an example repo doing this here: https://github.com/JMaylor/vuepabase. Live: https://vuepabase.netlify.app/

I still think it would be nice to have an initialised event to avoid doing all of the above but I don't think it's needed to bootstrap an SPA

from gotrue-js.

silasabbott avatar silasabbott commented on July 17, 2024

The reason onAuthStateChange() doesn't work for some cases is because it can't block the rendering of the page when certain frameworks/SPA libraries use async functions to determine page loading and route handling. The whole goal is not not display the secret page until the user has been confirmed to be authorized to view the page.

Here's a super basic example in a SvelteKit page:

<script lang="ts" context="module">
    export async function load() { 
        const auth = supabase.auth.user()
        
        // If user isn't logged in, redirect them back to the login page
        if (!auth) {
            return {
                status: 302,
                redirect: "/login"
            }
        }
        
        // If user is logged in, continue
        return {
            status: 200
        }
    }
</script>

When the page is first visited, there is always a redirect to the login whether the user is truly logged in or not. On any subsequent navigation to that page within the SPA, however, the redirect works as intended. It's as if the user object isn't getting retrieved right away, thus the value is still null for a short period of time. If the SPA initializes before the user object is retrieved, the user will not show as being logged in.

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024

The reason onAuthStateChange() doesn't work for some cases is because it can't block the rendering of the page when certain frameworks/SPA libraries use async functions to determine page loading and route handling. The whole goal is not not display the secret page until the user has been confirmed to be authorized to view the page.

Here's a super basic example in a SvelteKit page:

It's not really the job of any auth lib to block rendering, it's for the UI lib / app to decide when that can happen based on the auth state. You just have to "wait" for auth to be resolved. I've found the easiest approach for using firebase with SvelteKit is to wrap it in a store, and it should be no different for supabase:

https://www.captaincodeman.com/lazy-loading-firebase-with-sveltekit

Instead of the auth guard / slot rendering, it can easily be made to redirect instead (I think it's nicer that the URL doesn't change).

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024

It's not really the job of any auth lib to block rendering

Agree, but the emission of an event when the restore process has been completed (one way or another), would certainly make it easier for the frontend app to implement this more gracefully / atomically.

You just have to "wait" for auth to be resolved.

From how I read this statement, this is not the root issue (in relation to my initial request). The problem is not when you know auth is pending (ie when you click "log in") but when you have a "maybe" state (ie Supabase restores previous sessions automatically).

In this case, the only way to know if you should wait or not is to use an (ugly, and not-at-all-atomic) Promise race against a "best guess" timeout.

If you "just wait" without a poll / ugly timeout, then your "wait" will never resolve.
If you don't "wait" and let the state lazy resolve, then your UX has a horrible skipping flow:

[protected page] -> [redirect to login page] -> [lazy auth resolves] -> [redirect back to protected page]

Unless I missed something vital, nothing in the linked blog post resolves these issues?

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024

No, you don't do any redirect until you know you need to, which is only when auth has resolved. You only wait once, then make decisions on auth state when it's known. So the protected page doesn't redirect until it knows the user isn't signed in (vs doesn't know the auth state). There is no need to poll or race and it typically resolves very quickly.

Personally, I think an auth guard that doesn't redirect is nicer (i.e. if you need to authenticate, the URL doesn't change, you are just shown a sign-in UI) but having a redirect is really the same logic-wise.

Converting a callback into a promise or raising a DOM event from it is pretty trivial but the callback is already very convenient to use in most framework state libs which is how I believe most apps using those frameworks should be consuming it.

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024

But this is the point, presently there is no event to signal that auth has resolved when restoring a session?

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024

The callback is the signal. All you have to do if you need it in event form is to dispatch whatever event you want in that callback. Something like:

onAuthStateChange((event, detail) => document.dispatchEvent(new CustomEvent(event, { detail })))

You can then listen for it wherever

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024

This assumes an auth state change will happen. If there is no previous session to restore no authStateChange event will ever fire, meaning you will be waiting indefinitely.

On initial render, you cannot know if a previous session will trigger an authStateChange or not, as not every user has a previous session to restore.

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024

I'm assuming it works like firebase, and that the 'onAuthStateChange' will run to say the user is signed out.

i.e. it's indicating that someone isn't signed in, not that they have just gone through the act of signing out.

The state being resolved as "not signed in / signed out" is a change, and will result in a callback.

from gotrue-js.

prescience-data avatar prescience-data commented on July 17, 2024

This is not the scenario in question, a normal login logout flow is perfect as-is.

The scenario is around the Supabase automatic session restoration if previously authenticated.

When this occurs there is now authStateChange event triggered to signal that the attempt has completed, which is necessary to avoid the two "bad" outcomes I mentioned above. (Eg "redirect flicker" or "infinite resolved wait").

If a user has a previous session and has closed the browser and come back, Supabase will attempt to automatically restore that session. When successful it emits the "LOGGED_IN" event, but nothing if it fails to restore.

So the choices are to wait indefinitely for the logged in event where no previous session is ever restored, use an ugly timeout, or accept the redirect flicker.

from gotrue-js.

CaptainCodeman avatar CaptainCodeman commented on July 17, 2024

Apologies if my assumptions about the quality of supabase were incorrect.

From a quick test, it looks like the initial auth state returned is always correct, at least for being signed in or out and no old-session involved, so maybe it can be relied on. All the examples I've seen appear to work that way - fetch the user and then listen for auth change.

If there truly isn't a way to tell if the state is still pending that would be pretty crap, which is why I was assuming it can be relied on, although I don't know how because refreshing a token would have to be async as you say.

from gotrue-js.

PH4NTOMiki avatar PH4NTOMiki commented on July 17, 2024

I use hacky solution, something like this:

		if(!supabase.auth.currentUser && localStorage.getItem('supabase.auth.token')){
			await new Promise(resolve => {
				const _ = localStorage.removeItem;
				localStorage.removeItem = function removeItem(key) {
					console.log(arguments);
					if(key === 'supabase.auth.token')setTimeout(resolve, 0);
					return _.apply(localStorage, arguments);
				};
				supabase.auth.onAuthStateChange((event, _session) => {
					console.log(event, _session);
					if(['SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED'].includes(event))resolve();
				});
				// timeout after 5s to avoid never-loading state in case some error happens
				setTimeout(resolve, 5e3);
			});
		}

Hope this helps someone

from gotrue-js.

madeleineostoja avatar madeleineostoja commented on July 17, 2024

Looks promising! I couldn't get it to work unfortunately, session and user always returned null even after authenticating. Authentication worked, but seems like it no longer persisted the session after refreshing the window.

from gotrue-js.

xylish7 avatar xylish7 commented on July 17, 2024

Did you use the getSession() method ? As session is deprecated now.

from gotrue-js.

gragland avatar gragland commented on July 17, 2024

My solution is to avoid using the cached session if an access token is present in the URL hash. In that case I know that onAuthStateChange will fire whenever the OAuth or magic link flow completes, so I consider it to be in a loading state. This is from a React/Supabase starter kit I created and so far there hasn't been any issues with this method.

useEffect(() => {
  // Get hash portion of URL if coming from Supabase OAuth or magic link flow.
  // Store on `window` so we can access in other functions after hash is auto-removed.
  window.lastHash = queryString.parse(window.location.hash);

  // We have an `access_token` from OAuth or magic link flow avoid using
  // cached session so that user is `null` (loading state) until process completes.
  // Otherwise, a redirect to a protected page after social auth will redirect
  // right back to login due to cached session indicating they are logged out.
  if (!window.lastHash.access_token) {
    // Get current user and set in state
    const session = supabase.auth.session();
    if (session) {
      setUser(session.user);
    } else {
      setUser(false);
    }
  }

  // Subscribe to user on mount
  const { data } = supabase.auth.onAuthStateChange((event, session) => {
    if (session) {
      setUser(session.user);
    } else {
      setUser(false);
    }
  });
  
  // Unsubscribe on cleanup
  return () => data.unsubscribe();
}, []);

from gotrue-js.

madeleineostoja avatar madeleineostoja commented on July 17, 2024

Did you use the getSession() method ? As session is deprecated now.

Yep used both getSession and getUser, but even just refreshing my browser didn't persist the user being logged in

from gotrue-js.

hf avatar hf commented on July 17, 2024

Hey everyone. We are going to reconsider the behavior of onAuthStateChange in the next revision based on this feedback.

For now, please make sure you understand that the very first event (typically when the session is loaded from local storage or the URL fragment) typically fires before any React hooks run and components are mounted. For this reason you can't rely solely on the event callback to load the initial state of your components. Always use the getSession() method in combination with the callback.

I'll close this issue for now, please re-open if you feel it has not been sufficiently addressed.

from gotrue-js.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.