FastAPI Authentication in Practice: Securing Your API with JWT

leo 05/12/2025

In the previous two articles, we covered FastAPI basics and asynchronous interfaces. Today, let’s talk about an indispensable topic in web development: authentication.

Imagine you have an API endpoint that can query a user’s personal information or process payments. Without authentication, anyone could access these sensitive interfaces, which is clearly unacceptable. Therefore, we need a mechanism to confirm “who you are” and only allow legitimate users to access specific resources.

Among the many authentication schemes, JWT (JSON Web Token) is widely adopted for its simplicity, statelessness, and cross-origin friendliness. This article will guide you step-by-step on how to integrate JWT authentication into a FastAPI project.

I. What is JWT? How Does it Work?

JWT stands for JSON Web Token. It is essentially an encrypted and signed JSON object used as a credential for authentication between a client and a server.

1. Structure of a JWT

A JWT consists of three parts, separated by dots .:

  • Header: Specifies the algorithm used to generate the signature, e.g., HS256 (HMAC SHA-256).
  • Payload: Contains claims such as user ID, username, expiration time, etc. This part is public and can be read by anyone.
  • Signature: Generated by encrypting the header and payload using the algorithm specified in the header, combined with a server-side secret key.

A complete JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2. JWT Authentication Workflow

  1. User Login: The user sends a login request to the server with credentials like username and password.
  2. Server Verification & Token Generation: The server verifies the credentials. If valid, it creates a JWT and returns it to the client.
  3. Client Stores Token: The client (e.g., browser, mobile app) receives the token and typically stores it in localStoragesessionStorage, or a Cookie.
  4. Client Makes Protected Request: For subsequent requests to protected API endpoints, the client must include this token in the HTTP request header, usually in the Authorization header with the format Bearer <token>.
  5. Server Verifies Token: The server extracts the token from the request header and verifies:
    • If the signature is valid (prevents token tampering).
    • If the token has expired.
    • (Optional) Other claims in the token, such as user role.
  6. Server Processes Request: If token verification passes, the server considers the request legitimate and processes/returns the requested resource. If verification fails, it returns a 401 Unauthorized error.

Core Advantage: JWT is stateless. The server does not need to store any user state information in a database or session; it only needs the secret key for verification. This makes it ideal for distributed systems and microservices architectures.

II. Environment Setup

Before coding, we need to install a few essential libraries:

bash

pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] python-multipart
  • fastapi & uvicorn: Our web framework and server.
  • python-jose[cryptography]: For generating and verifying JWTs.
  • passlib[bcrypt]: For securely hashing passwords, not storing them in plaintext. Never store plaintext passwords in a database!
  • python-multipart: For supporting form data submission (e.g., username and password during login).

III. Hands-on Coding: Building a FastAPI App with JWT Authentication

We’ll build a simple user management system with the following features:

  • User registration
  • User login (obtain JWT)
  • Get current logged-in user information (protected endpoint)
  • Get all user information (requires admin privileges)

1. Project Structure

Let’s organize the code a bit more clearly:

text

.
├── main.py          # Main application file
└── auth.py          # Authentication-related utility functions

2. Writing Authentication Utilities (auth.py)

First, let’s put general logic like generating and verifying tokens in the auth.py file.

python

# auth.py
from datetime import datetime, timedelta
from typing import Optional

from jose import jwt
from passlib.context import CryptContext

