Introduction
Authentication is one of the most important parts when writing backend projects, you can either spin up one yourself or make use of providers that provide authentication services like Supabase. FastAPI, known for its high performance and automatic OpenAPI and JSON Schema generation, meets Supabase, a robust open-source alternative to Firebase, in a collaborative effort to streamline the development of modern applications.
Throughout this article, we'll walk through the steps of setting up Supabase for authentication, configuring FastAPI to work seamlessly with Supabase, and implementing robust authorization mechanisms to secure your application.
Supabase Setup
It's ideal to use Supabase local development to set up a Supabase project, however, we can head over to Supabase to create an account and create a project and have access to the online dashboard. To set up a local development Supabase project, we can use the Supabase CLI. Once we have the CLI installed, we can then use the supabase init
command to create a project in any folder of ours. The CLI relies on Docker and must be running when trying the command, for the first time running, the CLI will proceed to download required Docker images to run local containers of some Supabase services including PostgreSQL and a local dashboard.
After the init command, we can then run the command supabase start
, this command will start all Supabase services and print local credentials including keys and URLs.
Started supabase local development setup.
API URL: http://localhost:54321
GraphQL URL: http://127.0.0.1:54321/graphql/v1
DB URL: postgresql://postgres:postgres@localhost:54322/postgres
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324
JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
FastAPI setup
As of the time of writing this article, FastAPI requires at least a Python 3.8 version to run. To create a FastAPI project, we will set up a virtual environment, this way, packages installed in our project won't conflict with global python installation. To create a virtual environment, we will run this command at the root of our project.
python -m venv .venv
## or
python3 -m venv .venv
We can then activate the virtual environment.
On Windows:
.venv\Scripts\activate
On macOS and Linux:
source .venv/bin/activate
We will need a host of packages to install alongside FastAPI, including supabase, pydantic-settings and PyJWT. At the root of our project, we will create a requirements.txt file containing our required packages.
fastapi
uvicorn[standard]
supabase
pydantic-settings
PyJWT
To install the packages, we run the command.
pip install -r requirements.txt
It is good practice to have your API keys stored in environment variables rather than have them hard-coded in our code. To load environment variables, FastAPI uses Pydantic, which provides an optional Pydantic feature for loading a settings or config class from environment variables or secrets files through the pydantic-settings package.
In the root of our project, we will create a config.py
and a dotenv file where we will have our SUPABASE_URL
, SUPABASE_ANON_KEY
and SUPABASE_JWT_SECRET
secret.
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
/.env
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
supabase_url: str
supabase_anon_key: str
supabase_jwt_secret: str
model_config = SettingsConfigDict(env_file=".env")
Given a particular secret key in a dotenv file, the Settings class automatically loads its value into its properties using the snake case representation of the key.
Validating JWT
When you sign in using Supabase authentication, Supabase issues JWT tokens(access and refresh tokens) to maintain authorization without having to sign in users over and over again, as long as we can verify the JWT token with the JWT secret that was used to sign the token. At the root of the project, we will create an auth.py
.
from typing import Annotated
from fastapi import HTTPException, Header
import jwt
from config import Settings
settings = Settings()
def validate_jwt(authorization: Annotated[str, Header()]) -> str:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=400, detail="Invalid Authorization")
access_token = authorization.split(" ")[1]
if not access_token:
raise HTTPException(status_code=400, detail="Invalid Authorization")
try:
jwt.decode(
access_token,
settings.supabase_jwt_secret,
algorithms=["HS256"],
options={"verify_aud": False, "verify_signature": True},
)
except jwt.InvalidSignatureError as e:
print(f"Error: {e}")
raise HTTPException(status_code=400, detail="Invalid Authorization")
else:
return access_token
In any frontend app where Authentication has been successful, it is common to have an access token sent over the authorization bearer header when making requests to backend applications. In the validate_jwt
function we use the jwt.decode
function to decode the JWT access token obtained from the authorization bearer header using the settings.supabase_jwt_secret
of the Settings class and return the access_token
.
Creating Supabase client
Before creating a Supabase client, we will need to use the access token obtained for the validate_jwt
function. FastAPI has a very powerful but intuitive Dependency Injection system. Before creating our Supabase client we "depend" on having a valid access token.
from typing import Annotated
from fastapi import Depends, Header
from supabase import create_client, Client
from supabase.client import ClientOptions
from auth import validate_jwt
from config import Settings
settings = Settings()
async def get_supabase_client(
access_token: Annotated[str, Depends(validate_jwt)],
) -> Client:
supabase: Client = create_client(
settings.supabase_url,
settings.supabase_anon_key,
options=ClientOptions(
persist_session=False,
auto_refresh_token=False,
),
)
supabase.auth.set_session(access_token, refresh_token="")
supabase.postgrest.auth(access_token)
return supabase
/libs/supabase.py
Our get_supabase_client
function depends on the validate_jwt
dependency, and any of our route paths in the API that requires just authorization will depend on the validate_jwt
dependency, while any route path requires the Supabase client will depend on the sub-dependency get_supabase_client
. The access token is used to set a Supabase session, and also the postgrest session as this is required for RLS(Row Level Security) policies to hit when making Supabase client calls.
Protecting Route Paths
To protect routes that require only authorization we can use the validate_jwt
dependency directly on the @app
decorator and use the dependencies parameter to set the dependency, while for any route path that requires a Supabase client, we will use the sub-dependency get_supabase_client
on the route path function and receive the a Supabase client object.
from typing import List
from typing_extensions import Annotated
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from supabase import Client
from libs.supabase import get_supabase_client
from auth import validate_jwt
from models.todo import Todo
origins = [
"http://127.0.0.1:3000",
]
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/", dependencies=[Depends(validate_jwt)])
async def home():
return {"msg": "Hello World!"}
@app.get("/todos")
async def get_todos(
supabase: Annotated[Client, Depends(get_supabase_client)]
) -> List[Todo]:
todos = supabase.table("todos").select("*").execute()
return [Todo(**item) for item in todos.data]
Supabase authorization with FastAPI provides a robust solution for building secure and scalable web applications. By leveraging Supabase's authentication and authorization features, developers can offload user management tasks and focus on building the core functionality of their applications.
The full source code can be found here.
A simple Todo app in Next front end can be found here.