DEV Community

Emmanuel Onwuegbusi
Emmanuel Onwuegbusi

Posted on • Updated on

Setup User Auth for your Reflex Python app using supabase (supabase-py)

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.
supabase pricing

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.
supabase new project

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
Enter fullscreen mode Exit fullscreen mode
source .venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The above command will create the following file structure in auth_supabase directory:

authz

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
Enter fullscreen mode Exit fullscreen mode

.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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:
authhomepage

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:
authlogin

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
Enter fullscreen mode Exit fullscreen mode

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:

authsupabaseregistration

.gitignore

Add the following to the .gitignore file:

*.db
*.py[cod]
.web
__pycache__/
.venv/
.env
Enter fullscreen mode Exit fullscreen mode

run app

Run the following in the terminal to start the app:

reflex run
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
paulwababu profile image
paulsaul621

Super lovely! Thanks for this!