When you build an API, securing it is not an optional step; it's a critical responsibility. Vulnerabilities in authentication are consistently listed as a top security risk by industry standards like the OWASP Top 10, where "Identification and Authentication Failures" can lead to catastrophic data breaches.
The consequences of getting this wrong are severe. In 2022, the Australian telecommunications company Optus suffered a massive data breach where an unauthenticated API endpoint allowed attackers to access the personal data of nearly 10 million customers. Similarly, in 2021, a flaw in Peloton's API allowed unauthenticated requests to access private user account data, including location, gender, and age. These incidents highlight how a single insecure API can expose sensitive information on a massive scale. Proper authentication ensures that only legitimate users can perform actions, protecting both your application and your users' data from such disasters.
In this guide, we'll walk through how to implement authentication in FastAPI correctly. We will start with the simplest method, HTTP Basic Auth, to understand the core concepts. Then, we will move on to building a secure, production-ready system using the industry-standard OAuth2 protocol with JWT tokens for handling user sessions.
HTTP Basic Auth is the most straightforward way to protect an endpoint. The browser sends a username and password with every request.
FastAPI makes this easy to implement.
from typing import Annotated from fastapi import Depends, FastAPI from fastapi.security import HTTPBasic, HTTPBasicCredentials app = FastAPI() security = HTTPBasic() @app.get("/users/me") def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): return {"username": credentials.username, "password": credentials.password}
Here's what's happening:
security
object using HTTPBasic()
.Depends(security)
in our endpoint. FastAPI uses this to automatically handle the authentication flow.credentials
object will contain the username
and password
.But, Basic Auth has significant drawbacks. It sends the username and password in plain text (Base64 encoded, which is easily reversed) with every single request. This is insecure, especially over an unencrypted connection. For modern applications, we need something better.
OAuth2 is the industry-standard protocol for authorization. We'll use one of its common flows, the "Password Flow," where a user exchanges a username and password for an access token. This token is then used for all subsequent requests.
The token itself will be a JSON Web Token (JWT).
A JWT is a compact, URL-safe string that contains JSON data. It looks like a random jumble of characters, but it's composed of three parts: a header, a payload, and a signature.
Before we can authenticate a user, we need to store their password securely. You should never store passwords in plain text. If your database is ever compromised, attackers would have access to every user's password.
Instead, we store a "hash" of the password. A hash is a one-way conversion of the password into a gibberish-looking string. You can't reverse the hash to get the original password, but you can check if a given password matches a stored hash. For best practices, see the OWASP Password Storage Cheat Sheet.
Following FastAPI's latest recommendations, we'll use the pwdlib
library with the Argon2 algorithm. Argon2 is a modern, secure hashing algorithm designed to be resistant to GPU cracking attacks.
First, install pwdlib
with Argon2 support:
pip install "pwdlib[argon2]"
Here's how to create and verify password hashes:
# In a file like auth_security.py from pwdlib import PasswordHash # Create a PasswordHash instance with recommended settings (uses Argon2) password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """Checks if a plain password matches a hashed password.""" return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Hashes a plain password.""" return password_hash.hash(password)
Our authentication functions will need a database model to represent a user. This model will store the user's email, the hashed password, and an is_active
flag.
Here is what a simple User
model using SQLModel looks like.
from typing import ClassVar, Optional from sqlmodel import Field, SQLModel # Assuming TimestampMixin is defined as in the mixins article from .mixins import TimestampMixin class User(TimestampMixin, SQLModel, table=True): """Represents a user account in the system.""" __tablename__: ClassVar[str] = "user" id: Optional[int] = Field(default=None, primary_key=True, index=True) email: str = Field(unique=True, index=True, nullable=False) hashed_password: str = Field(nullable=False) is_active: bool = Field(default=True)
Notice that this model uses a TimestampMixin
. This is a reusable class that adds created_at
and updated_at
fields to our models, keeping our code clean and DRY. You can learn how to create this in my guide on Reusable Model Fields in SQLModel with Mixins.
Now let's put all the pieces together. The following code, which uses the python-jose
library for JWT operations, sets up the functions needed to create tokens and protect our endpoints.
# In a file like auth.py from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from sqlmodel import Session from app.backend.database.connection import get_db from app.backend.database.models import User from app.backend.database.utils.user_crud import get_user_by_email from app.backend.schemas.authentication.models import TokenData from app.common.utils.auth.auth_security import verify_password from app.config.main import get_settings # Load settings from our centralized config settings = get_settings() SECRET_KEY = settings.SECRET_KEY ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # This tells FastAPI where the client should go to get a token oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token") def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): """Creates a JWT access token.""" to_encode = data.copy() now = datetime.now(timezone.utc) if expires_delta: expire = now + expires_delta else: expire = now + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire, "nbf": now}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def authenticate_user(db: Session, email: str, password: str): """Authenticates a user by checking their email and password.""" user = await get_user_by_email(db, email=email) if not user or not verify_password(password, user.hashed_password): return False return user async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): """Decodes the JWT token to get the current 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]) email: str = payload.get("sub") if email is None: raise credentials_exception token_data = TokenData(email=email) except JWTError: raise credentials_exception user = await get_user_by_email(session=db, email=token_data.email) if user is None: raise credentials_exception return user async def get_current_active_user(current_user: User = Depends(get_current_user)): """Checks if the current user is active.""" if not current_user.is_active: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive User") return current_user
SECRET_KEY
and other settings from a centralized configuration, as described in my guide to Pydantic Settings. The SECRET_KEY
is a long, random string used to sign our JWTs.oauth2_scheme
: This creates an OAuth2PasswordBearer
instance. The tokenUrl
points to the endpoint where clients will send their username and password to get a token.create_access_token
: This function builds the JWT. It takes a dictionary of data, adds an expiration time (exp
), and signs it with our SECRET_KEY
. We use the sub
(subject) claim to store the user's email, which is standard practice.authenticate_user
: This function is used by our token endpoint. It finds a user by email using a CRUD function (get_user_by_email
) and then uses our verify_password
function to check if the provided password is correct. For a full guide on creating these database utility functions, see my article on Connecting FastAPI to a Database with SQLModel.get_current_user
: This is the core dependency for protecting our routes.
It depends on oauth2_scheme
, which tells FastAPI to look for a Bearer
token in the Authorization
header.
It decodes the token using jwt.decode
. If the token is invalid or expired, a JWTError
is raised.
It extracts the user's email from the sub
claim, fetches the user from the database and validates it using the TokenData
Pydantic model. This model ensures the data we extracted from the token has the expected structure and type.
from pydantic import BaseModel class TokenData(BaseModel): email: str
Finally, it fetches the user from the database.
get_current_active_user
: This is a layered dependency. It first calls get_current_user
and then adds another check to ensure the user's account is active.To protect an endpoint, you simply add Depends(get_current_active_user)
to it.
from typing import Annotated @app.get("/users/profile") async def read_users_profile(current_user: Annotated[User, Depends(get_current_active_user)]): return current_user
FastAPI will now handle the entire authentication flow for this endpoint. If a valid token isn't provided, it will automatically return a 401 Unauthorized
error.
Q: What's the difference between authentication and authorization? A: Authentication is about verifying who a user is. Authorization is about determining what an authenticated user is allowed to do. This article focuses on authentication.
Q: How do I add role-based permissions (e.g., admin vs. user)?
A: This is the next step after authentication, known as authorization. To implement it, you would typically add a role
field (e.g., 'admin', 'user') to your User
model. Then, you can create another dependency that checks the role
of the current_user
and raises a 403 Forbidden
error if they don't have the required permissions for a specific endpoint.
Q: Should I store the JWT in a cookie or in Local Storage?
A: Both are common. Storing it in an HttpOnly
cookie can protect against Cross-Site Scripting (XSS) attacks. Storing it in Local Storage is simpler but can be vulnerable to XSS. The choice depends on your security requirements.
Q: How do I handle expired tokens and keep users logged in?
A: A production-ready system uses two types of tokens: a short-lived access token (e.g., 15 minutes) and a long-lived refresh token. When the access token expires, your front-end application sends the refresh token to a special endpoint (e.g., /api/v1/auth/refresh
). This endpoint validates the refresh token and issues a new access token, keeping the user logged in without them needing to re-enter their password.
Q: How should I manage my SECRET_KEY
in production?
A: Your SECRET_KEY
should never be hardcoded in your source code. It must be a long, complex, and randomly generated string. As shown in the article, you should load it from an environment variable using a settings management library like Pydantic's BaseSettings
. This allows you to use different keys for development and production and keeps your secrets out of version control.
Q: Why not just use the user ID in the JWT sub
claim instead of the email?
A: You can use either. Using the email is common because it's guaranteed to be unique. Using the user ID is also perfectly fine and can be slightly more efficient for database lookups if the ID is the primary key.
David Muraya is a Solutions Architect specializing in Python, FastAPI, and Cloud Infrastructure. He is passionate about building scalable, production-ready applications and sharing his knowledge with the developer community. You can connect with him on LinkedIn.
Enjoyed this blog post? Check out these related posts!
Adding Google Authentication to Your FastAPI Application
A guide to adding Google Authentication to your FastAPI app.
Read More...
Secure FastAPI Environment Variables on Cloud Run with Secret Manager
A Step-by-Step Guide to Managing Production Secrets on Google Cloud.
Read More...
6 Essential FastAPI Middlewares for Production-Ready Apps
A guide to the 6 key middlewares for building secure, performant, and resilient FastAPI applications.
Read More...
Building Resilient Task Queues in FastAPI with ARQ Retries
How to Handle Failures and Implement Robust Retry Logic in FastAPI Background Jobs with ARQ.
Read More...
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.