FastAPI & Supabase: JWT Authentication Guide

by Jhon Lennon 45 views

Hey guys! Ever wanted to build a secure and robust backend using FastAPI and Supabase? You're in luck! This guide will walk you through setting up JWT (JSON Web Token) authentication and authorization in your FastAPI application, using Supabase for user management and database needs. We'll cover everything from the basics to more advanced concepts, ensuring your APIs are secure and your users are well-managed. Buckle up, because we're about to dive into the world of secure APIs!

Setting the Stage: Why FastAPI, Supabase, and JWT?

So, why these tools, specifically? Well, FastAPI is a modern, fast (hence the name!), and efficient framework for building APIs with Python. It's perfect for creating high-performance backends. Supabase, on the other hand, is an open-source Firebase alternative. It provides a powerful backend-as-a-service (BaaS) that includes a database (PostgreSQL), authentication, real-time functionality, and storage. It is awesome. Finally, JWTs are the industry standard for securely transmitting information between parties as a JSON object. They are particularly well-suited for API authentication and authorization because they are self-contained and easily verifiable.

Let's break down the advantages. FastAPI shines due to its speed, automatic data validation, and easy-to-use API documentation (thanks to OpenAPI and Swagger UI). It allows developers to build APIs with remarkable speed and reduced boilerplate code. Supabase handles the heavy lifting of user management, database interactions, and real-time updates. This allows us to focus on our core business logic. JWTs, in turn, provide a secure and stateless way to authenticate users. They're perfect for API-driven architectures because they eliminate the need to store session information on the server.

Here’s a practical analogy. Imagine building a house. FastAPI is your construction crew, efficiently building the structure (your API). Supabase is the land, the utilities, and the foundation (user management, database), already prepared for you. JWTs are the security badges, granting access to specific areas of the house (API endpoints) based on your credentials. Combining these three elements, you get a powerful and scalable backend solution!

Supabase Setup: Your User Management Hub

First things first, you’ll need a Supabase account. Head over to the Supabase website and create a new project. Once your project is set up, navigate to the authentication section in the Supabase dashboard. Supabase provides several authentication methods, but for this guide, we'll focus on email/password authentication (although you can easily integrate other methods like OAuth with providers such as Google, Facebook, etc.).

Enable email/password authentication and set up any necessary security configurations, such as password strength requirements and email verification. Supabase handles all the user registration, login, and password reset workflows for you. This is a massive time saver, letting you focus on the API logic and core features. Remember to note your Supabase project URL and API key; you'll need them later. These keys are like the keys to your house, so keep them safe and secure, never expose them to the public, and always store them as environment variables.

Inside your Supabase project, consider creating a roles table if you plan to implement role-based access control. This table will map users to different roles (e.g., admin, editor, viewer). You can also create a permissions table to define the specific actions each role can perform. This is all about securing your app. These tables help manage authorization and ensure only authorized users can access specific API endpoints or resources.

Don’t worry; we will get our hands dirty with the code later. The setup process is a straightforward one. The key here is to leverage the features Supabase provides to handle user management, which includes user registration, login, and password reset. The less time you spend on repetitive tasks like these, the better! The user management and authentication system provided by Supabase is going to handle a lot of the heavy lifting for you!

FastAPI: Crafting Your API Endpoints

Now, let's get our hands dirty with some FastAPI code! First, you'll need to install the necessary packages. Create a new Python project and install the following packages using pip:

pip install fastapi uvicorn python-dotenv jose[cryptography] python-multipart
  • FastAPI: The core framework for building your API.
  • uvicorn: An ASGI server to run your FastAPI application.
  • python-dotenv: To load environment variables from a .env file.
  • jose[cryptography]: For JWT encoding and decoding.
  • python-multipart: For handling file uploads.

Create a new file, for example, main.py, and start by importing the necessary modules and setting up your Supabase client:

import os
from typing import Annotated

from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from supabase import Client, create_client

load_dotenv()

# Environment variables
SUPABASE_URL = os.getenv("SUPABASE_URL")
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))

# Supabase client
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)

# FastAPI app
app = FastAPI()

# OAuth2 scheme for JWT
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

Next, define your login endpoint, which will handle user authentication and issue JWTs upon successful login. This endpoint will accept user credentials (email and password), authenticate against Supabase, and generate a JWT if the login is successful. This is your authentication gateway, so get it right!

# Define token data
from datetime import datetime, timedelta

from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    email: str | None = None

# Function to create access token
def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


# Login endpoint
@app.post("/token")
async def login_for_access_token(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
):
    email = form_data.username
    password = form_data.password
    try:
        # Authenticate with Supabase
        resp = supabase.auth.sign_in_with_password({"email": email, "password": password})
        user = resp.get("user")
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        access_token = create_access_token(
            data={"sub": email}, expires_delta=access_token_expires
        )
        return {"access_token": access_token, "token_type": "bearer"}
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid credentials")

