Setup User Auth for your Reflex Python app using supabase (supabase-py)
Tags: reflex, webdev, supabase, python
Published: 2023-10-31 20:56:19

In this article, we will configure user authentication for your reflex app using supabase-py. 

[Supabase](https://supabase.com/) 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](https://supabase.com/docs/reference/python/introduction) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8iprytz2epdukvvvvlin.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcfmidiz0adcbvz2k449.png)

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

![authz](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t6fynln2j7s4yuxsebat.png)

You can run the app using the following command in your terminal to see a welcome page when you go to [http://localhost:3000/](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**:
![authhomepage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g74e0vbluqg17pwyw9n1.png)



## 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:**
![authlogin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sltbszcugxajnq2peue3.png)


## 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:**

![authsupabaseregistration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yox3hgc212e8wutychx5.png)


## .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