# 1. Password hashing context, using bcrypt algorithm
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 2. JWT Configuration
SECRET_KEY = "your-secret-key-keep-it-safe-and-dont-share-it-with-anyone"  # Very important! In production, never hardcode; use environment variables.
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # Token expiration time

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    Verify if a plain password matches a hashed password.
    """
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    """
    Hash a plaintext password.
    """
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    """
    Generate a JWT Access Token.
    """
    to_encode = data.copy()
    
    # Set expiration time
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    
    to_encode.update({"exp": expire})
    
    # Generate Token
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

Note: SECRET_KEY is used to sign the JWT. If it is leaked, anyone can forge tokens. In production, you should use a very complex random string and load it via an environment variable (e.g., os.getenv("SECRET_KEY")).

3. Writing the Main Application (main.py)

Now, let’s write the main part of the FastAPI application.

python

# main.py
from datetime import timedelta
from typing import Optional, List

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from pydantic import BaseModel

# Import the authentication utility functions we just wrote
from auth import (
    SECRET_KEY,
    ALGORITHM,
    ACCESS_TOKEN_EXPIRE_MINUTES,
    verify_password,
    get_password_hash,
    create_access_token,
)

# --- Mock Database and Data Models ---

# Pydantic Models: For request/response data validation and serialization
class UserBase(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None

class UserCreate(UserBase):
    password: str  # Password is required when creating a user

class User(UserBase):
    id: int
    hashed_password: str  # The password stored in the "database" is hashed

    class Config:
        orm_mode = True

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None

# Mock Database
fake_users_db = {
    "johndoe": {
        "id": 1,
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6xW6X9GFgGO0f5s1e4.aK", # Password is "secret"
        "disabled": False,
    },
    "admin": {
        "id": 2,
        "username": "admin",
        "full_name": "Admin User",
        "email": "admin@example.com",
        "hashed_password": "$2b$12$EixZaYb4xU58Gpq1R0yWbeb00LU5qUaK6xW6X9GFgGO0f5s1e4.aK", # Password is also "secret"
        "disabled": False,
    }
}

# --- FastAPI App and Routes ---

app = FastAPI()

# Define OAuth2 Password Bearer scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# --- Core Dependencies ---

def get_user(db, username: str) -> Optional[User]:
    """Retrieve a user from the mock database."""
    if username in db:
        user_dict = db[username]
        return User(**user_dict)
    return None

async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """
    Dependency: Get the current logged-in user.
    It parses the Token from the request header and verifies its validity.
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        # 1. Decode Token
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    
    # 2. Get user from database
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
    """
    Dependency: Get the current active user.
    Adds a check for whether the user is disabled on top of `get_current_user`.
    """
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

async def get_current_admin_user(current_user: User = Depends(get_current_active_user)) -> User:
    """
    Dependency: Get the current admin user.
    Adds a check for user role on top of `get_current_active_user`.
    """
    if current_user.username != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not enough permissions"
        )
    return current_user

# --- Routes ---

@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    """
    User login endpoint for obtaining JWT Token.
    FastAPI provides `OAuth2PasswordRequestForm` to easily handle standard OAuth2 password login requests.
    """
    # 1. Verify user exists
    user = get_user(fake_users_db, username=form_data.username)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # 2. Verify password is correct
    if not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # 3. Create and return Token
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    # In the token payload, we typically include a unique identifier like username (sub)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    
    return {"access_token": access_token, "token_type": "bearer"}

@app.post("/users/", response_model=User)
async def create_user(user: UserCreate):
    """User registration endpoint."""
    # Check if username already exists
    db_user = get_user(fake_users_db, username=user.username)
    if db_user:
        raise HTTPException(status_code=400, detail="Username already registered")
    
    # Hash the password before storing in the "database"
    hashed_password = get_password_hash(user.password)
    fake_users_db[user.username] = {
        "id": len(fake_users_db) + 1,
        "username": user.username,
        "email": user.email,
        "full_name": user.full_name,
        "hashed_password": hashed_password,
        "disabled": False,
    }
    
    # Return created user info (excluding plaintext password)
    return fake_users_db[user.username]

@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    """
    Get personal information of the current logged-in user.
    This endpoint is protected by the `get_current_active_user` dependency; a valid Token is required to access.
    """
    return current_user

@app.get("/users/", response_model=List[User])
async def read_all_users(current_user: User = Depends(get_current_admin_user)):
    """
    Get information of all users.
    This endpoint is protected by the `get_current_admin_user` dependency; only admins can access.
    """
    return list(fake_users_db.values())

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