Here’s how this code works. We import the required libraries. The /token endpoint accepts a form_data object (username and password). It then attempts to authenticate the user using the Supabase auth.sign_in_with_password method. If the authentication is successful, the function generates a JWT. If it fails, it returns an HTTP 401 Unauthorized error. This is your key to getting into your app.

Now, let's create a dependency to secure our API endpoints. This dependency will be responsible for validating the JWTs received in the Authorization header of incoming requests. This is where the real security magic happens. This is an important step to safeguard your API. The next step is to create a dependency to secure your API endpoints. It validates the JWTs in the incoming requests' Authorization header.

# Function to get current user
def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(  # noqa: F841
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email: str = payload.get("sub")
        if email is None:
            raise credentials_exception
        token_data = TokenData(email=email)
    except JWTError:
        raise credentials_exception
    return token_data

This function retrieves the token from the Authorization header, decodes it, and validates it. If the token is valid, it returns the user's email. If it’s invalid or expired, it raises an HTTP 401 error. This ensures that only authorized users can access your protected API endpoints. This dependency is the gatekeeper, controlling access to your valuable resources.

Securing Your API Endpoints

Finally, let's secure some API endpoints using the dependency we just created. Here’s how you can do it:

# Protected endpoint
@app.get("/users/me")
async def read_users_me(
    current_user: Annotated[TokenData, Depends(get_current_user)]
):
    return {"email": current_user.email}

In this example, the /users/me endpoint is protected. The get_current_user dependency is used to validate the JWT. If the token is valid, the endpoint returns the user's email. If the token is invalid or missing, the user will receive an HTTP 401 Unauthorized error. This protects your user data. The dependency get_current_user ensures that only authenticated users can access the endpoint. This is the simplest level of protection. You can add more complex logic, like role-based access control, within your dependency to tailor your access controls.

You can extend this approach to protect other endpoints and implement more complex authorization logic. For example, you can add role-based access control by checking the user's role in the database before granting access to a resource. This is a very common pattern and is the foundation of more complex security. You can customize the get_current_user function to include more advanced checks, such as verifying user roles and permissions. This flexibility is what makes FastAPI and Supabase such powerful tools.

Running Your Application

To run your FastAPI application, use the following command in your terminal:

uvicorn main:app --reload

This command starts the Uvicorn server, which serves your FastAPI application. The --reload flag enables automatic code reloading, so your app will automatically restart whenever you make changes to your code. Then, open your browser and go to http://127.0.0.1:8000/docs to see the automatically generated API documentation (Swagger UI). You can use this documentation to test your API endpoints.

To test the authentication flow, first, obtain a JWT by sending a POST request to the /token endpoint with your email and password. Then, use the JWT in the Authorization header (e.g., Bearer <your_jwt>) of subsequent requests to protected endpoints like /users/me.

Deployment Considerations: Docker and Beyond

For production deployments, consider using Docker to containerize your application. This simplifies deployment and ensures consistency across different environments. Create a Dockerfile in your project directory with the following contents:

FROM python:3.11-slim-buster

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

This Dockerfile defines the steps to build a Docker image for your FastAPI application. It specifies the base Python image, sets the working directory, copies the requirements.txt file (which lists your project dependencies), installs the dependencies, copies your application code, and finally, runs the Uvicorn server.

Build the Docker image using the following command:

docker build -t fastapi-supabase-jwt .

Run the Docker container:

docker run -p 8000:8000 fastapi-supabase-jwt

This command builds a Docker image for your application and runs it, exposing port 8000. You can now access your API at http://localhost:8000. Deploying your application with Docker is a breeze! You can also deploy your FastAPI application to cloud platforms like AWS, Google Cloud, or Azure. These platforms offer various services for container orchestration and management, making deployment even easier.

Advanced Topics and Best Practices

  • Role-Based Access Control (RBAC): Implement RBAC to manage user roles and permissions within your application. Store user roles in the database and use them to control access to specific API endpoints. This is how you really build out your app security.
  • Rate Limiting: Implement rate limiting to protect your API from abuse. Use libraries like fastapi-limiter to limit the number of requests a user can make within a certain time frame. Don't let your API get hammered!
  • Input Validation: Always validate user input to prevent security vulnerabilities. Use FastAPI's built-in data validation capabilities to ensure that the data received by your API is in the expected format. Always validate your data!
  • Error Handling: Implement comprehensive error handling to provide informative error messages to the client. This includes handling exceptions gracefully and returning appropriate HTTP status codes. Provide good error messages.
  • Testing: Write unit tests and integration tests to ensure that your API behaves as expected. Test your code.
  • Environment Variables: Use environment variables to store sensitive information, such as API keys and database credentials. Never hardcode these values in your code. Always protect your keys.

Conclusion: Your Secure API Journey Begins!

That's it, guys! You've learned how to build a secure API with FastAPI, Supabase, and JWT. We've covered the setup, authentication, and authorization processes. You now have a solid foundation for building secure and scalable APIs. Remember to keep your secrets safe, always validate your inputs, and stay updated with the latest security best practices. Keep coding, keep building, and keep your APIs secure! Good luck and happy coding!