Introduction
Authentication is a core part of almost every web application. It allows users to create accounts, log in, and access protected resources.
In this tutorial, you will learn how to implement authentication in FastAPI using JSON Web Tokens (JWT).
We will build a complete authentication system that includes:
- User Signup
- User Login
- Password hashing for security
- JWT token generation
- Protected routes
We will use:
- FastAPI for the backend
- SQLAlchemy for database operations
- PostgreSQL as a database
- Passlib for password hashing
- Python-JOSE for JWT token
By the end of this tutorial, you will understand how authentication works in real-world backend systems.
Table of Contents
- Prerequisites
- Project Overview
- Setting Up the Project
- Database Setup (User Model)
- Creating Pydantic Schemas
- Password Hashing with Passlib
- JWT Authentication Setup
- User Signup API
- User Login API
- Creating Protected Route
- Testing the Authentication System
- Common Errors and Fixes
- Conclusion
Prerequisites
Before starting this tutorial, make sure you have the following:
- Python 3.9 or later
- Basic knowledge of Python
- Basic understanding of APIs
- PostgreSQL installed and running
- A code editor such as VS Code
It is also recommended that you are familiar with:
- FastAPI basics
- SQLAlchemy ORM
If you are new to FastAPI with SQLAlchemy, you can read this complete guide:
Build a CRUD App with FastAPI, SQLAlchemy and PostgreSQL.
Project Overview
In this tutorial, we will build a simple authentication system using FastAPI.
The system will allow user to:
- Create a new account (Signup)
- Login using email and password
- Receives a JWT token after login
- Access protected routes using the token
How the System Works
Let’s understand the project flow step by step.
- The user sends a signup request with email and password.
- The password is hashed and stored in the database.
- The user logs in with their credentials.
- If the credentials are correct, the server generates a JWT token.
- The client stores this token.
- For protected routes, the client sends the token in the request.
- The server verifies the token before allowing access.
- After successful verification, the server allows access to the protected route.
Setting Up the Project
In this section, we will set up the FastAPI project and install all required dependencies.
Step 1: Create a Project Folder
Create a new folder for your project.
mkdir fastapi-auth
cd fastapi-authStep 2: Create a Virtual Environment
Create and activate a virtual environment for the project.
python -m venv venvStep 3: Activate the Virtual Environment
On Windows:
venv\Scripts\activateOn macOS/Linux:
source venv/bin/activateStep 4: Install Required Dependencies
Install all necessary packages using the following command:
pip install fastapi uvicorn sqlalchemy psycopg2-binary passlib[bcrypt] python-joseLet's understand the purpose of each package:
fastapi– Python framework for building APIsuvicorn– An ASGI server to run the FastAPI app.sqlalchemy– ORM for databasepsycopg2-binary– PostgreSQL driver used for PostgreSQL database queries.passlib[bcrypt]– used for password hashing.python-jose– used for JWT token handling.
Step 5: Create Project Structure
Create the folder and files using the following command:
mkdir app
cd app
touch main.py database.py models.py schemas.py crud.py auth.pyLet’s understand the purpose of each file we created.
main.py– main entry point of the appdatabase.py– used for database connectionmodels.py– used for SQAlchemy modelsschemas.py– used for pydantic schemascrud.py– used for CRUD operationsauth.py– used for Authentication logic (JWT, password, hashing)
Step 6: Run the FastAPI Server
Open the main.py file and add the following code:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Auth API is running"}Run the Server:
Open the terminal and enter the following command:
uvicorn app.main:app --reloadOpen your browser:
http://127.0.0.1:8000If everything is working fine, you will see:
{"message": "Auth API is running"}Database Setup (User Model)
In this section, we will connect FastAPI to PostgreSQL using SQLAlchemy and create the User table.
Step 1: Configure Database Connection
Open the database.py file and add the following code for database connection:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql://username:password@localhost/fastapi_auth"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False,
Note: Replace username, password, and fastapi_auth with your PostgreSQL credential and database name. However, If you don’t know how to create a database in PostgreSQL, I recommend you to read our following guide:
How to Setup PostgreSQL Database
Step 2: Create the User Model
Open the models.py file and add the following code:
from sqlalchemy import Column, Integer, String
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True
Step 3: Create Database Tables
Now open the main.py file and add the following code:
from .database import Base, engine
from .models import User
Base.metadata.create_all(bind=engine)Run this once to create the tables in PostgreSQL.
If you want to understand SQLAlchemy models, I recommend you to read the following section of our previous tutorial:
Now that we have created the SQLAlchemy models and built the connections, the next step is to define pydantic schema for request and response validation.
Creating Pydantic Schemas
Pydantic schema defines how data is sent to and from the API.
In our project, we will create two schemas:
UserCreatefor SignupUserResponsefor responses
Add the following piece of code inside the schemas.py file.
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserResponse(BaseModel):
id: int
email: EmailStr
class Config:
orm_mode = TrueLet's understand the purpose of each module and schema.
EmailStrensures the email is valid email format.BaseModelis the base class for pydantic schemas.UserCreateis used to validate signup data from requests.UserResponseis used to validate the response from the API.
Password Hashing with Passlib
Storing plain text passwords in the database is a serious security risk. Instead we use a hashed version of the password.
Hashing converts the password into a fixed string that cannot be reversed.
Step 1: Import Required Modules
Open the auth.py file and import the following module for hashing the password:
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")Step 2: Create Hash Function
Let’s create a function that takes a plain password and returns a hashed password.
def hash_password(password: str) -> str:
return pwd_context.hash(password)Step 3: Create Verify Password Function
Let’s create a function that compares the user password hashed version with the hashed password in the database.
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)Why Hashing a Password is Important
- Protects user data if the database is leaked.
- Passwords cannot be reversed.
- Industry standard security practice.
Now that we have created the functions to hash and verify passwords. The next step is to implement authentication using JWT.
JWT Authentication Setup
In this section, we will set up JWT (JSON Web Token) authentication.
JWT is used to securely send information between the client and the server.
After login, the server generates a token, and the client uses this token to access protected routes.
Let's implement JWT authentication step by step.
Step 1: Add Required Imports
Open auth.py again and add the following code:
from jose import JWTError, jwt
from datetime import datetime, timedeltaStep 2: Define Secret Key and Algorithm
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30Lets understand what we have defined above:
SECRET_KEY– used to sign the token. Always keep it secret and use a strong key.ALGORITHM– method used to encode the token.ACCESS_TOKEN_EXPIRE_MINUTES– token expiration time.
Step 3: Create Token Function
Let’s create a function that takes the user data and creates a token.
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode,
Let’s understand how it works.
- User logs in successfully
- Server creates a token with user data
- Token is sent back to client
- Client stores the token (usually in localStorage or cookies)
- Client sends token in future requests
Step 4: Decode and Verify Token
In order to verify the token we need to decode it first.
Let’s create a function that takes the token and decodes it if the token is valid, otherwise it would return None.
def verify_access_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return NoneUser Signup API
In this section, we will create an API endpoint to register new users.
This endpoint will:
- Accept user email and password
- Hash the password
- Store the user in the database
Step 1: Create CRUD Function
Open crud.py and add the following code:
```python
from sqlalchemy.orm import Session
from . import models, schemas
from .auth import hash_password
from fastapi import HTTPException
def create_user(db: Session, user: schemas.UserCreate):
existing_user = db.query(models.User).filter(models.User.email == user.email).first
Step 2: Create Database Dependency
In database.py add:
from sqlalchemy.orm import Session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()This function creates the database session to perform CRUD operations.
Step 3: Create Signup Route
Open main.py and create the /signup route.
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from . import schemas, crud
from .database import get_db
app = FastAPI()
@app.post("/signup", response_model=schemas.UserResponse)
def signup(
Step 4: Test the Signup API
Open the following URL in the browser:
http://127.0.0.1:8000/docsFind /signup route. Test it with the following data:
{
"email": "test@example.com",
"password": "mypassword123"
}You will get response like:
{
"id": 1,
"email": "test@example.com"
}Now that we have created the /signup route to register users. The next step is to create a /login route to log in the user.
User Login API
In this section, we will create the login endpoint.
This endpoint will:
- Accept email and password
- Verify user credentials
- Generates a JWT token
- Return the token to the user
Step 1: Create Authenticate Function
Open crud.py and add the following code:
from .auth import verify_password
def authenticate_user(db: Session, email: str, password: str):
user = db.query(models.User).filter(models.User.email == email).first()
if not user:
return None
if
Step 2: Create Login Route
Open main.py and add the following code:
from .auth import create_access_token
from pydantic import BaseModel
class LoginRequest(BaseModel):
email: str
password: str
@app.post("/login")
def login(user: LoginRequest, db: Session = Depends(get_db)):
db_user
Step 3: Test the Login API
Open the following URL in the browser:
http://127.0.0.1:8000/docsTest the /login route using the following credentials:
{
"email": "test@example.com",
"password": "mypassword123"
}The API will return the following response:
{
"access_token": "your_jwt_token_here",
"token_type": "bearer"
}Now that our signup and login routes are made, the next step is to make a protected route that requires a valid JWT to access.
Creating Protected Route
In this section, we will create a protected route that only authenticated users can access.
In this router we will:
- Extract the JWT token from the request
- Verify the token
- Get the current user
- Allow access only if the user is valid
Step 1: Import Required Modules
Open auth.py and add the following imports if not existed:
from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from .database import get_db
from . import modelsStep 2: Create OAuth2 Scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")This tells FastAPI that the token will come from Authorization: Brearer .
In a full-stack application the frontend application sends it in the request header.
Step 3: Create a Function to Get Current User
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
):
payload = verify_access_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid or expired token"
Step 4: Create a Protected Route
Now open main.py and add the following code:
from .auth import get_current_user
@app.get("/profile")
def get_profile(current_user = Depends(get_current_user)):
return {
"message": "Access granted",
"user_id": current_user.id,
"email": current_user.email
}Step 5: Test the Protected Route
Open the following URL to test the protected route:
http://127.0.0.1:8000/docs- First, login using
/loginand copy the token - Go to
/profilein Swagger UI - Click
Authorize - Enter:
Bearer your_token_here- Send request
The API will return the following response:
{
"message": "Access granted",
"user_id": 1,
"email": "test@example.com"
}If you don't send the token, the API will return the response like:
{
"detail": "Not authenticated"
}Testing the Authentication System
Now that our project is ready, we need to test it fully step by step to make sure it is working.
Step 1: Run the Server
Run the server if it is running already.
uvicorn app.main:app --reloadOnce the server has run, open the following URL in the browser:
http://127.0.0.1:8000/docsThis will open Swagger UI documentation to test the FastAPI endpoints.
Step 2: Test Signup Route
- Open the
/signuproute. - Send a POST request using the following data:
{
"email": "user@example.com",
"password": "mypassword123"
}You should receive a similar response.
{
"id": 1,
"email": "user@example.com"
}Step 3: Test Login Route
- Now open the
/loginroute. - Send a POST request using the following data:
{
"email": "user@example.com",
"password": "mypassword123"
}You should receive a JWT token in the response.
{
"access_token": "eyJ0eXAiOiJKV1QiLCJh...",
"token_type": "bearer"
}Step 4: Test the Protected Route
- Open the
/profileendpoint. - Click Authorize in Swagger UI.
- Paste the JWT token.
- Send a GET request.
You should see the following response:
{
"message": "Access granted",
"user_id": 1,
"email": "user@example.com"
}Step 5: Test Without Token
Now try /profile without providing the JWT token.
You should get the following response:
{
"detail": "Not authenticated"
}It means, no one can access the protected route without a valid JWT. Your authentication is now fully functional. However there are some cases where you can get errors, so our next section covers some common errors and fixes.
Common Errors and Fixes
Even with a correct setup, small mistakes can break your authentication system.
Here are the most frequent issues:
1. Database Connection Error
Error:
could not connect to server: Connection refused Fix:
- Check PostgreSQL is running.
- Ensure
username,password, and database name inDATABASE_URLare correct.
2. Duplicate Email on Signup
Error:
IntegrityError: duplicate key value violates unique constraintFix:
- Check if the user already exists.
- Make sure you add the validation in
crud.py.
existing_user = db.query(models.User).filter(models.User.email == user.email).first()
if existing_user:
raise HTTPException(status_code=400, detail="User already exists")3. JWT Token Expired
Error:
JWTError: Signature has expiredFix
- Token expires after
ACCESS_TOKEN_EXPIRE_MINUTES - For testing, increase the expiration time temporarily.
ACCESS_TOKEN_EXPIRE_MINUTES = 60This would set the token expiration time to 60 minutes.
4. Invalid Credentials on Login
Error:
While testing the /login endpoint you could get the following error:
Invalid credentialsFix:
- Make sure you are sending correct email and password
- Ensure password is hashed properly
- Use verify_password() function to compare
5. Protected Route Returns “Not authenticated”
Error:
While testing the /profile endpoint you could get the following error:
{"detail": "Not authenticated"}Fix:
Make sure to send token in Authorization header:
Authorization: Bearer <your_token>- Check token is not expired
- Make sure oauth2_scheme is correctly set
Conclusion
Congratulationals! You have successfully built a full authentication system with FastAPI, SQLAlchemy and PostgreSQL.
In this tutorial we have learned:
- Database Setup – Connected FastAPI with PostgreSQL and created a
Usertable. - Pydantic Schemas – Validated user input and output.
- Password Hashing – Secured user password with Passlib.
- JWT Authentication – Generated and verified token for secure access.
- User Signup and Login – Created endpoints to register and authenticate users.
- Protected Routes – Restricted access using JWT token.
- Testing and Troubleshooting – Ensured everything works and fixed common errors.
Next Steps
- Implement role-based access control such as admin, user, etc.
- Connect this backend to Next.js for a full-stack application.
- Deploy the system to a cloud server like VPS, Heroku.
Key Takeaways
- Never store plain text passwords.
- JWT token makes API stateless and secure.
- Always validate input with Pydantic.
- Test endpoints separately to catch errors early.
Now your backend is production-ready for authentication and you can confidently integrate it with a frontend like Next.js. For this purpose, I recommend you to read our tutorial on:
How to Connect FastAPI backend with Next.js Frontend Step by Step