Skip to main content

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:

  1. Header - Contains the type of token and the signing algorithm
  2. Payload - Contains the claims (user data and token metadata)
  3. 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:

  1. Python 3.6+ installed
  2. FastAPI installed: pip install fastapi
  3. Uvicorn installed: pip install uvicorn[standard]
  4. PyJWT installed: pip install python-jose[cryptography]
  5. 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:

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

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

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

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

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

python
@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:

bash
uvicorn main:app --reload

Testing the JWT Authentication

Let's test our JWT authentication system:

  1. 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"
    }
  2. 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
    }

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:

bash
pip install sqlalchemy

Then create a database model:

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

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

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

  1. Use HTTPS: Always use HTTPS in production to protect tokens in transit
  2. Store secrets securely: Never hardcode secret keys in your code
  3. Short expiry times: Set short expiration times for tokens
  4. Refresh tokens: Implement a refresh token flow for better security
  5. Validate claims: Always validate token claims (issuer, audience, expiry)
  6. Secure token storage: Store tokens securely on the client-side (e.g., HttpOnly cookies)
  7. Token revocation: Implement a mechanism to revoke tokens if needed

Common JWT Issues and Solutions

IssueSolution
Expired tokensImplement token refresh mechanism
Token storageUse HttpOnly cookies or secure storage
CSRF attacksUse CSRF tokens with HttpOnly cookies
Token sizeKeep payload small, store large data in database
Secret rotationPlan for periodic secret rotation

Summary

In this tutorial, we've explored how to implement JWT authentication in FastAPI:

  1. We set up the project and dependencies
  2. We created Pydantic models for our data structures
  3. We implemented password hashing for secure storage
  4. We created functions to generate and verify JWT tokens
  5. We built API endpoints for authentication and protected resources
  6. We expanded the example with database integration
  7. 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

Exercises

  1. Implement a token refresh endpoint to issue new access tokens
  2. Add role-based authorization to protect different endpoints
  3. Create a frontend application that uses JWT authentication with your FastAPI backend
  4. Implement token blacklisting for logout functionality
  5. 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! :)