FastAPI is a fast and easy Python framework for building APIs. It’s as fast as Node.js and Go, with built-in support for authentication, type checking, and auto-generated docs.
Authentication verifies who a user is, while authorization controls what they can do. These are essential for protecting user data and preventing unauthorized access.
This guide will show you how to add secure authentication and authorization to your FastAPI app—from basic logins to OAuth2 with JWT.
Prerequisites
Before you begin, make sure you have Python 3.8 or higher and pip
installed. This guide assumes you know basic Python, web development, and REST APIs. Some knowledge of JWT tokens and OAuth2 is helpful, but don’t worry—we’ll explain them as we go.
Getting started with FastAPI authentication
To follow along with this tutorial effectively, you'll create a new FastAPI project from scratch.
Start by setting up your development environment:
mkdir fastapi-auth && cd fastapi-auth
python3 -m venv venv
source venv/bin/activate
Now, install the required dependencies for your authentication system:
pip install "fastapi" "uvicorn" "python-jose[cryptography]" "passlib[bcrypt]" "python-multipart"
Here's what each package does in your authentication system:
fastapi
provides the web framework foundationuvicorn
serves as your ASGI server for running the applicationpython-jose
handles JWT token creation and validationpasslib
withbcrypt
manages secure password hashingpython-multipart
enables form data parsing for login endpoints
Create a new main.py
file in your project directory and add this foundational code:
from fastapi import FastAPI
app = FastAPI(title="Authentication Demo", version="1.0.0")
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI Authentication Demo"}
Start the development server to make sure everything works correctly:
uvicorn main:app --reload
Navigate to http://localhost:8000
in your browser, and you should see the welcome message:
You can also visit http://localhost:8000/docs
to explore FastAPI's automatic interactive API documentation powered by Swagger UI:
Once that’s working, you’re ready to move on to the next step.
Understanding FastAPI's security framework
FastAPI features a built-in security system that adheres to the OpenAPI standard. It makes it easy to add different types of authentication like API keys, HTTP Basic authentication, OAuth2 with JWT tokens, and even custom options.
It uses dependency injection to handle security. This means you can create reusable functions for authentication and apply them to specific endpoints or entire groups of routes. This helps keep your code clean and makes it easier to manage access across your app.
FastAPI also automatically updates your API documentation with the correct security information. This helps others understand how to connect to your API and what kind of authentication is needed.
Implementing basic authentication
To start, we’ll use HTTP Basic authentication. This method isn’t recommended for production due to its security limitations, but it helps learn how FastAPI’s security system works.
Create a new file named auth.py
and add the following code:
import secrets
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
def authenticate_user(credentials: HTTPBasicCredentials = Depends(security)):
# In a real application, you'd verify against a database
correct_username = secrets.compare_digest(credentials.username, "admin")
correct_password = secrets.compare_digest(credentials.password, "secret")
if not (correct_username and correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
Now update your main.py
file to use this authentication:
from fastapi import FastAPI, Depends
from auth import authenticate_user
app = FastAPI(title="Authentication Demo", version="1.0.0")
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI Authentication Demo"}
@app.get("/protected")
async def protected_route(current_user: str = Depends(authenticate_user)):
return {"message": f"Hello {current_user}, this is a protected route!"}
The authenticate_user
function shows several important concepts. You use secrets.compare_digest()
for secure string comparison, which prevents timing attacks by ensuring the comparison takes a constant amount of time regardless of where the strings differ. The HTTPException
with a 401 status code properly signals authentication failure to the client.
When you visit the /protected
endpoint in your browser or API documentation, you'll get prompted to enter credentials. The browser will display a login dialog, and you'll need to enter "admin" as the username and "secret" as the password to access the protected resource:
After entering the correct credentials, you'll be redirected to the protected route and see a message like this:
Now that you’ve seen how basic authentication works, let’s move on to making it more secure.
Password hashing and user management
Production apps should never store passwords in plain text. Instead, you need to hash passwords using secure algorithms. Let's build a better user system with proper password hashing.
First, create a models.py
file to define your user data structures:
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
class UserCreate(BaseModel):
username: str
email: str
full_name: str
password: str
Now, let's start building the authentication system. Replace your auth.py
file with the imports and configuration:
from datetime import datetime, timedelta
from typing import Optional
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from models import User, UserInDB
# Configuration
SECRET_KEY = "your-secret-key-here" # In production, use environment variables
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
This sets up the basic configuration. The CryptContext
handles password hashing using bcrypt, and OAuth2PasswordBearer
prepares us for JWT token authentication (which we'll add next).
Next, add the fake database and password functions:
...
# Fake database - replace with real database in production
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", # secret
"disabled": False,
}
}
def verify_password(plain_password, hashed_password):
"""Verify a password against its hash."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
"""Generate a hash for a password."""
return pwd_context.hash(password)
The fake database contains a user with a pre-hashed password. The hashed password corresponds to the plain text "secret". The two functions handle password hashing and verification using bcrypt.
Finally, add the user lookup and authentication functions:
...
def get_user(db, username: str):
"""Get a user from the database."""
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
"""Check if username and password are correct."""
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
These functions handle user lookup and authentication. get_user
finds a user in the database, and authenticate_user
checks if the provided username and password are correct by verifying the hashed password.
Now your authentication system uses secure password hashing and is ready for the next step where you will implement JWT tokens.
JWT token-based authentication
JSON Web Tokens (JWT) provide a secure way to authenticate users without storing sessions on the server. Each token contains all the user information needed, making your API stateless and scalable.
Let's add JWT token creation to your auth.py
file:
from datetime import datetime, timedelta, timezone
from typing import Optional
from passlib.context import CryptContext
...
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""Create a JWT access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
This function creates a JWT token with an expiration time. The token includes the user data and is signed with your secret key to prevent tampering. We use datetime.now(datetime.timezone.utc)
for proper timezone-aware UTC timestamps.
Next, add the token validation functions:
...
async def get_current_user(token: str = Depends(oauth2_scheme)):
"""Get the current user from the JWT token."""
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
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
"""Get the current active user (not disabled)."""
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
These functions extract the user information from the JWT token and verify it's valid. The get_current_active_user
function adds an extra check to make sure the user account isn't disabled.
Now update your main.py
file to create a login endpoint and use JWT authentication:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from datetime import timedelta
from auth import (
authenticate_user, create_access_token, get_current_active_user,
fake_users_db, ACCESS_TOKEN_EXPIRE_MINUTES
)
from models import User
app = FastAPI(title="Authentication Demo", version="1.0.0")
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI Authentication Demo"}
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""Authenticate user and return access token."""
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)):
"""Get current user information."""
return current_user
@app.get("/protected")
async def protected_route(current_user: User = Depends(get_current_active_user)):
return {"message": f"Hello {current_user.full_name}, this is a protected route!"}
/highlight]
The /token
endpoint accepts username and password, then returns a JWT token. The /users/me
endpoint shows how to get the current user's information using the token.
Now restart your server and visit http://localhost:8000/docs
. You'll see the new authentication system in action:
Click the "Authorize" button, enter "johndoe" as username and "secret" as password, then try accessing the protected routes:
Fill in the login form with your credentials:
You'll see how JWT tokens work seamlessly with FastAPI's automatic documentation. Once authorized, you can access protected routes:
Your API now uses secure JWT tokens instead of basic authentication, making it ready for production use.
Working with a database
While our examples have used in-memory storage, production applications need persistent data storage. Let's integrate with a real database using SQLAlchemy and show you how to implement authentication with proper database operations.
First, install the required database dependencies:
pip install "sqlalchemy" "databases[sqlite]" "alembic"
For this tutorial, we'll use SQLite since it doesn't require additional setup. In production, you'd typically use PostgreSQL or MySQL.
Create a database.py
file for your database configuration:
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Table, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.sql import func
# Database URL - SQLite for development
DATABASE_URL = "sqlite:///./auth.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Association table for many-to-many relationship between users and roles
user_roles = Table(
'user_roles',
Base.metadata,
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))
)
class DBUser(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)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
roles = relationship("DBRole", secondary=user_roles, back_populates="users")
class DBRole(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True)
description = Column(String)
users = relationship("DBUser", secondary=user_roles, back_populates="roles")
# Create tables
Base.metadata.create_all(bind=engine)
def get_db():
"""Dependency to get database session."""
db = SessionLocal()
try:
yield db
finally:
db.close()
This sets up your database schema with proper relationships between users and roles. The get_db
function provides a database session for your endpoints.
Next, create database operations in a crud.py
file:
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from database import DBUser, DBRole
from models import UserCreate
from typing import Optional
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_user_by_id(db: Session, user_id: int):
"""Get user by ID."""
return db.query(DBUser).filter(DBUser.id == user_id).first()
def get_user_by_username(db: Session, username: str):
"""Get user by username."""
return db.query(DBUser).filter(DBUser.username == username).first()
def get_user_by_email(db: Session, email: str):
"""Get user by email."""
return db.query(DBUser).filter(DBUser.email == email).first()
def create_user(db: Session, user: UserCreate):
"""Create a new user."""
hashed_password = pwd_context.hash(user.password)
db_user = DBUser(
username=user.username,
email=user.email,
full_name=user.full_name,
hashed_password=hashed_password
)
# Assign default user role
user_role = db.query(DBRole).filter(DBRole.name == "user").first()
if user_role:
db_user.roles.append(user_role)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
def authenticate_user(db: Session, username: str, password: str):
"""Authenticate user with username and password."""
user = get_user_by_username(db, username)
if not user:
return False
if not pwd_context.verify(password, user.hashed_password):
return False
return user
def create_role(db: Session, name: str, description: str = ""):
"""Create a new role."""
db_role = DBRole(name=name, description=description)
db.add(db_role)
db.commit()
db.refresh(db_role)
return db_role
def get_all_users(db: Session, skip: int = 0, limit: int = 100):
"""Get all users with pagination."""
return db.query(DBUser).offset(skip).limit(limit).all()
These functions handle all your database operations, from creating users to authentication.
Now, update your authentication system to work with the database. Update your auth.py
file. First, remove the old fake database and functions that are no longer needed:
# Remove this entire section:
fake_users_db = {
"johndoe": {
... }
}
def verify_password(plain_password, hashed_password):
...
def get_password_hash(password):
...
def get_user(db, username: str):
...
def authenticate_user(fake_db, username: str, password: str):
...
Then, update the imports and add the new database functions:
from datetime import datetime, timedelta, timezone
from typing import Optional
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from database import get_db, DBUser
from models import User, UserInDB
import crud
# Configuration
SECRET_KEY = "your-secret-key-here" # In production, use environment variables
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def convert_db_user_to_user(db_user: DBUser) -> User:
"""Convert database user to Pydantic user model."""
return User(
username=db_user.username,
email=db_user.email,
full_name=db_user.full_name,
disabled=not db_user.is_active,
roles=[role.name for role in db_user.roles]
)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
"""Create a JWT access token."""
...
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
"""Get the current user from the JWT token."""
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
except JWTError:
raise credentials_exception
db_user = crud.get_user_by_username(db, username=username)
if db_user is None:
raise credentials_exception
return convert_db_user_to_user(db_user)
async def get_current_active_user(current_user: User = Depends(get_current_user)):
"""Get the current active user (not disabled)."""
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
The key changes are using the database session and converting between database models and Pydantic models.
Finally, update your main.py
file to use the database:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from datetime import timedelta
from sqlalchemy.orm import Session
from database import get_db
import crud
from auth import (
create_access_token, get_current_active_user,
ACCESS_TOKEN_EXPIRE_MINUTES
)
from models import User, UserCreate
app = FastAPI(title="FastAPI Authentication with Database", version="1.0.0")
@app.get("/")
async def root():
return {"message": "Welcome to FastAPI Authentication Demo"}
@app.post("/register", response_model=User)
async def register_user(user: UserCreate, db: Session = Depends(get_db)):
"""Register a new user."""
# Check if user already exists
db_user = crud.get_user_by_username(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
new_user = crud.create_user(db=db, user=user)
return User(
username=new_user.username,
email=new_user.email,
full_name=new_user.full_name,
disabled=not new_user.is_active,
roles=[role.name for role in new_user.roles]
)
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
"""Authenticate user and return access token."""
user = crud.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"},
)
....
Now you need to create the initial roles in your database. Create a simple script to set up your database:
from sqlalchemy.orm import Session
from database import SessionLocal, engine, Base
import crud
# Create tables
Base.metadata.create_all(bind=engine)
def init_db():
db = SessionLocal()
# Create roles if they don't exist
if not crud.get_role_by_name(db, "user"):
crud.create_role(db, "user", "Regular user")
if not crud.get_role_by_name(db, "admin"):
crud.create_role(db, "admin", "Administrator")
if not crud.get_role_by_name(db, "moderator"):
crud.create_role(db, "moderator", "Moderator")
db.close()
print("Database initialized successfully!")
if __name__ == "__main__":
init_db()
You'll also need to add the missing function to crud.py
:
...
def get_role_by_name(db: Session, name: str):
"""Get role by name."""
return db.query(DBRole).filter(DBRole.name == name).first()
Run the setup script to initialize your database:
python setup_db.py
Database initialized successfully!
Now restart your server and test the new database-backed authentication system:
uvicorn main:app --reload
Visit http://localhost:8000/docs
to see the new /register
endpoint in your API documentation:
Test the registration by clicking on the /register
endpoint and trying to register a new user:
Fill in the registration form with sample data like:
{
"username": "testuser",
"email": "test@example.com",
"full_name": "Test User",
"password": "testpassword123"
}
After successful registration, you'll see the created user response:
Now test logging in with your newly registered user. Go to the /token
endpoint and use the credentials you just created:
Your authentication system now uses a real database for persistent storage, making it ready for production deployment!
Next steps
You've built a complete authentication system with FastAPI that includes user registration, JWT tokens, and database integration. Your API now has the foundation needed for real-world applications.
To take your authentication further, explore FastAPI's documentation on advanced security topics including OAuth2 scopes, dependency injection patterns, and middleware integration. The database documentation covers more advanced SQLAlchemy patterns and async database operations.
For production deployments, review the deployment guide which covers environment configuration, HTTPS setup, and containerization best practices.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github