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.
About the Author
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.
Related Blog Posts
Enjoyed this blog post? Check out these related posts!

How to Protect Your FastAPI OpenAPI/Swagger Docs with Authentication
A Guide to Securing Your API Documentation with Authentication
Read More...

Adding Google Authentication to Your FastAPI Application
A guide to adding Google Authentication to your FastAPI app.
Read More...

A Practical Guide to FastAPI Security
A Comprehensive Checklist for Production-Ready Security for a FastAPI Application
Read More...

How to Handle File Uploads in FastAPI
A Practical Guide to Streaming and Validating File Uploads
Read More...
Contact Me
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.