In the previous two articles, we covered FastAPI basics and asynchronous interfaces. Today, let’s talk about an indispensable topic in web development: authentication.
Imagine you have an API endpoint that can query a user’s personal information or process payments. Without authentication, anyone could access these sensitive interfaces, which is clearly unacceptable. Therefore, we need a mechanism to confirm “who you are” and only allow legitimate users to access specific resources.
Among the many authentication schemes, JWT (JSON Web Token) is widely adopted for its simplicity, statelessness, and cross-origin friendliness. This article will guide you step-by-step on how to integrate JWT authentication into a FastAPI project.
I. What is JWT? How Does it Work?
JWT stands for JSON Web Token. It is essentially an encrypted and signed JSON object used as a credential for authentication between a client and a server.
1. Structure of a JWT
A JWT consists of three parts, separated by dots .:
- Header: Specifies the algorithm used to generate the signature, e.g.,
HS256(HMAC SHA-256). - Payload: Contains claims such as user ID, username, expiration time, etc. This part is public and can be read by anyone.
- Signature: Generated by encrypting the header and payload using the algorithm specified in the header, combined with a server-side secret key.
A complete JWT looks like this:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2. JWT Authentication Workflow
- User Login: The user sends a login request to the server with credentials like username and password.
- Server Verification & Token Generation: The server verifies the credentials. If valid, it creates a JWT and returns it to the client.
- Client Stores Token: The client (e.g., browser, mobile app) receives the token and typically stores it in
localStorage,sessionStorage, or a Cookie. - Client Makes Protected Request: For subsequent requests to protected API endpoints, the client must include this token in the HTTP request header, usually in the
Authorizationheader with the formatBearer <token>. - Server Verifies Token: The server extracts the token from the request header and verifies:
- If the signature is valid (prevents token tampering).
- If the token has expired.
- (Optional) Other claims in the token, such as user role.
- Server Processes Request: If token verification passes, the server considers the request legitimate and processes/returns the requested resource. If verification fails, it returns a
401 Unauthorizederror.
Core Advantage: JWT is stateless. The server does not need to store any user state information in a database or session; it only needs the secret key for verification. This makes it ideal for distributed systems and microservices architectures.
II. Environment Setup
Before coding, we need to install a few essential libraries:
bash
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart
fastapi&uvicorn: Our web framework and server.python-jose[cryptography]: For generating and verifying JWTs.passlib[bcrypt]: For securely hashing passwords, not storing them in plaintext. Never store plaintext passwords in a database!python-multipart: For supporting form data submission (e.g., username and password during login).
III. Hands-on Coding: Building a FastAPI App with JWT Authentication
We’ll build a simple user management system with the following features:
- User registration
- User login (obtain JWT)
- Get current logged-in user information (protected endpoint)
- Get all user information (requires admin privileges)
1. Project Structure
Let’s organize the code a bit more clearly:
text
. ├── main.py # Main application file └── auth.py # Authentication-related utility functions
2. Writing Authentication Utilities (auth.py)
First, let’s put general logic like generating and verifying tokens in the auth.py file.
python
# auth.py
from datetime import datetime, timedelta
from typing import Optional
from jose import jwt
from passlib.context import CryptContext
# 1. Password hashing context, using bcrypt algorithm
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 2. JWT Configuration
SECRET_KEY = "your-secret-key-keep-it-safe-and-dont-share-it-with-anyone" # Very important! In production, never hardcode; use environment variables.
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Token expiration time
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify if a plain password matches a hashed password.
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a plaintext password.
"""
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""
Generate a JWT Access Token.
"""
to_encode = data.copy()
# Set expiration time
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
# Generate Token
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Note: SECRET_KEY is used to sign the JWT. If it is leaked, anyone can forge tokens. In production, you should use a very complex random string and load it via an environment variable (e.g., os.getenv("SECRET_KEY")).
3. Writing the Main Application (main.py)
Now, let’s write the main part of the FastAPI application.
python
# main.py
from datetime import timedelta
from typing import Optional, List
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from pydantic import BaseModel
# Import the authentication utility functions we just wrote
from auth import (
SECRET_KEY,
ALGORITHM,
ACCESS_TOKEN_EXPIRE_MINUTES,
verify_password,
get_password_hash,
create_access_token,
)
# --- Mock Database and Data Models ---
# Pydantic Models: For request/response data validation and serialization
class UserBase(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserCreate(UserBase):
password: str # Password is required when creating a user
class User(UserBase):
id: int
hashed_password: str # The password stored in the "database" is hashed
class Config:
orm_mode = True
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
# Mock Database
fake_users_db = {
"johndoe": {
"id": 1,
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6xW6X9GFgGO0f5s1e4.aK", # Password is "secret"
"disabled": False,
},
"admin": {
"id": 2,
"username": "admin",
"full_name": "Admin User",
"email": "admin@example.com",
"hashed_password": "$2b$12$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6xW6X9GFgGO0f5s1e4.aK", # Password is also "secret"
"disabled": False,
}
}
# --- FastAPI App and Routes ---
app = FastAPI()
# Define OAuth2 Password Bearer scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# --- Core Dependencies ---
def get_user(db, username: str) -> Optional[User]:
"""Retrieve a user from the mock database."""
if username in db:
user_dict = db[username]
return User(**user_dict)
return None
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
"""
Dependency: Get the current logged-in user.
It parses the Token from the request header and verifies its validity.
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
# 1. Decode Token
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
# 2. Get user from database
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)) -> User:
"""
Dependency: Get the current active user.
Adds a check for whether the user is disabled on top of `get_current_user`.
"""
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
async def get_current_admin_user(current_user: User = Depends(get_current_active_user)) -> User:
"""
Dependency: Get the current admin user.
Adds a check for user role on top of `get_current_active_user`.
"""
if current_user.username != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user
# --- Routes ---
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
User login endpoint for obtaining JWT Token.
FastAPI provides `OAuth2PasswordRequestForm` to easily handle standard OAuth2 password login requests.
"""
# 1. Verify user exists
user = get_user(fake_users_db, username=form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# 2. Verify password is correct
if not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
# 3. Create and return Token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
# In the token payload, we typically include a unique identifier like username (sub)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
"""User registration endpoint."""
# Check if username already exists
db_user = get_user(fake_users_db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
# Hash the password before storing in the "database"
hashed_password = get_password_hash(user.password)
fake_users_db[user.username] = {
"id": len(fake_users_db) + 1,
"username": user.username,
"email": user.email,
"full_name": user.full_name,
"hashed_password": hashed_password,
"disabled": False,
}
# Return created user info (excluding plaintext password)
return fake_users_db[user.username]
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
"""
Get personal information of the current logged-in user.
This endpoint is protected by the `get_current_active_user` dependency; a valid Token is required to access.
"""
return current_user
@app.get("/users/", response_model=List[User])
async def read_all_users(current_user: User = Depends(get_current_admin_user)):
"""
Get information of all users.
This endpoint is protected by the `get_current_admin_user` dependency; only admins can access.
"""
return list(fake_users_db.values())
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
IV. Testing Our API
Now, let’s run and test this application.
- Start the server:
python main.py - Test using Swagger UI:
Open your browser and go tohttp://127.0.0.1:8000/docs. You’ll see an interactive API documentation.
a. Register a user: Find the/users/endpoint, click “Try it out”, enter information for a new user, e.g.:json { "username": "newuser", "password": "newpassword" }
Click “Execute”. If successful, you’ll see the returned user info wherehashed_passwordis the encrypted password.
b. User login (obtain Token): Find the/tokenendpoint, click “Try it out”. Enter username and password (e.g.,johndoeandsecret). Click “Execute”. You’ll get a response like:json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer" }
Copy thisaccess_token.
c. Access a protected endpoint: Find the/users/me/endpoint, click “Try it out”. You’ll notice the docs interface prompts you to “Authorize”. Click the “Authorize” button at the top right. In the pop-up dialog, paste theaccess_tokeninto the input field in the formatBearer <access_token>(note the space between Bearer and the token).Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Click “Authorize” and “Close”. Now click “Execute” for/users/me/. You will successfully get the information for userjohndoe.
d. Test permission control:
– Usejohndoe‘s token to access the/users/endpoint. You’ll get a403 Forbiddenerror becausejohndoeis not an admin.
– Useadmin‘s token (password is alsosecret) to access the/users/endpoint. You will successfully get the list of all users.
V. Key Technical Takeaways
- Core Libraries:
python-jose: Responsible for JWT generation (jwt.encode) and verification (jwt.decode).passlib: Responsible for password hashing (pwd_context.hash) and verification (pwd_context.verify). Never store plaintext passwords.
- Key Concepts:
OAuth2PasswordBearer: A security utility provided by FastAPI that automatically extracts and returns the token string from theAuthorization: Bearer <token>request header. We use it to define theoauth2_schemedependency.- Dependency Injection (
Depends): This is a very powerful feature of FastAPI. We encapsulate the authentication logic in functions likeget_current_userand inject them into route handler functions that require authentication viaDepends(). Benefits include:- Code Reuse: Multiple endpoints can share the same authentication logic.
- Separation of Concerns: Route functions focus on business logic; authentication logic is handled by dedicated functions.
- Easy Testing: Dependencies can be easily replaced in tests.
- Authentication Flow:
- User submits credentials via
/tokenendpoint; server verifies and returns JWT. - Client carries the JWT in subsequent requests.
- Protected endpoints verify the JWT’s validity via the dependency
Depends(get_current_user)and extract user information. - Route handler function receives and uses the extracted user object.
- User submits credentials via
- Permission Control:
- More granular permission checks can be built on top of
get_current_user, likeget_current_active_userandget_current_admin_user. - Complex permission logic can be implemented by layering dependencies.
- More granular permission checks can be built on top of
- Security Best Practices:
- Protect your
SECRET_KEY: Use environment variables or a secure key management service. - Set reasonable token expiration time:
ACCESS_TOKEN_EXPIRE_MINUTESshould not be too long to reduce the risk if a token is leaked. - Use HTTPS: In production, always use HTTPS to transmit data, including JWTs, to prevent eavesdropping.
- Consider using Refresh Tokens: For applications requiring long-term login, you can use an Access Token + Refresh Token pattern. The Access Token is short-lived, while the Refresh Token is long-lived. When the Access Token expires, the client can use the Refresh Token to obtain a new Access Token without requiring the user to log in again.
- Protect your
VI. Summary
Through this article, you have learned the complete process of implementing JWT-based authentication in FastAPI. Starting from the basic principles of JWT, we demonstrated with a full code example how to implement user registration, login, and how to use dependency injection to protect endpoints and implement permission control.
JWT is a very flexible and powerful authentication tool. Understanding and mastering it is crucial for building secure modern web applications. We hope this article helps you successfully integrate authentication into your FastAPI projects.