Creating a Secure FastAPI Application with JWT Authentication
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. One of its prominent features is the ability to implement security protocols, such as JWT (JSON Web Tokens) authentication, which is essential for protecting your application and managing user sessions effectively. In this article, we will delve into how to create a secure FastAPI application using JWT authentication, providing clear code examples, step-by-step instructions, and actionable insights.
What is JWT Authentication?
JWT, or JSON Web Token, is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Why Use JWT?
- Stateless Authentication: JWT allows for stateless authentication, meaning that the server does not need to store session information.
- Compact: The compact size of JWT makes it easy to transfer over the URL, POST parameters, or inside an HTTP header.
- Cross-Domain: JWT can be used across different domains, making them ideal for microservices architectures.
Setting Up Your FastAPI Application
To get started, we need to set up a basic FastAPI application. If you haven't installed FastAPI and its dependencies yet, you can do so using pip:
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt]
uvicorn
is an ASGI server for running FastAPI applications.python-jose
is used for creating and verifying JWT tokens.passlib
is a password hashing library to securely manage user passwords.
Basic FastAPI Application Structure
Create a new directory for your application and add a file named main.py
. Below is the structure of your FastAPI application:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
# Constants
SECRET_KEY = "your_secret_key" # Change this to a strong secret key
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# FastAPI instance
app = FastAPI()
# Models
class User(BaseModel):
username: str
email: str
full_name: str = None
disabled: bool = None
class UserInDB(User):
hashed_password: str
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 Password Bearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# User database (for demonstration purposes)
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": pwd_context.hash("secret"),
"disabled": False,
}
}
Implementing JWT Authentication
Now that we have the basic structure in place, let's implement JWT authentication.
Step 1: Create a Token
We need a function to create JWT tokens:
def create_access_token(data: dict, expires_delta: 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
Step 2: Authenticate Users
Next, we’ll create a function to verify user credentials:
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(db, username: str, password: str):
user = get_user(db, username)
if not user or not verify_password(password, user.hashed_password):
return False
return user
Step 3: Create the Token Endpoint
Now, we can set up the /token
endpoint to authenticate users and return a JWT:
@app.post("/token", response_model=Token)
async def login(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"}
Step 4: Protecting Routes
To protect specific routes, we can create a dependency that will require a valid token:
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
Secure a Protected Route
Now, let’s create a protected route that only authenticated users can access:
@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
Running the Application
You can run your FastAPI application using Uvicorn:
uvicorn main:app --reload
Conclusion
In this article, we covered how to create a secure FastAPI application using JWT authentication. By implementing JWT, you can ensure that your application is protected against unauthorized access, providing a better user experience. With the provided code snippets, you can easily set up your own FastAPI application with secure user authentication.
Feel free to expand upon this foundation by integrating a real database, improving error handling, and optimizing your security measures. Happy coding!