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
silentworks
silentworksβ€’9mo ago
Your Python framework will have a way of managing instances per session. You should use that when creating clients.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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?
silentworks
silentworksβ€’9mo ago
Not sure what you mean here.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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?
silentworks
silentworksβ€’9mo ago
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?
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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.
silentworks
silentworksβ€’9mo ago
I think I've helped you before, now that you mention reflex
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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.
silentworks
silentworksβ€’9mo ago
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.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
yes that was I was also thinking. And I wanted to ask: what should I store in the cookie?
silentworks
silentworksβ€’9mo ago
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.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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?
class FlaskSessionStorage(SyncSupportedStorage):
def __init__(self):
self.storage = session

def get_item(self, key: str) -> str | None:
if key in self.storage:
return self.storage[key]

def set_item(self, key: str, value: str) -> None:
self.storage[key] = value

def remove_item(self, key: str) -> None:
if key in self.storage:
self.storage.pop(key, None)
class FlaskSessionStorage(SyncSupportedStorage):
def __init__(self):
self.storage = session

def get_item(self, key: str) -> str | None:
if key in self.storage:
return self.storage[key]

def set_item(self, key: str, value: str) -> None:
self.storage[key] = value

def remove_item(self, key: str) -> None:
if key in self.storage:
self.storage.pop(key, None)
silentworks
silentworksβ€’9mo ago
Yes, use create_client and not Client.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
ok, I do use create_client, just saw you use Client in the repo you shared
No description
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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:
class ReflexCookieStorage(rx.State, SyncSupportedStorage):
"""A storage implementation that uses Reflex's Cookie state to store session data."""

auth_storage: str = rx.Cookie("", name="auth_storage")

def get_item(self, key: str) -> str | None:
"""Get an item from storage."""
try:
if not self.auth_storage:
return None
data = self.auth_storage
return data["toplevel"].get(key)
except Exception as e:
logger.error(f"Error getting storage item: {e}. Key: {key}")
return None

def set_item(self, key: str, value: str) -> None:
"""Set an item in storage."""
logger.info(f"Setting storage item: {key} = {value}")
try:
current_data = {"toplevel": {}}
if self.auth_storage and self.auth_storage["toplevel"]:
current_data = self.auth_storage
current_data["toplevel"][key] = value
self.auth_storage = current_data
except Exception as e:
logger.error(f"Error setting storage item: {e}. Key: {key}, Value: {value}")

def remove_item(self, key: str) -> None:
"""Remove an item from storage."""
try:
if not self.auth_storage:
return
current_data = self.auth_storage
if key in current_data["toplevel"]:
del current_data["toplevel"][key]
self.auth_storage = current_data
except Exception as e:
logger.error(f"Error removing storage item: {e}. Key: {key}")
class ReflexCookieStorage(rx.State, SyncSupportedStorage):
"""A storage implementation that uses Reflex's Cookie state to store session data."""

auth_storage: str = rx.Cookie("", name="auth_storage")

def get_item(self, key: str) -> str | None:
"""Get an item from storage."""
try:
if not self.auth_storage:
return None
data = self.auth_storage
return data["toplevel"].get(key)
except Exception as e:
logger.error(f"Error getting storage item: {e}. Key: {key}")
return None

def set_item(self, key: str, value: str) -> None:
"""Set an item in storage."""
logger.info(f"Setting storage item: {key} = {value}")
try:
current_data = {"toplevel": {}}
if self.auth_storage and self.auth_storage["toplevel"]:
current_data = self.auth_storage
current_data["toplevel"][key] = value
self.auth_storage = current_data
except Exception as e:
logger.error(f"Error setting storage item: {e}. Key: {key}, Value: {value}")

def remove_item(self, key: str) -> None:
"""Remove an item from storage."""
try:
if not self.auth_storage:
return
current_data = self.auth_storage
if key in current_data["toplevel"]:
del current_data["toplevel"][key]
self.auth_storage = current_data
except Exception as e:
logger.error(f"Error removing storage item: {e}. Key: {key}")
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.
silentworks
silentworksβ€’9mo ago
That’s the correct behaviour. Are you testing from localhost? Also are you testing in the same browser?
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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.
silentworks
silentworksβ€’9mo ago
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
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
I tested it, it behaves the same way as my other app where I'm trying to add supabase auth
silentworks
silentworksβ€’9mo ago
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?
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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.
silentworks
silentworksβ€’9mo ago
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-L115
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
yeah 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.
silentworks
silentworksβ€’9mo ago
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.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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?
client.auth.set_session(
auth_response.session.access_token,
auth_response.session.refresh_token,
)
client.auth.set_session(
auth_response.session.access_token,
auth_response.session.refresh_token,
)
Cause in your flask demo you just exchange the token and do not set any session.
silentworks
silentworksβ€’9mo ago
The exchange_code_for_session function handles the setting of the session internally.
dachsteinhustler
dachsteinhustlerOPβ€’9mo ago
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?
garyaustin
garyaustinβ€’9mo ago
Only if that is the only client running the session. If two clients have that session they will fail when either refreshes the token
dachsteinhustler
dachsteinhustlerOPβ€’8mo ago
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?
silentworks
silentworksβ€’8mo ago
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.
dachsteinhustler
dachsteinhustlerOPβ€’8mo ago
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.

Did you find this page helpful?