Skip to content

Latest commit

 

History

History
961 lines (731 loc) · 24 KB

File metadata and controls

961 lines (731 loc) · 24 KB

<< Back


FastAPI JWT Authentication - Complete Tutorial

A comprehensive guide to building a production-ready JWT authentication system with FastAPI, following industry best practices.

Table of Contents


Project Overview

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

Prerequisites

  • Python 3.10 or higher
  • Basic understanding of Python and REST APIs
  • uv package 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"

Step 1: Project Setup

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\activate

Step 2: Install Dependencies

Install all required packages:

uv add fastapi uvicorn python-jose[cryptography] bcrypt python-multipart python-dotenv pydantic[email] pydantic-settings

Package Breakdown:

  • fastapi - Modern web framework for building APIs
  • uvicorn - ASGI server for running FastAPI
  • python-jose[cryptography] - JWT token creation and validation
  • bcrypt - Secure password hashing
  • python-multipart - Form data parsing support
  • python-dotenv - Environment variable management
  • pydantic[email] - Data validation with email support
  • pydantic-settings - Settings management with Pydantic v2

Step 3: Project Structure

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

Step 4: Environment Configuration

Create .gitignore

See file: .gitignore

Content includes:

  • Environment variables (.env)
  • Virtual environment (.venv/)
  • Python cache files
  • IDE files
  • Testing artifacts
  • Distribution files

Generate Secret Key

Generate a secure secret key using OpenSSL:

openssl rand -hex 32

Copy the output for the next step.

Create .env

See file: .env

Configuration includes:

  • SECRET_KEY - Your generated secret key (REQUIRED)
  • ALGORITHM - JWT signing algorithm (HS256)
  • ACCESS_TOKEN_EXPIRE_MINUTES - Token expiration time
  • APP_NAME - Application name
  • APP_VERSION - Application version
  • DEBUG - Debug mode toggle
  • BACKEND_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"]

Step 5: Core Configuration

Settings Management

See file: core/config.py

This module provides:

  • Settings class using Pydantic BaseSettings
  • Automatic environment variable loading from .env
  • Type validation for all configuration values
  • Centralized configuration management
  • Single settings instance exported

Key Features:

  • Loads from environment variables and .env file
  • Validates data types automatically
  • Provides default values where appropriate
  • Case-sensitive environment variable names

Core Package Init

See file: core/__init__.py

Purpose:

  • Exports the settings instance for easy importing throughout the app
  • Usage: from core import settings

Step 6: Security Utilities

Password and JWT Functions

See file: core/security.py

This module provides:

  1. verify_password(plain_password, hashed_password)

    • Verifies a plain password against a bcrypt hash
    • Returns True if password matches
  2. get_password_hash(password)

    • Hashes a password using bcrypt
    • Handles bcrypt's 72-byte limitation
    • Returns hashed password as string
  3. create_access_token(data, expires_delta)

    • Creates a JWT access token
    • Encodes user data and expiration time
    • Signs with SECRET_KEY
  4. 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

Step 7: Database Layer

In-Memory Database (for learning)

See file: database/fake_db.py

This module provides:

  1. get_user(username)

    • Retrieves user by username
    • Returns user dict or None
  2. create_user(username, user_data)

    • Creates a new user
    • Returns created user data
  3. 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.

Database Package Init

See file: database/__init__.py


Step 8: Authentication Models

Pydantic Schemas

See file: auth/models.py

This module defines:

  1. UserCreate - User registration schema

    • Username validation (3-50 chars, alphanumeric)
    • Email validation
    • Password validation (min 8 chars, complexity requirements)
  2. UserResponse - User response schema (no sensitive data)

    • Safe to return to client
    • Excludes password
  3. Token - Token response schema

    • Access token
    • Token type
  4. TokenData - Token payload schema

    • Data extracted from JWT
  5. 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

Step 9: Authentication Dependencies

JWT Validation Dependencies

See file: auth/dependencies.py

This module provides:

  1. oauth2_scheme

    • OAuth2 password bearer scheme
    • Tells FastAPI where to find the token
  2. get_current_user(token)

    • FastAPI dependency for protected routes
    • Extracts and validates JWT token
    • Returns authenticated user
    • Raises 401 if invalid
  3. 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}

