Comments (26)
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.
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.
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.
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.
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.
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 whencreateClient()
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()
andsession()
don't make any network requests or get any values directly from localStorage. They simply grab the related object and return it. If you calluser()
beforecreateClient()
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.
@madeleineostoja Maybe #335 will help ? At least it helped in my case.
from gotrue-js.
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.
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.
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.
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.
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.
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.
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.
But this is the point, presently there is no event to signal that auth has resolved when restoring a session?
from gotrue-js.
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.
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.
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.
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.
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.
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.
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.
Did you use the getSession()
method ? As session is deprecated now.
from gotrue-js.
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.
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.
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)
- "User with this email not found" error when using generateLink HOT 9
- Google OAuth doesn't work in Safari with next-js-auth-helpers HOT 1
- New, unsigned in user can not be deleted from supabase console. HOT 1
- [email protected] breaks client auth with edge functions HOT 15
- New error code is missing in error object
- user object warning logged, even when not touching `session.user` HOT 34
- Security and performance risk with `getUser` and `getSession` HOT 6
- Global supabase.auth.signOut() doesn't fire the "SIGNED_OUT" event for onAuthStateChange in other instances where a user is logged in HOT 5
- Current session lost when auth function call fails HOT 1
- Impossible to check null session without getSession warning HOT 9
- `getSession` should validate the session with the JWT_SECRET HOT 2
- getAuthenticatorAssuranceLevel() triggers "getSession() could be insecure" warnings HOT 3
- PKCE flow issue with other than supabase `code` query in URL
- Still having getSession warning whenever _saveSession is called HOT 7
- Session from session_id claim in JWT does not exist HOT 1
- Token has expired or is invalid or duplicate key value violates unique constraint "refresh_tokens_pkey" HOT 1
- NPM version tagged as latest still 2.62.2 HOT 1
- Client-side Supabase Auth Session/Access Token Expires Too Soon HOT 3
- No connection to my local hosted supbabase/auth instance on newer versions HOT 1
- `errorCode` is `undefined` because `data.error` is not processed HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from gotrue-js.