In this article, we will configure user authentication for your reflex app using supabase-py.
Supabase is an open-source Firebase alternative. It provides you with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings so as to develop a scalable system in less time.
supabase-py is Supabase client for Python. You can use supabase-py
to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
Outline
- Register for supabase
- Create a new supabase project
- Get your Project URL and API key (anon public)
- Get your Project JWT Secret
- Create a new folder, open it with a code editor
- Create a virtual environment and activate
- Install requirements
- reflex setup
- .env file
- supabase__client.py
- auth_supabase.py
- base_state.py
- login.py
- registration.py
- .gitignore
- run app
- conclusion
Register for supabase
You will need to create an account with Supabase. You can either go for their free or pro or team or enterprise plan depending on the scale of your project. For this tutorial, I will go with the free plan but you can choose other plans.
Go to https://supabase.com/pricing and select a plan, then signup.
Create a new supabase project
Go to https://supabase.com/dashboard/projects to create a new supabase project to set up a dedicated environment for your application or project. You will give your project a name, and Database password.
Get your Project URL and API key (anon public)
Once your project is created, you will see your Project URL and API key (anon public). You can copy them and save to a safe file.
Get your Project JWT Secret
Go to Project Settings > API > JWT Settings
to get your Project JWT Secret
Create a new folder, open it with a code editor
We will build the auth project now. Create a new folder on your computer and name it auth_supabase
then open it with a code editor like VS Code.
Create a virtual environment and activate
Open the terminal. Use the following command to create a virtual environment .venv
and activate it:
python3 -m venv .venv
source .venv/bin/activate
Install requirements
We will install reflex
to build the app, supabase
the client for Python, Python-dotenv
to read key-value pairs from a .env
file, PyJWT (JSON Web Token implementation in Python) and set them as environment variables.
Run the following command in the terminal:
pip install reflex==0.4.5 supabase==2.4.0 python-dotenv==1.0.0 PyJWT==2.8.0
reflex setup
Now, we need to create the project using reflex. Run the following command to initialize the template app in auth_supabase
directory.
reflex init --template blank
The above command will create the following file structure in auth_supabase
directory:
You can run the app using the following command in your terminal to see a welcome page when you go to http://localhost:3000/ in your browser
reflex run
.env file
Create a new file .env
in your project root directory and add the following to set your API key environment variables
SUPABASE_URL=""
SUPABASE_KEY=""
JWT_SECRET=""
JWT_ALGORITHM="HS256"
In the empty string of SUPABASE_URL
and SUPABASE_KEY
place your Project URL and API key (anon public) you saved initially. These will help us communicate to supabase. Also, replace the empty string of JWT_SECRET with the copied JWT SECRET.
supabase__client.py
Go to the auth_supabase
subdirectory and create a new file supabase__client.py
. Replace with the following code:
import os
from dotenv import load_dotenv
import logging
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("watchfiles").setLevel(logging.WARNING)
import supabase
# load env
load_dotenv()
def supabase_client():
# setup supabase
supabase_url = os.getenv("SUPABASE_URL")
supabase_key = os.getenv("SUPABASE_KEY")
client = supabase.Client(supabase_url, supabase_key)
return client
The above code creates a supabase client. We will use the supabase client in other files.
auth_supabase.py
We will build the homepage of the app. Go to the auth_supabase
subdirectory and open the auth_supabase.py
file. Replace with the following code:
"""App module to demo authentication with supabase."""
import reflex as rx
from .base_state import State
from .registration import registration_page as registration_page
from .login import require_login
def show_logout_or_login_comp() -> rx.Component:
return rx.cond(
State.is_hydrated & State.token_is_valid,
rx.chakra.box(
rx.chakra.link("Protected Page", href="/protected",padding_right="10px"),
rx.chakra.link("Logout", href="/", on_click=State.do_logout),
spacing="1.5em",
padding_top="10%",
),
rx.chakra.box(
rx.chakra.link("Register", href="/register",padding_right="10px"),
rx.chakra.link("Login", href="/login"),
spacing="1.5em",
padding_top="10%",
)
)
def index() -> rx.Component:
"""Render the index page.
Returns:
A reflex component.
"""
return rx.fragment(
rx.chakra.color_mode_button(rx.chakra.color_mode_icon(), float="right"),
rx.chakra.vstack(
rx.chakra.heading("Welcome to my homepage!", font_size="2em"),
show_logout_or_login_comp(),
)
)
@require_login
def protected() -> rx.Component:
"""Render a protected page.
The `require_login` decorator will redirect to the login page if the user is
not authenticated.
Returns:
A reflex component.
"""
return rx.chakra.vstack(
rx.chakra.heading(
"Protected Page", font_size="2em"
),
rx.chakra.link("Home", href="/"),
rx.chakra.link("Logout", href="/", on_click=State.do_logout),
)
app = rx.App()
app.add_page(index)
app.add_page(protected)
The above code imports various modules, including "reflex" for web components and state management.
The show_logout_or_login_comp
function renders a component based on whether the frontend has access to the latest state values from the backend and the user's token is valid.
The index()
function defines the main page of the application.
The protected()
function is a protected page that requires authentication. It uses the @require_login
decorator to ensure that only authenticated users can access it. It provides links to the homepage and a logout option.
Finally, an rx.App
object is created, and the "index" and protected
pages are added to it.
The above code renders the following page:
base_state.py
Create a new file base_state.py
in the auth_supabase
subdirectory and add the following code.
"""
Top-level State for the App.
Authentication data is stored in the base State class so that all substates can
access it for verifying access to event handlers and computed vars.
"""
import os
import jwt
import time
import reflex as rx
from dotenv import load_dotenv
# load env
load_dotenv()
JWT_SECRET = os.getenv("JWT_SECRET")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM")
class State(rx.State):
auth_token: str = rx.Cookie("auth_token",secure=True)
def do_logout(self):
"""signout."""
self.auth_token = ""
yield
@rx.cached_var
def decodeJWT(self) -> dict:
"""
Decode the JWT token.
This method decodes the JWT token using the provided secret and algorithm,
verifies its authenticity, and checks if it's within the valid time range.
Returns:
dict: A dictionary containing the decoded JWT token if it's valid,
otherwise returns an empty dictionary.
Raises:
Exception: Any exception encountered during the decoding process.
"""
try:
decoded_token = jwt.decode(self.auth_token,JWT_SECRET,do_verify=True,algorithms=[JWT_ALGORITHM],audience="authenticated",leeway=1)
return decoded_token if decoded_token["exp"] >= time.time() and decoded_token["iat"] <= time.time() else None
except Exception as e:
return {}
@rx.var
def token_is_valid(self) -> bool:
"""
Check if the JWT token is valid.
This method checks if the JWT token is valid by attempting to decode it.
If decoding is successful, it returns True, indicating that the token is valid.
If decoding fails for any reason, it returns False.
Returns:
bool: True if the JWT token is valid, False otherwise.
"""
try:
return bool(
self.decodeJWT
)
except Exception:
return False
The above code declares a "State" class, which inherits from rx.State
to manage the application's state. It has a cookie (string State Var) auth_token
to store an authentication token securely.
The do_logout
method within the State
class is responsible for signing the user out.
decodeJWT
method decodes the JWT token using the provided secret and algorithm, verifies its authenticity, and checks if it's within the valid time range. It returns a dictionary containing the decoded JWT token if it's valid, otherwise, it returns an empty dictionary.
token_is_valid
method checks if the JWT token is valid by attempting to decode it. If decoding is successful, it returns True, indicating that the token is valid else it returns False.
login.py
Create a new file login.py
in the auth_supabase
subdirectory and add the following code.
"""Login page and authentication logic."""
import reflex as rx
from .base_state import State
from .supabase__client import supabase_client
LOGIN_ROUTE = "/login"
REGISTER_ROUTE = "/register"
class LoginState(State):
"""Handle login form submission and redirect to proper routes after authentication."""
error_message: str = ""
redirect_to: str = ""
is_loading: bool = False
def on_submit(self, form_data) -> rx.event.EventSpec:
"""Handle login form on_submit.
Args:
form_data: A dict of form fields and values.
"""
# set the following values to spin the button
self.is_loading = True
yield
self.error_message = ""
email = form_data["email"]
password = form_data["password"]
try:
user_sign_in = supabase_client().auth.sign_in_with_password({"email": email, "password": password})
self.auth_token = user_sign_in.session.access_token
self.error_message = ""
return LoginState.redir() # type: ignore
except:
self.error_message = "There was a problem logging in, please try again."
# reset state variable again
self.is_loading = False
yield
def redir(self) -> rx.event.EventSpec | None:
"""Redirect to the redirect_to route if logged in, or to the login page if not."""
if not self.is_hydrated:
# wait until after hydration
return LoginState.redir() # type: ignore
page = self.get_current_page()
if not self.token_is_valid and page != LOGIN_ROUTE:
self.redirect_to = page
# reset state variable again
self.is_loading = False
yield
return rx.redirect(LOGIN_ROUTE)
elif page == LOGIN_ROUTE:
# reset state variable again
self.is_loading = False
yield
return rx.redirect(self.redirect_to or "/")
@rx.page(route=LOGIN_ROUTE)
def login_page() -> rx.Component:
"""Render the login page.
Returns:
A reflex component.
"""
login_form = rx.chakra.form(
rx.chakra.input(placeholder="email", id="email", type_="email"),
rx.chakra.password(placeholder="password", id="password"),
rx.chakra.button("Login", type_="submit", is_loading=LoginState.is_loading),
width="80vw",
on_submit=LoginState.on_submit,
)
return rx.fragment(
rx.cond(
LoginState.is_hydrated, # type: ignore
rx.chakra.vstack(
rx.cond( # conditionally show error messages
LoginState.error_message != "",
rx.chakra.text(LoginState.error_message),
),
login_form,
rx.chakra.link("Register", href=REGISTER_ROUTE),
padding_top="10vh",
),
)
)
def require_login(page: rx.app.ComponentCallable) -> rx.app.ComponentCallable:
"""Decorator to require authentication before rendering a page.
If the user is not authenticated, then redirect to the login page.
Args:
page: The page to wrap.
Returns:
The wrapped page component.
"""
def protected_page():
return rx.fragment(
rx.cond(
State.is_hydrated,
rx.cond(
State.token_is_valid, page(), login_page()
),
rx.chakra.center(
# When this spinner mounts, it will redirect to the login page
rx.chakra.spinner(),
),
)
)
protected_page.__name__ = page.__name__
return protected_page
Above, the LoginState
class manages the state of the login page. It handles form submission with the on_submit
method, which sets the is_loading
flag to indicate form submission and attempts to sign in the user using Supabase. If successful, it sets the access token, and if there's an error, it displays an error message. The redir
method handles page redirection based on the user's token validity.
The login_page()
function defines the login page itself, rendering a login form and displaying error messages if present. It also provides a link to the registration page.
The require_login
decorator function ensures that only authenticated users can access certain pages. It wraps the protected page and redirects unauthenticated users to the login page.
The above code renders the following page:
registration.py
Create a new file registration.py
in the auth_supabase
subdirectory and add the following code.
"""New user registration form and validation logic."""
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
import reflex as rx
from .base_state import State
from .login import LOGIN_ROUTE, REGISTER_ROUTE
import re
from .supabase__client import supabase_client
class RegistrationState(State):
"""Handle registration form submission and redirect to login page after registration."""
success: bool = False
error_message: str = ""
is_loading: bool = False
async def handle_registration(
self, form_data
) -> AsyncGenerator[rx.event.EventSpec | list[rx.event.EventSpec] | None, None]:
"""Handle registration form on_submit.
Set error_message appropriately based on validation results.
Args:
form_data: A dict of form fields and values.
"""
# set the following values to spin the button
self.is_loading = True
yield
email = form_data["email"]
if not email:
self.error_message = "email cannot be empty"
rx.set_focus("email")
# reset state variable again
self.is_loading = False
yield
return
if not is_valid_email(email):
self.error_message = "email is not a valid email address."
rx.set_focus("email")
# reset state variable again
self.is_loading = False
yield
return
password = form_data["password"]
if not password:
self.error_message = "Password cannot be empty"
rx.set_focus("password")
# reset state variable again
self.is_loading = False
yield
return
if password != form_data["confirm_password"]:
self.error_message = "Passwords do not match"
[
rx.set_value("confirm_password", ""),
rx.set_focus("confirm_password"),
]
# reset state variable again
self.is_loading = False
yield
return
# sign up with supabase
supabase_client().auth.sign_up({
"email": email,
"password": password,
})
# Set success and redirect to login page after a brief delay.
self.error_message = ""
self.success = True
self.is_loading = False
yield
await asyncio.sleep(3)
yield [rx.redirect(LOGIN_ROUTE), RegistrationState.set_success(False)]
@rx.page(route=REGISTER_ROUTE)
def registration_page() -> rx.Component:
"""Render the registration page.
Returns:
A reflex component.
"""
register_form = rx.chakra.form(
rx.chakra.input(placeholder="email", id="email", type_="email"),
rx.chakra.password(placeholder="password", id="password"),
rx.chakra.password(placeholder="confirm", id="confirm_password"),
rx.chakra.button("Register", type_="submit", is_loading=RegistrationState.is_loading,),
width="80vw",
on_submit=RegistrationState.handle_registration,
)
return rx.fragment(
rx.cond(
RegistrationState.success,
rx.chakra.vstack(
rx.chakra.text("Registration successful, check your mail to confirm signup so as to login!"),
rx.chakra.spinner(),
),
rx.chakra.vstack(
rx.cond( # conditionally show error messages
RegistrationState.error_message != "",
rx.chakra.text(RegistrationState.error_message),
),
register_form,
padding_top="10vh",
),
)
)
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
The above code declares a RegistrationState
class which manages the state of the registration page. It handles form submission with the handle_registration
method, which sets the is_loading
flag to indicate form submission and validates the email and password provided. If validation fails, it sets an error message and returns focus to the respective field. If successful, it uses Supabase to sign up the user and sets the success flag, which is followed by a delay and redirection to the login page.
The registration_page()
function defines the registration page, rendering the registration form and displaying success messages or error messages based on the registration process. It also provides a link to the login page.
The is_valid_email
function checks if an email address is valid based on a regular expression pattern. If it matches, the email is considered valid.
The above code renders the following page:
.gitignore
Add the following to the .gitignore file:
*.db
*.py[cod]
.web
__pycache__/
.venv/
.env
run app
Run the following in the terminal to start the app:
reflex run
When a user registers, supabase sends a confirm sign-up message to the user's mail. the user's details get saved in supabase users model. You can also login and logout from the web app. You will notice that when you login you will now be able to access a protected page that is meant for only authenticated users. The protected page is added only to show an example.
conclusion
You can get the code: https://github.com/emmakodes/auth_supabase
Top comments (1)
Super lovely! Thanks for this!