JWT Authentication in FastAPI: Secure Your Python API With OAuth2 and JSON Web Tokens

Authentication is the process of verifying who a user is before granting access to protected resources. In API development, the standard approach is token-based authentication using JSON Web Tokens (JWT). The user sends their credentials once, receives a signed token, and includes that token in the header of every subsequent request. FastAPI makes this pattern straightforward through its built-in OAuth2 support, Pydantic validation, and dependency injection system. This guide walks through the complete implementation: installing the right libraries, hashing passwords with Argon2id, creating and verifying JWT tokens, building the login endpoint, and protecting routes with a reusable get_current_user dependency.

How JWT Authentication Works

The flow has four steps. First, the client sends a username and password to the login endpoint. Second, the server verifies the credentials, creates a JWT containing the user's identity and an expiration time, signs it with a secret key, and returns it. Third, the client stores the token and includes it in the Authorization: Bearer <token> header of every subsequent request. Fourth, the server extracts the token, verifies the signature and expiration, and identifies the user.

A JWT is not encrypted—anyone can decode and read the payload. But it is signed, which means the server can verify that it issued the token and that the contents have not been tampered with. The token contains claims like the user's identity (sub) and when the token expires (exp). Because the token is self-contained, the server does not need to look up a session in a database on every request. This makes JWT authentication stateless and well-suited for distributed systems and microservices.

Dependencies and Setup

pip install fastapi uvicorn pyjwt pwdlib[argon2] python-multipart

Here is what each package does: pyjwt handles encoding and decoding JWT tokens. pwdlib[argon2] provides password hashing using the Argon2id algorithm, which is the current recommendation in the FastAPI documentation. python-multipart is required for OAuth2 form data parsing (the login form sends username and password as form fields, not JSON).

Note

The official FastAPI docs now use pwdlib with Argon2id instead of the older passlib with bcrypt, and PyJWT instead of python-jose. Both older libraries still work, but the current recommendation is pwdlib and PyJWT for new projects.

Step 1: Password Hashing With pwdlib

Never store plain-text passwords. Hash them before saving to the database, and verify the hash when the user logs in. pwdlib uses Argon2id by default, which is memory-hard and resistant to GPU-based brute force attacks.

# auth/security.py

from pwdlib import PasswordHash

password_hash = PasswordHash.recommended()

def hash_password(password: str) -> str:
    return password_hash.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return password_hash.verify(plain_password, hashed_password)

PasswordHash.recommended() creates a hasher with secure default settings for Argon2id. The hash method produces a hashed string that includes the algorithm parameters and a random salt. The verify method checks a plain-text password against the stored hash.

Step 2: Creating and Verifying JWT Tokens

# auth/token.py

from datetime import datetime, timedelta, timezone
import jwt

SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def decode_access_token(token: str) -> dict:
    return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])

The create_access_token function takes a dictionary of claims (typically {"sub": username}), adds an expiration timestamp, and signs it. The decode_access_token function verifies the signature and expiration. If the token is expired or tampered with, jwt.decode raises an InvalidTokenError.

Common Mistake

Never hard-code your SECRET_KEY in source code. Generate it with openssl rand -hex 32 and store it in an environment variable. If the secret is compromised, anyone can forge valid tokens for any user.

Step 3: Pydantic Schemas for Auth

# auth/schemas.py

from pydantic import BaseModel

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

class TokenData(BaseModel):
    username: str | None = None

class UserCreate(BaseModel):
    username: str
    email: str
    password: str

class UserOut(BaseModel):
    username: str
    email: str
    disabled: bool = False

Token defines the response from the login endpoint. TokenData represents the claims extracted from a decoded JWT. UserCreate is the schema for registration (includes the plain-text password). UserOut is the schema for returning user data (excludes the password).

Step 4: The Login Endpoint

# main.py

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from auth.security import verify_password
from auth.token import create_access_token
from auth.schemas import Token

app = FastAPI()

# Simulated user database (replace with real DB lookup)
fake_users_db = {
    "kandi": {
        "username": "kandi",
        "email": "kandi@example.com",
        "hashed_password": "$argon2id$v=19$m=65536,t=3,p=4$...",  # pre-hashed
        "disabled": False,
    }
}

def authenticate_user(username: str, password: str):
    user = fake_users_db.get(username)
    if not user:
        return None
    if not verify_password(password, user["hashed_password"]):
        return None
    return user

