A comprehensive guide to building a production-ready JWT authentication system with FastAPI, following industry best practices.
- Project Overview
- Prerequisites
- Step 1: Project Setup
- Step 2: Install Dependencies
- Step 3: Project Structure
- Step 4: Environment Configuration
- Step 5: Core Configuration
- Step 6: Security Utilities
- Step 7: Database Layer
- Step 8: Authentication Models
- Step 9: Authentication Dependencies
- Step 10: Authentication Routes
- Step 11: Main Application
- Step 12: Running the Application
- Step 13: Testing the API
- Key Concepts Explained
- Best Practices Implemented
- Next Steps
This tutorial will guide you through building a JWT authentication system with:
- User registration with password validation
- User login with JWT token generation
- Protected routes requiring authentication
- Secure password hashing with bcrypt
- Centralized configuration management
- Production-ready project structure
- Python 3.10 or higher
- Basic understanding of Python and REST APIs
uvpackage manager installed
To install uv:
# On macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"Create and initialize your project:
# Create project directory
mkdir fastapi-jwt-auth
cd fastapi-jwt-auth
# Initialize uv project
uv init
# Create virtual environment
uv venv
# Activate virtual environment
# On macOS/Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activateInstall all required packages:
uv add fastapi uvicorn python-jose[cryptography] bcrypt python-multipart python-dotenv pydantic[email] pydantic-settingsPackage Breakdown:
fastapi- Modern web framework for building APIsuvicorn- ASGI server for running FastAPIpython-jose[cryptography]- JWT token creation and validationbcrypt- Secure password hashingpython-multipart- Form data parsing supportpython-dotenv- Environment variable managementpydantic[email]- Data validation with email supportpydantic-settings- Settings management with Pydantic v2
Create the following directory structure:
fastapi-jwt-auth/
├── .env
├── .gitignore
├── Tutorial.md
├── main.py
├── core/
│ ├── __init__.py
│ ├── config.py
│ └── security.py
├── auth/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── dependencies.py
└── database/
├── __init__.py
└── fake_db.py
Create directories and empty __init__.py files:
# Create directories
mkdir core auth database
# Create __init__.py files
touch core/__init__.py auth/__init__.py database/__init__.py
# Create other files
touch .env .gitignore Tutorial.md main.py
touch core/config.py core/security.py
touch auth/models.py auth/routes.py auth/dependencies.py
touch database/fake_db.pySee file: .gitignore
Content includes:
- Environment variables (.env)
- Virtual environment (.venv/)
- Python cache files
- IDE files
- Testing artifacts
- Distribution files
Generate a secure secret key using OpenSSL:
openssl rand -hex 32Copy the output for the next step.
See file: .env
Configuration includes:
SECRET_KEY- Your generated secret key (REQUIRED)ALGORITHM- JWT signing algorithm (HS256)ACCESS_TOKEN_EXPIRE_MINUTES- Token expiration timeAPP_NAME- Application nameAPP_VERSION- Application versionDEBUG- Debug mode toggleBACKEND_CORS_ORIGINS- Allowed CORS origins
Important: Replace the placeholder SECRET_KEY with your generated key.
Ref:
SECRET_KEY=Your_openssl_secret_key
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=20 # (can be changed as per requirement) token will expire after 20 mins from the time when issued
# Application
APP_NAME=FastAPI JWT Authentication TUTORIAL
APP_VERSION=0.1.0
DEBUG=True # set to False for production
# CORS Origins (comma-separated)
# ports depend on the stack trying to access this API usually, React(3000) and Vue(8080)
BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8080"]
See file: core/config.py
This module provides:
Settingsclass using Pydantic BaseSettings- Automatic environment variable loading from .env
- Type validation for all configuration values
- Centralized configuration management
- Single
settingsinstance exported
Key Features:
- Loads from environment variables and .env file
- Validates data types automatically
- Provides default values where appropriate
- Case-sensitive environment variable names
See file: core/__init__.py
Purpose:
- Exports the
settingsinstance for easy importing throughout the app - Usage:
from core import settings
See file: core/security.py
This module provides:
-
verify_password(plain_password, hashed_password)- Verifies a plain password against a bcrypt hash
- Returns True if password matches
-
get_password_hash(password)- Hashes a password using bcrypt
- Handles bcrypt's 72-byte limitation
- Returns hashed password as string
-
create_access_token(data, expires_delta)- Creates a JWT access token
- Encodes user data and expiration time
- Signs with SECRET_KEY
-
decode_access_token(token)- Decodes and verifies a JWT token
- Validates signature and expiration
- Returns decoded payload
Security Features:
- Uses bcrypt for password hashing (industry standard)
- JWT tokens with expiration
- Configurable signing algorithm
- Proper error handling
See file: database/fake_db.py
This module provides:
-
get_user(username)- Retrieves user by username
- Returns user dict or None
-
create_user(username, user_data)- Creates a new user
- Returns created user data
-
user_exists(username)- Checks if username is taken
- Returns boolean
Note: This is a simple in-memory database for learning purposes. In production, replace with a real database like PostgreSQL, MongoDB, or MySQL.
See file: database/__init__.py
See file: auth/models.py
This module defines:
-
UserCreate- User registration schema- Username validation (3-50 chars, alphanumeric)
- Email validation
- Password validation (min 8 chars, complexity requirements)
-
UserResponse- User response schema (no sensitive data)- Safe to return to client
- Excludes password
-
Token- Token response schema- Access token
- Token type
-
TokenData- Token payload schema- Data extracted from JWT
-
UserInDB- Database user schema- Includes hashed password
- Used internally only
Validation Features:
- Automatic email validation
- Password strength requirements (uppercase, lowercase, digit)
- Username pattern matching
- Field length constraints
See file: auth/dependencies.py
This module provides:
-
oauth2_scheme- OAuth2 password bearer scheme
- Tells FastAPI where to find the token
-
get_current_user(token)- FastAPI dependency for protected routes
- Extracts and validates JWT token
- Returns authenticated user
- Raises 401 if invalid
-
get_current_active_user(current_user)- Additional validation layer
- Can check user status (active/disabled)
- Future enhancement hook
Usage in routes:
@app.get("/protected")
async def protected_route(current_user: UserInDB = Depends(get_current_user)):
return {"user": current_user.username}See file: auth/__init__.py
See file: auth/routes.py
This module provides three endpoints:
-
POST /auth/register- Register a new user
- Validates input (username, email, password)
- Hashes password with bcrypt
- Stores user in database
- Returns user info (without password)
-
POST /auth/login- Login with username and password
- Validates credentials
- Generates JWT access token
- Returns token (30 min expiration by default)
-
GET /auth/me- Get current user profile
- Requires valid JWT token
- Returns authenticated user info
Features:
- Comprehensive error handling
- Proper HTTP status codes
- Detailed API documentation
- Request/response validation
See file: main.py
This module includes:
-
FastAPI App Configuration
- App title, version, description
- Automatic API documentation (Swagger & ReDoc)
-
CORS Middleware
- Allows frontend integration
- Configurable origins from settings
-
Route Inclusions
- Includes authentication router
-
Endpoints:
GET /- Welcome/info endpointGET /protected- Example protected routeGET /health- Health check endpoint
Features:
- Production-ready structure
- CORS support for frontend
- Health check for monitoring
- Comprehensive documentation
- Example protected route
# Make sure your virtual environment is activated
# Then run:
uvicorn main:app --reload
# Or simply:
python main.pyThe server will start at http://localhost:8000
FastAPI automatically generates interactive API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Swagger UI Features:
- Try out all endpoints interactively
- See request/response schemas
- Test authentication flow
- Built-in authorization support
-
Open Documentation
- Navigate to http://localhost:8000/docs
-
Register a New User
- Click on
POST /auth/register - Click "Try it out"
- Enter test data:
{ "username": "testuser", "email": "test@example.com", "password": "SecurePass123" } - Click "Execute"
- Verify 201 response
- Click on
-
Login
- Click on
POST /auth/login - Click "Try it out"
- Enter credentials:
- username:
testuser - password:
SecurePass123
- username:
- Click "Execute"
- Copy the
access_tokenfrom response
- Click on
-
Authorize
- Click the "Authorize" button (top right)
- Enter:
Bearer <your_access_token> - Click "Authorize"
- Click "Close"
-
Test Protected Routes
- Try
GET /auth/me- Returns your profile - Try
GET /protected- Returns protected message - Both should work with valid token
- Try
# Register
curl -X POST "http://localhost:8000/auth/register" \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123"
}'
# Login
curl -X POST "http://localhost:8000/auth/login" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=testuser&password=SecurePass123"
# Save token from response, then:
TOKEN="your-token-here"
# Access protected route
curl -X GET "http://localhost:8000/protected" \
-H "Authorization: Bearer $TOKEN"
# Get current user
curl -X GET "http://localhost:8000/auth/me" \
-H "Authorization: Bearer $TOKEN"import requests
BASE_URL = "http://localhost:8000"
# Register
response = requests.post(
f"{BASE_URL}/auth/register",
json={
"username": "testuser",
"email": "test@example.com",
"password": "SecurePass123"
}
)
print("Register:", response.json())
# Login
response = requests.post(
f"{BASE_URL}/auth/login",
data={
"username": "testuser",
"password": "SecurePass123"
}
)
token_data = response.json()
token = token_data["access_token"]
print("Token:", token)
# Access protected route
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{BASE_URL}/protected", headers=headers)
print("Protected:", response.json())
response = requests.get(f"{BASE_URL}/auth/me", headers=headers)
print("Profile:", response.json())Try registering with invalid passwords to see validation in action:
// Too short
{"username": "user1", "email": "user1@test.com", "password": "Short1"}
// No uppercase
{"username": "user2", "email": "user2@test.com", "password": "nouppercase1"}
// No lowercase
{"username": "user3", "email": "user3@test.com", "password": "NOLOWERCASE1"}
// No digit
{"username": "user4", "email": "user4@test.com", "password": "NoDigitPass"}
// Valid password
{"username": "user5", "email": "user5@test.com", "password": "ValidPass123"}What is JWT?
- Compact, self-contained tokens for secure data transmission
- No server-side session storage needed
- Stateless authentication
Structure: Three parts separated by dots (.)
header.payload.signature
xxxxx.yyyyy.zzzzz
Parts:
- Header: Token type (JWT) and signing algorithm (HS256)
- Payload: Claims (user data, expiration time)
- Signature: Cryptographic signature for verification
Benefits:
- Stateless (scalable)
- Self-contained (all info in token)
- Cross-domain authentication
- Mobile-friendly
Security Considerations:
- Always use HTTPS in production
- Keep tokens short-lived (15-30 minutes)
- Store securely (httpOnly cookies or secure storage)
- Validate signature on every request
Why Bcrypt?
- Industry standard for password hashing
- Designed to be slow (resistant to brute-force)
- Automatically handles salting
- Adjustable work factor
How it works:
- Generate random salt
- Hash password with salt
- Store hash (never store plain password)
- Verification: hash input password with stored salt, compare
Important Note:
- Bcrypt has 72-byte limit (handled in our code)
- Each hash takes ~100-300ms (by design)
- This prevents rapid password guessing
The Flow:
- Client sends credentials to
/auth/login - Server validates credentials
- Server generates JWT access token
- Client stores token
- Client sends token in
Authorization: Bearer <token>header - Server validates token on protected routes
- Server returns requested data
Authorization Header Format:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
What is it?
- Design pattern where dependencies are "injected" into functions
- FastAPI handles the injection automatically
Example:
async def get_current_user(token: str = Depends(oauth2_scheme)):
# Validate token, return user
return user
@app.get("/protected")
async def protected(current_user = Depends(get_current_user)):
# current_user is automatically resolved
return {"user": current_user.username}Benefits:
- Code reusability
- Automatic validation
- Clear dependency tree
- Easy testing (mock dependencies)
- Reduces boilerplate
What is Pydantic?
- Data validation library using Python type hints
- Automatic validation, serialization, documentation
Example:
class UserCreate(BaseModel):
username: str = Field(min_length=3)
email: EmailStr
password: str = Field(min_length=8)Benefits:
- Automatic validation
- Clear error messages
- IDE autocompletion
- Generates API documentation
- Type safety
✅ Separation of Concerns - Clear module boundaries (core, auth, database)
✅ Package Organization - Logical grouping of related functionality
✅ Single Responsibility - Each module has one clear purpose
✅ Centralized Settings - All config in one place (core/config.py)
✅ Environment Variables - Sensitive data in .env file
✅ Type Validation - Pydantic validates all config values
✅ Environment Awareness - Easy dev/staging/prod configurations
✅ Password Hashing - Bcrypt with proper salting
✅ JWT Tokens - Signed, time-limited tokens
✅ Token Validation - Verify signature and expiration
✅ Password Validation - Enforce strong passwords
✅ No Secrets in Code - All sensitive data in environment
✅ Type Hints - Full type annotations throughout
✅ Docstrings - Comprehensive documentation
✅ Error Handling - Proper HTTP status codes
✅ Input Validation - Pydantic models validate all inputs
✅ DRY Principle - No code duplication
✅ RESTful Endpoints - Standard HTTP methods and paths
✅ Consistent Responses - Predictable response formats
✅ Proper Status Codes - 200, 201, 400, 401, etc.
✅ Auto Documentation - Swagger/ReDoc generated
✅ CORS Support - Ready for frontend integration
✅ Virtual Environment - Isolated dependencies
✅ Package Manager - uv for fast, reliable installs
✅ Hot Reload - Development server auto-restarts
✅ Git Ignore - Proper .gitignore configuration
main.py- Application entry point.env- Environment variables (never commit!).gitignore- Git ignore rules
core/__init__.py- Package initializationcore/config.py- Settings managementcore/security.py- Security utilities
auth/__init__.py- Package initializationauth/models.py- Pydantic schemasauth/dependencies.py- Auth dependenciesauth/routes.py- Authentication endpoints
database/__init__.py- Package initializationdatabase/fake_db.py- In-memory database
- Refresh Tokens - Add long-lived refresh tokens
- Access token: 15-30 minutes
- Refresh token: 7-30 days
- Endpoint to refresh access tokens
- Token Blacklist - Implement logout functionality
- Store revoked tokens
- Check blacklist on validation
- Rate Limiting - Prevent brute force attacks
- Limit login attempts per IP
- Account lockout after failures
- HTTPS Only - Enforce secure connections
- SQLAlchemy Integration - ORM for database operations
- Create User model
- Database migrations with Alembic
- Connection pooling
- PostgreSQL Setup - Production database
- Docker container for development
- Connection string in .env
- Async database operations
- Database Migrations - Version control for schema
- Alembic configuration
- Migration scripts
- Rollback support
- Email Verification - Verify user emails
- Send verification email on registration
- Verification token endpoint
- Email service integration (SendGrid, SES)
- Password Reset - Forgot password flow
- Request reset email
- Time-limited reset tokens
- Update password endpoint
- Role-Based Access Control (RBAC) - User permissions
- User roles (admin, user, moderator)
- Permission decorators
- Role-based endpoints
- OAuth2 Social Login - Third-party authentication
- Google OAuth2
- GitHub OAuth2
- Social account linking
- Two-Factor Authentication (2FA) - Extra security layer
- TOTP implementation
- QR code generation
- Backup codes
- Comprehensive Testing - Ensure code quality
- Unit tests with pytest
- Integration tests
- Test coverage reporting
- CI test automation
- Logging & Monitoring - Observability
- Structured logging
- Log aggregation (ELK, CloudWatch)
- Error tracking (Sentry)
- Performance monitoring
- Docker Containerization - Consistent deployments
- Multi-stage Dockerfile
- Docker Compose for local dev
- Container registry
- CI/CD Pipeline - Automated deployments
- GitHub Actions / GitLab CI
- Automated testing
- Deployment automation
- Environment management
- Performance Optimization - Scale efficiently
- Database query optimization
- Caching (Redis)
- Load testing
- CDN for static assets
- Documentation - Comprehensive docs
- API documentation
- Architecture diagrams
- Deployment guide
- Troubleshooting guide
Issue: ModuleNotFoundError: No module named 'pydantic_settings'
Solution: Install pydantic-settings separately
$ uv add pydantic-settings
Issue: Virtual environment not activating
Solution:
# On macOS/Linux
$ source .venv/bin/activate
# On Windows
$ .venv\Scripts\activate
# Or use:
$ uv run python main.py
Issue: 401 Unauthorized on protected routes
Solution: Ensure token is in Authorization header
Header: Authorization: Bearer <your-token-here>
Check token hasn't expired (default 30 minutes)
Issue: ValueError: password cannot be longer than 72 bytes
Solution: Already handled in get_password_hash()
Password is truncated to 72 bytes
Issue: Login returns 401 even with correct credentials
Solution: Check password was hashed during registration
Verify username matches exactly (case-sensitive)
Issue: Environment variables not loading
Solution:
1. Ensure .env file is in project root
2. Check .env formatting (no quotes needed)
3. Restart server after changing .env
4. Verify load_dotenv() is called
Issue: SECRET_KEY validation error
Solution: Ensure SECRET_KEY is set in .env
Generate with: openssl rand -hex 32
Never use default/example keys
Issue: Port 8000 already in use
Solution: Use different port
$ uvicorn main:app --port 8001 --reload
Issue: CORS errors from frontend
Solution: Add frontend URL to BACKEND_CORS_ORIGINS in .env
BACKEND_CORS_ORIGINS=["http://localhost:3000"]
- FastAPI: https://fastapi.tiangolo.com/
- Pydantic: https://docs.pydantic.dev/
- Uvicorn: https://www.uvicorn.org/
- Python-JOSE: https://python-jose.readthedocs.io/
- JWT Debugger: https://jwt.io/ - Decode and verify JWTs
- OpenAPI Editor: https://editor.swagger.io/ - API design
- Postman: https://www.postman.com/ - API testing
- Insomnia: https://insomnia.rest/ - API client
Congratulations! 🎉
You've built a production-ready JWT authentication system with FastAPI following industry best practices!
Happy coding! 💻