IV. Testing Our API

Now, let’s run and test this application.

  1. Start the server: python main.py
  2. Test using Swagger UI:
    Open your browser and go to http://127.0.0.1:8000/docs. You’ll see an interactive API documentation.
    a. Register a user: Find the /users/ endpoint, click “Try it out”, enter information for a new user, e.g.:
    json { "username": "newuser", "password": "newpassword" }
    Click “Execute”. If successful, you’ll see the returned user info where hashed_password is the encrypted password.
    b. User login (obtain Token): Find the /token endpoint, click “Try it out”. Enter username and password (e.g., johndoe and secret). Click “Execute”. You’ll get a response like:
    json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer" }
    Copy this access_token.
    c. Access a protected endpoint: Find the /users/me/ endpoint, click “Try it out”. You’ll notice the docs interface prompts you to “Authorize”. Click the “Authorize” button at the top right. In the pop-up dialog, paste the access_token into the input field in the format Bearer <access_token> (note the space between Bearer and the token).
    Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    Click “Authorize” and “Close”. Now click “Execute” for /users/me/. You will successfully get the information for user johndoe.
    d. Test permission control:
    – Use johndoe‘s token to access the /users/ endpoint. You’ll get a 403 Forbidden error because johndoe is not an admin.
    – Use admin‘s token (password is also secret) to access the /users/ endpoint. You will successfully get the list of all users.

V. Key Technical Takeaways

  1. Core Libraries:
    • python-jose: Responsible for JWT generation (jwt.encode) and verification (jwt.decode).
    • passlib: Responsible for password hashing (pwd_context.hash) and verification (pwd_context.verify). Never store plaintext passwords.
  2. Key Concepts:
    • OAuth2PasswordBearer: A security utility provided by FastAPI that automatically extracts and returns the token string from the Authorization: Bearer <token> request header. We use it to define the oauth2_scheme dependency.
    • Dependency Injection (Depends): This is a very powerful feature of FastAPI. We encapsulate the authentication logic in functions like get_current_user and inject them into route handler functions that require authentication via Depends(). Benefits include:
      • Code Reuse: Multiple endpoints can share the same authentication logic.
      • Separation of Concerns: Route functions focus on business logic; authentication logic is handled by dedicated functions.
      • Easy Testing: Dependencies can be easily replaced in tests.
  3. Authentication Flow:
    1. User submits credentials via /token endpoint; server verifies and returns JWT.
    2. Client carries the JWT in subsequent requests.
    3. Protected endpoints verify the JWT’s validity via the dependency Depends(get_current_user) and extract user information.
    4. Route handler function receives and uses the extracted user object.
  4. Permission Control:
    • More granular permission checks can be built on top of get_current_user, like get_current_active_user and get_current_admin_user.
    • Complex permission logic can be implemented by layering dependencies.
  5. Security Best Practices:
    • Protect your SECRET_KEY: Use environment variables or a secure key management service.
    • Set reasonable token expiration time: ACCESS_TOKEN_EXPIRE_MINUTES should not be too long to reduce the risk if a token is leaked.
    • Use HTTPS: In production, always use HTTPS to transmit data, including JWTs, to prevent eavesdropping.
    • Consider using Refresh Tokens: For applications requiring long-term login, you can use an Access Token + Refresh Token pattern. The Access Token is short-lived, while the Refresh Token is long-lived. When the Access Token expires, the client can use the Refresh Token to obtain a new Access Token without requiring the user to log in again.

VI. Summary

Through this article, you have learned the complete process of implementing JWT-based authentication in FastAPI. Starting from the basic principles of JWT, we demonstrated with a full code example how to implement user registration, login, and how to use dependency injection to protect endpoints and implement permission control.

JWT is a very flexible and powerful authentication tool. Understanding and mastering it is crucial for building secure modern web applications. We hope this article helps you successfully integrate authentication into your FastAPI projects.