Auth Package Init

See file: auth/__init__.py


Step 10: Authentication Routes

API Endpoints

See file: auth/routes.py

This module provides three endpoints:

  1. 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)
  2. POST /auth/login

    • Login with username and password
    • Validates credentials
    • Generates JWT access token
    • Returns token (30 min expiration by default)
  3. 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

Step 11: Main Application

FastAPI Application Setup

See file: main.py

This module includes:

  1. FastAPI App Configuration

    • App title, version, description
    • Automatic API documentation (Swagger & ReDoc)
  2. CORS Middleware

    • Allows frontend integration
    • Configurable origins from settings
  3. Route Inclusions

    • Includes authentication router
  4. Endpoints:

    • GET / - Welcome/info endpoint
    • GET /protected - Example protected route
    • GET /health - Health check endpoint

Features:

  • Production-ready structure
  • CORS support for frontend
  • Health check for monitoring
  • Comprehensive documentation
  • Example protected route

Step 12: Running the Application

Start the Server

# Make sure your virtual environment is activated
# Then run:
uvicorn main:app --reload

# Or simply:
python main.py

The server will start at http://localhost:8000

Access Interactive Documentation

FastAPI automatically generates interactive API documentation:

Swagger UI Features:

  • Try out all endpoints interactively
  • See request/response schemas
  • Test authentication flow
  • Built-in authorization support

Step 13: Testing the API

Using Swagger UI (Recommended for Beginners)

  1. Open Documentation

  2. 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
  3. Login

    • Click on POST /auth/login
    • Click "Try it out"
    • Enter credentials:
      • username: testuser
      • password: SecurePass123
    • Click "Execute"
    • Copy the access_token from response
  4. Authorize

    • Click the "Authorize" button (top right)
    • Enter: Bearer <your_access_token>
    • Click "Authorize"
    • Click "Close"
  5. Test Protected Routes

    • Try GET /auth/me - Returns your profile
    • Try GET /protected - Returns protected message
    • Both should work with valid token

Using cURL

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

Using Python Requests

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())

Testing Password Validation

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"}

Key Concepts Explained

1. JWT (JSON Web Token)

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

2. Password Hashing with Bcrypt

Why Bcrypt?

  • Industry standard for password hashing
  • Designed to be slow (resistant to brute-force)
  • Automatically handles salting
  • Adjustable work factor

How it works:

  1. Generate random salt
  2. Hash password with salt
  3. Store hash (never store plain password)
  4. 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

3. OAuth2 Password Flow

The Flow:

  1. Client sends credentials to /auth/login
  2. Server validates credentials
  3. Server generates JWT access token
  4. Client stores token
  5. Client sends token in Authorization: Bearer <token> header
  6. Server validates token on protected routes
  7. Server returns requested data

Authorization Header Format:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

4. Dependency Injection in FastAPI

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

5. Pydantic Models

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

Best Practices Implemented

Project Structure

Separation of Concerns - Clear module boundaries (core, auth, database)
Package Organization - Logical grouping of related functionality
Single Responsibility - Each module has one clear purpose

Configuration

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

Security

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

Code Quality

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

API Design

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

Development

Virtual Environment - Isolated dependencies
Package Manager - uv for fast, reliable installs
Hot Reload - Development server auto-restarts
Git Ignore - Proper .gitignore configuration


Project File Reference

Core Files

  • main.py - Application entry point
  • .env - Environment variables (never commit!)
  • .gitignore - Git ignore rules

Core Module

Auth Module

Database Module


Next Steps

Level 1: Enhanced Security 🔒

  • 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

Level 2: Real Database 🗄️

  • 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

Level 3: Advanced Features 🚀

  • 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

Level 4: Production Ready 🏭

  • 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

Troubleshooting

Common Issues and Solutions

Installation Issues

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

Authentication Issues

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)

Configuration Issues

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

Runtime Issues

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"]

Resources

Official Documentation

Tools

Congratulations! 🎉

You've built a production-ready JWT authentication system with FastAPI following industry best practices!

Happy coding! 💻