Back to Scaling Python Applications guides

Authentication and Authorization with FastAPI: A Complete Guide

Stanley Ulili
Updated on June 25, 2025

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 foundation
  • uvicorn serves as your ASGI server for running the application
  • python-jose handles JWT token creation and validation
  • passlib with bcrypt manages secure password hashing
  • python-multipart enables form data parsing for login endpoints

Create a new main.py file in your project directory and add this foundational code:

main.py
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:

Screenshot of the web browser

You can also visit http://localhost:8000/docs to explore FastAPI's automatic interactive API documentation powered by Swagger UI:

Screenshot of the FastAPI docs

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:

auth.py
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:

main.py
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:

Screenshot here of the login prompt and response

After entering the correct credentials, you'll be redirected to the protected route and see a message like this:

Successfully accessed the protected route after logging in with valid credentials

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:

models.py
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:

auth.py
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:

auth.py
...

# 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:

auth.py
...

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:

auth.py
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:

auth.py
...

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:

main.py
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:

Screenshot showing the FastAPI docs with the new token endpoint and the authorize button

Click the "Authorize" button, enter "johndoe" as username and "secret" as password, then try accessing the protected routes:

Screenshot showing the authorize button

Fill in the login form with your credentials:

Screenshot of the login form filled

You'll see how JWT tokens work seamlessly with FastAPI's automatic documentation. Once authorized, you can access protected routes:

Screenshot showing successful JWT authentication and accessing 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:

database.py
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:

crud.py
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:

auth.py
# 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:

auth.py
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:

main.py
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:

setup_db.py
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:

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
Output
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:

Screenshot showing the FastAPI docs with the new /register endpoint visible in the list

Test the registration by clicking on the /register endpoint and trying to register a new user:

Screenshot showing the /register endpoint expanded with the request body form

Fill in the registration form with sample data like:

 
{
  "username": "testuser",
  "email": "test@example.com", 
  "full_name": "Test User",
  "password": "testpassword123"
}

Screenshot showing the registration form filled out with sample data

After successful registration, you'll see the created user response:

Screenshot showing successful user registration response

Now test logging in with your newly registered user. Go to the /token endpoint and use the credentials you just created:

Screenshot showing login with the newly registered user credentials

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.

Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github