FastAPI JWT Auth: A Simple Example
FastAPI JWT Auth: A Simple Example
Hey guys! Today, we’re diving deep into something super cool and incredibly useful for web development: JWT authentication in FastAPI . If you’ve been building APIs, you know how crucial security is. You don’t want just anyone accessing your precious data, right? That’s where authentication comes in, and JSON Web Tokens (JWTs) are a popular and efficient way to handle it. In this article, I’m going to walk you through a straightforward FastAPI JWT auth example , showing you how to implement this robust security measure step-by-step. We’ll cover everything from setting up your FastAPI project to generating and verifying JWTs. So, buckle up and let’s get this security party started!
Table of Contents
Why JWTs for Authentication?
Alright, let’s chat about
why
JWTs are such a big deal in the world of authentication, especially when you’re working with a modern framework like
FastAPI
. Think of a JWT as a small, self-contained package of information that securely transmits data between two parties. In our case, it’s often between a client (like your web browser or mobile app) and your API server (built with FastAPI). The magic of JWTs lies in their structure: they consist of three parts – a header, a payload, and a signature – all Base64 encoded and separated by dots. The header typically defines the type of token (JWT) and the signing algorithm used. The payload is where the actual data lives, often containing user information like their ID, roles, and expiration time (this is called ‘claims’). Crucially, the signature is generated using the header, payload, and a secret key known only to the server. This signature verifies that the token hasn’t been tampered with. So, when a user logs in successfully, your FastAPI application can generate a JWT containing their essential details and send it back to them. From then on, for every subsequent request, the client simply includes this JWT, usually in the
Authorization
header. Your FastAPI application can then easily decode and verify the token’s signature using the secret key. If the signature is valid, the application knows the request is legitimate and the user is authenticated. This stateless nature is a huge advantage. Unlike traditional session-based authentication where the server has to store session data for each active user, JWTs put the necessary information right on the client. This makes your API more scalable and less reliant on server-side memory. Plus, JWTs are designed to be compact, making them efficient for transmission over networks. So, for
FastAPI JWT auth
, you’re not just choosing a method; you’re opting for a secure, scalable, and efficient way to protect your APIs.
Setting Up Your FastAPI Project
Before we can get our hands dirty with the actual FastAPI JWT auth example , we need to set up a clean and functional FastAPI project. If you’re new to FastAPI, don’t worry, it’s super beginner-friendly! First things first, you’ll need Python installed on your machine. Once that’s sorted, let’s create a virtual environment. This is a best practice that keeps your project’s dependencies isolated. Open your terminal or command prompt, navigate to your desired project directory, and run these commands:
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
Now that your virtual environment is active, it’s time to install the necessary libraries. For FastAPI itself, you’ll need
fastapi
and an ASGI server like
uvicorn
to run your application. We’ll also need a library to handle JWTs.
python-jose
is a fantastic choice for this, and
passlib
with
bcrypt
is great for securely hashing passwords. So, let’s install them:
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt]
With our dependencies installed, we can create our main application file, let’s call it
main.py
. Inside
main.py
, we’ll start by importing
FastAPI
and creating an instance of our application:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Welcome to the secure API!"}
To run your FastAPI application, simply use
uvicorn
in your terminal:
uvicorn main:app --reload
This command starts the server, and
--reload
is a lifesaver during development as it automatically restarts the server whenever you make changes to your code. You can then visit
http://127.0.0.1:8000
in your browser, and you should see the “Welcome to the secure API!” message. We’ve successfully set up the foundation for our
FastAPI JWT authentication example
. Next up, we’ll tackle user management and the crucial JWT generation part!
Implementing User Authentication and JWT Generation
Alright, let’s get to the heart of our
FastAPI JWT auth example
: user authentication and the generation of those all-important JWTs. For this, we’ll need a way to represent our users and a mechanism to verify their credentials. We’ll start by defining a simple
User
model and a
TokenData
model for our JWT payload. We’ll also need a
security.py
file to house our JWT-related logic.
First, create a
models.py
file for your user and token data structures:
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
Now, let’s create
security.py
. This file will contain our JWT secret key, the
ALGORITHM
, functions for creating JWTs, and a way to hash and verify passwords. It’s
super
important to keep your secret key safe and not hardcode it directly in production. For now, we’ll use a placeholder. In a real application, load this from environment variables.
from datetime import datetime, timedelta
from typing import Optional
from jose import jwt
from passlib.context import CryptContext
# --- Configuration --- #
SECRET_KEY = "YOUR_SUPER_SECRET_KEY_GOES_HERE" # **NEVER hardcode this in production!**
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# --- Password Hashing --- #
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
# --- JWT Handling --- #
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# --- Dummy User Data (for demonstration) --- #
# In a real app, this would come from a database
fake_users_db = {
"john_doe": {
"username": "john_doe",
"email": "john.doe@example.com",
"full_name": "John Doe",
"hashed_password": get_password_hash("secretpassword"),
"disabled": False
}
}
def authenticate_user(username, password):
user = fake_users_db.get(username)
if not user or not verify_password(password, user["hashed_password"]):
return None
return UserInDB(**user)
Now, let’s go back to
main.py
and add the endpoints for user creation (optional, for setup), login, and token generation. We’ll also need a
OAuth2PasswordBearer
to handle token dependency. Remember to import necessary components from
security.py
and
models.py
.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from datetime import timedelta
from models import User, UserInDB
from security import (authenticate_user, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, fake_users_db, get_password_hash)
app = FastAPI()
# --- OAuth2 Scheme --- #
# This tells FastAPI that we'll be using Bearer tokens for authentication
# and the 'tokenUrl' is the endpoint where clients can get a token.
# Define the URL where clients can obtain an access token
TOKEN_URL = "/token"
# Create an OAuth2PasswordBearer instance
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=TOKEN_URL)
# --- User Models for Request/Response --- #
class TokenData(BaseModel):
username: str | None = None
class UserCreate(BaseModel):
username: str
email: str | None = None
password: str
# --- Endpoints --- #
@app.post("/token")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"username": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me", response_model=User)
def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
@app.post("/users/")
def create_user(user: UserCreate):
if user.username in fake_users_db:
raise HTTPException(status_code=400, detail="Username already registered")
hashed_password = get_password_hash(user.password)
fake_users_db[user.username] = {
"username": user.username,
"email": user.email,
"full_name": user.full_name,
"hashed_password": hashed_password,
"disabled": False
}
return {"message": f"User {user.username} created successfully"}
# --- Helper function to get the current user --- #
def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("username")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"},
)
except jwt.JWTError:
raise credentials_exception
user = fake_users_db.get(token_data.username)
if user is None:
raise credentials_exception
return UserInDB(**user)
@app.get("/")
def read_root():
return {"message": "Welcome to the secure API!"}
This setup covers user authentication via a login endpoint that returns a JWT, and a protected endpoint (
/users/me
) that requires a valid JWT to access. The
get_current_user
dependency handles the token verification. Pretty neat, right? We’ve built the core of our
FastAPI JWT authentication example
!
Protecting Your API Endpoints
Now that we’ve got our user authentication and JWT generation sorted, the next logical step in our
FastAPI JWT auth example
is to learn how to
protect
specific API endpoints. This means ensuring that only authenticated users, who can provide a valid JWT, can access certain routes. FastAPI makes this incredibly straightforward using its dependency injection system. Remember the
get_current_user
function we created in
security.py
(or our
main.py
for simplicity)? That’s our key!
Let’s say you have a sensitive data endpoint, perhaps one that retrieves user profiles or modifies database entries. You wouldn’t want just anyone hitting that, obviously. So, you’ll add
Depends(get_current_user)
to the endpoint’s function signature. This tells FastAPI: “Before you execute this endpoint’s logic, first run the
get_current_user
function. If
get_current_user
returns successfully (meaning a valid user was found and the token is good), then proceed with the endpoint’s logic, passing the
current_user
object. If
get_current_user
raises an
HTTPException
(like
401 Unauthorized
due to an invalid or missing token), then stop right there and return that exception to the client.”
Let’s add a simple example of a protected endpoint. Suppose we have an endpoint to get some