Supabase Python Understanding Struggles - how to do OAuth for multiple users and which token to stor
Initially I got supabase auth working fine with my python project for both email + password as well as Google OAuth.
Then I realized I use a single supabase client for all users, which meant that all users shared the same user profile lol.
I'm handling all the supabase auth stuff in my python backend fyi, i.e. redirection, exchange code for session etc.
Now I'm trying to figure out:
A) Which token do I need to store as a cookie to persist sessions? I.e. when a user logs in from the same browser but with a different tab, the user should be logged in automatically
B) How to pass the token from the cookie to the supabase client to retrieve the correct session?
C) Google OAuth: when using the global supabase instance, I can login with google. In my tests however I always fetched a new supabase instance on each page load. Afterwards I get redirected from google with the
code=
in the header. But now I get the classic oth auth code and code verifier should be non-empty
error. This makes me think: can't I use separate supabase clients for OAuth?
My main source of knowledge are the docs: https://supabase.com/docs/reference/python/auth-setsession , happy about any other resources.Python: Set the session data | Supabase Docs
Supabase API reference for Python: Set the session data
32 Replies
Your Python framework will have a way of managing instances per session. You should use that when creating clients.
okay thank you let me think more into this direction.
is there any way of restoring the supabase client without having to store the full client? With some identifier or something?
Not sure what you mean here.
what differentiates one supabase client from another?
Probably some tokens and IDs right?
So if I just store these values, perhaps I wouldn't need to keep the whole supabase client in memory but just these values to re-instantiate the same client.
Also: my python framework doesn't persist user sessions across browser tabs.
So I'd need to store some JWT or something to re-instantiate the session on new tabs no?
Your framework will handle that, the internals shouldn't be something you worry about. If you want to know more then you will need to investigate how your framework does it.
Which framework are you using? also how are you storing the user's state when they sign in?
I'm using https://reflex.dev
In the per user state it's only possible to store objects that can be pickled. the supabase client can't be pickled itself (
cannot pickle '_thread.RLock' object
).
Currently I'm getting the user email from the auth calls to supabase and store them as strings in the backend.Reflex Β· Web apps in Pure Python
The open-source framework to build and deploy web apps using Python.
I think I've helped you before, now that you mention reflex
But even if I could pickle the supabase client, I'd need to store some supabase access token in local storage.
Everything I'm doing with supabase in my app is happening in the backend.
So there's nothing that automatically stores some auth tokens in the browser storage.
haha yeah and thank you very much for your fast answers last time and today again π
last time I apparently had an environment issue.
It got solved by re-instantiating the virtual environment I used for whatever reason.
So reflex uses FastAPI under the hood which means you have access to cookies. You can create a cookie store for the Supabase client and it will persist the user's state in the cookie.
yes that was I was also thinking.
And I wanted to ask: what should I store in the cookie?
FastAPI is a little tricky too because of how it spawn instances. When you start adding more workers you need a cookie store on the user's computer to track the cookie store itself or in my case redis store.
The
supabase
library would handle this for you, you just need to pass it the cookie store when creating the client.
This is a flask example I have on GitHub, look at this line https://github.com/silentworks/flask-notes/blob/main/app/supabase.py#L20
I'm currently working on a FastAPI example but won't be done today. More likely to finish by the end of the week that will show how to do this. But it's similar to the Flask example I shared above.So I just have to give
create_client
or Client
of supabase a storage option that looks like this but the implementation is adjusted to fastapi / reflex to interact with local storage?
Yes, use
create_client
and not Client
.ok, I do use
create_client
, just saw you use Client
in the repo you shared
Thanks so far Andrew will try it out!
Thanks again for pointing me in the right direction.
Yesterday I was working long on it, but couldn't fully make it happen.
In Reflex I can't create arbitrary cookies on the fly.
So what I'm doing instead, I create one supabase cookie and store all the other info inside as JSON:
Well anyway, I don't want you to look at the code, but ask:
- does supabase only store a single cookie at a time?
- or should it store multiple values at the same time during the auth flow?
It looks to me as if only one value is stored in the cookie at the same time. The fields change, i.e. at one point it stores
supabase.auth.token-code-verifier
and another time supabase.auth.token
. But it always seems to overwrite values, it never stores more than one at the same time.Thatβs the correct behaviour. Are you testing from localhost? Also are you testing in the same browser?
I'm testing from localhost on the same browser.
Only testing google oauth all this time btw.
Oh so if that's the correct behavior that only a single cookie is stored, then everything should be fine.
That thing that "isn't" working is that the session doesn't persist across browser windows, or even page redirects.
I.e. the redirect from google with the code
http://localhost:3000/?code=2bfa6...0e2237
, I parse the code and exchange it for a session - that works ππΎ
But then I redirect to http://localhost:3000
(without the code param), I perform another supabase_client.auth.get_session()
with a fresh supabase client.
I would think that this fresh supabase client reads the cookies and sets the session.
But the session is null, and the user isn't logged in (after the redirect).
This is the same with cross tab behavior - when I log in (without redirecting I can log in), and open another browser tab it doesn't restart the session.
Is this intended behavior? I would have thought the session persists through the cookies.This means the cookie isn't being persisted in that case.
Is this an open source project? if so you can share the repo with me and I can take a look
No and the project also has a lot of code that's unrelated to supabase.
But created a new project now that only uses the supabase auth logic: https://github.com/dentroai/supabase_auth_X_reflex
Tried to add to the README everything you might need to know to run it and understand the project.
GitHub
GitHub - dentroai/supabase_auth_X_reflex: Demo App to use Supabase ...
Demo App to use Supabase Auth with Reflex. Contribute to dentroai/supabase_auth_X_reflex development by creating an account on GitHub.
I tested it, it behaves the same way as my other app where I'm trying to add supabase auth
Thanks for this. I'll take a look at it and get back to you
I've tested with email and password and the cookie seems to persist. I refreshed and tested in a different browser tab and the state is kept.
@dachsteinhustler how do you get a query parameter from the url in reflex?
I get the query params with
self.router.page.params
in reflex.
yes the state is kept with email + password in other tabs.
But when I close the browser and open again I'm not logged in automatically.
I think I mentioned this in the Readme somewhere.
The bigger issue though is the google auth with the redirects.Google auth requires a bit more work where you have to redirect to a page that does the
supabase.auth.exchangeCodeForSession
call.
So the sign_in_with_oauth
will redirect to a page https://github.com/silentworks/flask-notes/blob/main/app/auth.py#L37 then on that page you handle the exchange https://github.com/silentworks/flask-notes/blob/main/app/auth.py#L107-L115yeah that's exactly what I'm doing in the demo repo.
The thing is that right now I'm instantiating a new supabase client on every new page.
I.e. also after I redirect from the callback to the dashboard, as you do in your case
return redirect(url_for(next))
Not saying it's the right thing to do to always instantiate a fresh supabase client. Just couldn't find another way until now.
And when I get the fresh supabase client, it can't find any session.
Not sure whether this is a supabase issue, a reflex issue or something with my local setup.
But trying to figure it out.
That's why I initially asked about which cookies are expected to be stored by supabase. Cause I can only see one at a time when doing google oauth (gotta check again for email + password).
The line logger.info(f"Current keys in auth_storage: {list(self.auth_storage['toplevel'].keys())}")
never prints out more than a single cookie var at a time.I tried to remove the
get_supabase_client
outside of the AuthState
class but couldn't get rx.Cookie
working once it's no longer inside of the AuthState
class. I don't fully understand reflex to work out how to move it outside without the code erroring out.yeah lol took me quite a bit of time to actually get it working like that.
Explanation became a bit long, sorry about the lack of conciseness.
Explanation:
- in reflex only state vars can be used with
rx.Cookie
(https://reflex.dev/docs/api-reference/browser-storage/#rx.cookie)
- in reflex only classes that inherit from rx.State
can have state vars. These are 'state classes'.
- State classes are basically the backend of reflex apps. The frontend (functions in other files that return rx.Component
) interacts via the State classes event handlers with the backend.
- methods / state vars of one state class can only be accessed from another state var, e.g. reflex_cookie_storage = await self.get_state(ReflexCookieStorage)
and then do something with this state e.g. print(reflex_cookie_storage.auth_storage)
- side note: a single state classes exists for every user session, thus making them perfect for per user supabase clients.
- so for the cookie store, we need a class that inherits both SyncSupportedStorage
and rx.State
- I created a separate State class called ReflexCookieStorage
. Probably I could just inherit SyncSupportedStorage
in the main AuthState
and move get_item
, set_item
and remove_item
inside there, but not sure.
- To pass the cookie store to the supabase, you need to instantiate in a state class. In the demo either in AuthState
or in ReflexCookieStorage
. Because only in state classes you can use cookies or get another state.
- that's why you can move the get_supabase_client
only to another State class. Or don't pass the cookie store class into the supabase client, then you can instantiate supabase from anywhere!
btw not sure if you use a lot of LLMs, but sonnet-3.5 knows quite a bit about reflex.
so when you do res = supabase.auth.exchange_code_for_session({"auth_code": code})
, you shouldn't need to call set_session
later on, right?
Cause in your flask demo you just exchange the token and do not set any session.The
exchange_code_for_session
function handles the setting of the session internally.yeah I see.
So I pinpointed the issue: somehow I can't set the value of the
supabase.auth.token
cookie dynamically in reflex.
If I copy the value, and set it hardcoded, it works.
No idea what that could be, perhaps the format is wrong or I don't know what.
Getting slowly crazy
but I only need access_token
and the refresh_token
to set a new session right?
So if I just store those two manually in a cookie and retrieve them to instantiate a new session, I should be fine no?Only if that is the only client running the session. If two clients have that session they will fail when either refreshes the token
ok great so I just need a separate session for every user, that should already be the case with the code from the demo repo thanks!
thank you guys very much for your help ππΎ
Right now I got it to work with a dirty workaround where I use the cookie store AND additionally store the access token and refresh token.
Still using the cookie store cause it does work for email + password and I couldn't figure out how to get the token verifier without using the cookie store.
I think it's some issue with reflex and storing the auth token upon exchanging the code for a token when doing google oauth.
Will test it out and then see whether I can improve it!
It it possible to use
AsyncSupportedStorage
instead SyncSupportedStorage
with the flask setup that you have Andrew?By default it will use that if you are using the
create_async_client
instead of create_client
. Also note that Flask is a sync framework, it's not async. FastAPI is async.thank you! I also tried this now.
Giving up now to add supabase auth to my reflex app after trying unsuccessfully for far too long.