@app.post("/token", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(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 = create_access_token(data={"sub": user["username"]})
    return Token(access_token=access_token, token_type="bearer")

OAuth2PasswordRequestForm is a FastAPI dependency that extracts username and password from form data. The endpoint looks up the user, verifies the password hash, creates a JWT with the username in the sub claim, and returns it. The WWW-Authenticate: Bearer header in the error response is part of the OAuth2 spec and tells the client what authentication scheme to use.

Pro Tip

The login endpoint at /token is the URL referenced by OAuth2PasswordBearer(tokenUrl="token"). When you open the Swagger UI at /docs, FastAPI generates an "Authorize" button that sends credentials to this endpoint and stores the returned token for subsequent requests. This makes testing protected endpoints directly from the browser straightforward.

Step 5: The get_current_user Dependency

This dependency extracts the token from the Authorization header, decodes it, and returns the authenticated user. Any route that includes this dependency is automatically protected.

# auth/dependencies.py

from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from auth.token import decode_access_token
from auth.schemas import TokenData

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_access_token(token)
        username: str | None = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception

    # Replace with real DB lookup
    user = fake_users_db.get(token_data.username)
    if user is None:
        raise credentials_exception
    return user

OAuth2PasswordBearer tells FastAPI to expect a Bearer token in the Authorization header. It extracts the token string and passes it to get_current_user. The function decodes the JWT, checks the sub claim, looks up the user, and returns it. If anything fails—expired token, invalid signature, missing user—it raises a 401 error.

The user lookup after decoding is important. Even if a token is valid, the user may have been deleted or disabled since the token was issued. Always verify against the current state of your database.

Step 6: Protecting Routes

To protect any endpoint, add get_current_user as a dependency. FastAPI handles the rest.

from typing import Annotated
from auth.dependencies import get_current_user

@app.get("/users/me", response_model=UserOut)
def read_current_user(current_user: Annotated[dict, Depends(get_current_user)]):
    return current_user

@app.get("/items/")
def list_items(current_user: Annotated[dict, Depends(get_current_user)]):
    return {"owner": current_user["username"], "items": ["item1", "item2"]}

Both routes require a valid JWT. When a client sends a request without a token or with an invalid token, FastAPI returns a 401 response. The Swagger UI shows a lock icon next to protected endpoints and provides the "Authorize" button for testing.

Security Best Practices

  • Store the SECRET_KEY in an environment variable. Generate it with openssl rand -hex 32 and never commit it to version control.
  • Use short-lived access tokens. Set expiration to 15–30 minutes. For longer sessions, implement a refresh token mechanism where a separate long-lived token is used to obtain new access tokens.
  • Enforce HTTPS in production. JWTs are transmitted in HTTP headers. Without TLS, tokens can be intercepted in transit.
  • Validate the user on every request. After decoding the token, look up the user in the database to check if the account still exists and is not disabled.
  • Use Argon2id for password hashing. It is memory-hard and resistant to GPU-based attacks. The pwdlib library with recommended settings handles this correctly.
  • Rate-limit the login endpoint. Without rate limiting, attackers can try thousands of password combinations per second. Add rate limiting middleware or use a reverse proxy like Nginx to throttle login requests.

Frequently Asked Questions

What is JWT authentication in FastAPI?

JWT authentication uses signed JSON Web Tokens to verify user identity. A user sends credentials to a login endpoint, receives a signed token, and includes it in the Authorization: Bearer header of subsequent requests. FastAPI's OAuth2PasswordBearer extracts the token, and a get_current_user dependency decodes it to identify and validate the user.

Should I use PyJWT or python-jose for JWT tokens in FastAPI?

The current official FastAPI documentation uses PyJWT. It provides a focused, well-maintained API for JWT encoding and decoding. python-jose was previously recommended but PyJWT is now the standard choice for new projects. For RSA or ECDSA signing, install PyJWT with the cryptography extra.

What password hashing algorithm should I use with FastAPI?

The current recommendation is pwdlib with Argon2id. Argon2id is memory-hard, making it resistant to GPU-based brute force attacks. It replaced the older recommendation of passlib with bcrypt. Use PasswordHash.recommended() which defaults to Argon2id with secure parameters.

How do I protect a FastAPI route with JWT authentication?

Add current_user: Annotated[dict, Depends(get_current_user)] to the route handler's parameters. The get_current_user dependency extracts the Bearer token from the Authorization header, decodes it, validates the claims, and returns the authenticated user. If the token is missing, expired, or invalid, FastAPI returns a 401 response automatically.

Key Takeaways

  1. Use PyJWT and pwdlib for new projects: These are the libraries recommended in the current FastAPI documentation. PyJWT handles token creation and verification. pwdlib with Argon2id handles password hashing.
  2. OAuth2PasswordBearer is a token extraction mechanism: It tells FastAPI to look for a Bearer token in the Authorization header and generates the Authorize button in Swagger UI. It does not implement the full OAuth2 spec—your login endpoint and token creation logic are yours to define.
  3. The get_current_user dependency is the gatekeeper: This single dependency decodes the token, validates claims, and looks up the user. Adding it to any route protects that route. It plugs directly into FastAPI's dependency injection system.
  4. Always validate the user after decoding: A valid token does not guarantee the user still exists or is still active. Always check against the database to catch deleted or disabled accounts.
  5. Keep secrets out of code: The SECRET_KEY used to sign tokens must be stored in an environment variable, not in source code. If it leaks, every token ever issued can be forged.

JWT authentication in FastAPI follows a clear pattern: hash passwords with Argon2id, create signed tokens with PyJWT, extract them with OAuth2PasswordBearer, and validate them with a reusable dependency. Every piece plugs into FastAPI's existing systems—Pydantic validates the schemas, dependency injection wires the authentication check into routes, and Swagger UI generates an interactive login flow automatically. The result is a secure, standards-compliant authentication system that is straightforward to implement, easy to test, and ready for production.

back to articles