FastAPI JWT Authentication
Authentication is a crucial aspect of web applications, ensuring that users are who they claim to be. JSON Web Tokens (JWT) provide a modern approach to authentication that works well with APIs. In this tutorial, we'll explore how to implement JWT authentication in FastAPI applications.
What are JSON Web Tokens (JWT)?
JSON Web Tokens are an open standard (RFC 7519) that defines a compact and self-contained way to securely transmit information between parties as a JSON object. JWTs are often used for authentication and information exchange.
A JWT consists of three parts separated by dots:
- Header - Contains the type of token and the signing algorithm
- Payload - Contains the claims (user data and token metadata)
- Signature - Ensures the token hasn't been altered
Example JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Why Use JWT in FastAPI?
FastAPI works exceptionally well with JWT authentication because:
- Stateless - No need to store session data on the server
- Scalable - Works across multiple servers without shared session stores
- Cross-domain - Can be used across different domains
- Mobile-friendly - Great for APIs that serve mobile applications
Prerequisites
Before we begin, ensure you have:
- Python 3.6+ installed
- FastAPI installed:
pip install fastapi
- Uvicorn installed:
pip install uvicorn[standard]
- PyJWT installed:
pip install python-jose[cryptography]
- Passlib for password hashing:
pip install passlib[bcrypt]
Setting Up JWT Authentication in FastAPI
Let's build a complete JWT authentication system step by step.
Step 1: Project Setup
First, create a new directory and set up the basic project structure:
# main.py
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# Secret key for JWT signing - keep this safe in production!
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
app = FastAPI()
Step 2: Define User Models
Next, we'll create Pydantic models for our user data:
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
Step 3: Set Up Password Hashing
We'll use passlib with bcrypt for secure password hashing:
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
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)
Step 4: User Authentication Functions
Now, let's add functions to simulate a user database and authenticate users:
# This simulates a database. In a real application, you'd use a proper database.
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": get_password_hash("secret"),
"disabled": False,
}
}
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
Step 5: Create JWT Tokens
Let's implement functions to create and verify JWT tokens:
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=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme)):
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("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
Step 6: Create API Endpoints
Finally, let's create our API endpoints:
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(fake_users_db, 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={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
@app.get("/users/me/items/")
async def read_own_items(current_user: User = Depends(get_current_active_user)):
return [{"item_id": "Foo", "owner": current_user.username}]
Step 7: Run the Application
You can run the application using uvicorn:
uvicorn main:app --reload
Testing the JWT Authentication
Let's test our JWT authentication system:
-
Get an access token: Send a POST request to
/token
with username and password- URL:
http://localhost:8000/token
- Form data:
username=johndoe&password=secret
- Output:
json{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
} - URL:
-
Access protected endpoint: Use the token to access the
/users/me/
endpoint- URL:
http://localhost:8000/users/me/
- Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- Output:
json{
"username": "johndoe",
"email": "[email protected]",
"full_name": "John Doe",
"disabled": false
} - URL:
Real-World Implementation Example
Let's expand our example to show how JWT authentication can be used in a real-world scenario with a proper database integration using SQLAlchemy:
Database Setup
First, install SQLAlchemy:
pip install sqlalchemy
Then create a database model:
# database.py
from sqlalchemy import Boolean, Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
# For PostgreSQL: SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class UserDB(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
email = Column(String, unique=True, index=True)
full_name = Column(String)
hashed_password = Column(String)
disabled = Column(Boolean, default=False)
# Create the database tables
Base.metadata.create_all(bind=engine)
Repository Functions
Add functions to interact with the database:
# crud.py
from sqlalchemy.orm import Session
from . import models, schemas
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db: Session, username: str):
return db.query(models.UserDB).filter(models.UserDB.username == username).first()
def create_user(db: Session, user: schemas.UserCreate):
hashed_password = get_password_hash(user.password)
db_user = models.UserDB(
username=user.username,
email=user.email,
full_name=user.full_name,
hashed_password=hashed_password
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
Updated API with Database Integration
# main.py with database integration
from datetime import datetime, timedelta
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from . import crud, models, schemas
from .database import SessionLocal
app = FastAPI()
# Dependency to get the database session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
return crud.create_user(db=db, user=user)
@app.post("/token", response_model=schemas.Token)
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
user = authenticate_user(db, 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={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me/", response_model=schemas.User)
async def read_users_me(current_user: schemas.User = Depends(get_current_active_user)):
return current_user
Security Best Practices for JWT
When working with JWT authentication, follow these best practices:
- Use HTTPS: Always use HTTPS in production to protect tokens in transit
- Store secrets securely: Never hardcode secret keys in your code
- Short expiry times: Set short expiration times for tokens
- Refresh tokens: Implement a refresh token flow for better security
- Validate claims: Always validate token claims (issuer, audience, expiry)
- Secure token storage: Store tokens securely on the client-side (e.g., HttpOnly cookies)
- Token revocation: Implement a mechanism to revoke tokens if needed
Common JWT Issues and Solutions
Issue | Solution |
---|---|
Expired tokens | Implement token refresh mechanism |
Token storage | Use HttpOnly cookies or secure storage |
CSRF attacks | Use CSRF tokens with HttpOnly cookies |
Token size | Keep payload small, store large data in database |
Secret rotation | Plan for periodic secret rotation |
Summary
In this tutorial, we've explored how to implement JWT authentication in FastAPI:
- We set up the project and dependencies
- We created Pydantic models for our data structures
- We implemented password hashing for secure storage
- We created functions to generate and verify JWT tokens
- We built API endpoints for authentication and protected resources
- We expanded the example with database integration
- We reviewed security best practices
JWT authentication provides a secure, stateless way to authenticate users in your FastAPI applications. By following the steps and best practices outlined in this tutorial, you can implement robust authentication for your API.
Additional Resources
- FastAPI Official Security Documentation
- JWT.io - Official JWT website with debugger and libraries
- RFC 7519 - The JWT specification
- OWASP Authentication Cheatsheet
Exercises
- Implement a token refresh endpoint to issue new access tokens
- Add role-based authorization to protect different endpoints
- Create a frontend application that uses JWT authentication with your FastAPI backend
- Implement token blacklisting for logout functionality
- Update the example to use environment variables for secret keys
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)