Python Fastapi Jwt Oauth2 Authentication

Written by

in

FastAPI has quickly become the go‑to framework for building modern, high‑performance APIs in Python. One of the most common requirements for any production‑grade API is robust authentication, and JSON Web Tokens (JWT) combined with OAuth2 provide a secure, scalable solution. In this guide we’ll walk through everything you need to know to implement JWT‑based OAuth2 authentication in a FastAPI application—from setting up the project and generating tokens to protecting routes and handling token revocation. By the end, you’ll have a ready‑to‑deploy authentication layer that follows best practices and boosts your API’s SEO visibility with relevant keywords like “FastAPI JWT”, “OAuth2 authentication”, and “Python security”.

Why Choose FastAPI for JWT OAuth2?

FastAPI shines for authentication for several reasons:

  • Asynchronous support: Handles high‑throughput workloads without blocking.
  • Automatic OpenAPI docs: Swagger UI and ReDoc expose your security schemes automatically, improving developer experience.
  • Dependency injection: Cleanly separates authentication logic from business logic.
  • Type hints: Enables static analysis tools to catch security‑related bugs early.

Core Concepts You Need to Know

JSON Web Token (JWT)

A JWT is a compact, URL‑safe token that consists of three Base64‑encoded parts: header, payload, and signature. The payload typically contains user claims (e.g., sub for user ID, exp for expiration) that the server can trust because they are signed with a secret key or an RSA private key.

OAuth2 Password Grant Flow

FastAPI recommends the OAuth2 “password” flow for APIs that issue JWTs directly after a username/password check. The flow works like this:

  1. Client sends username and password to /token.
  2. Server validates credentials and returns an access_token (JWT) and token_type.
  3. Client includes the token in the Authorization: Bearer <token> header for subsequent requests.

Step‑by‑Step Implementation

1. Project Setup

python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install fastapi uvicorn python-multipart python-jose[cryptography] passlib[bcrypt]

We use python-jose for JWT handling and passlib for password hashing.

2. Create the FastAPI App

from fastapi import FastAPI

app = FastAPI(title="FastAPI JWT OAuth2 Example")

3. Define User Models and Fake Database

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    username: str
    full_name: Optional[str] = None
    disabled: Optional[bool] = None

class UserInDB(User):
    hashed_password: str

# Simple in‑memory "database"
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderland",
        "hashed_password": "$2b$12$KIXQZ3u0KZp1E/6J8Fh9Ue8a9Fh9xKkZ8GZsGZkW3e0a6Zc9EwB5e",  # password: secret
        "disabled": False,
    }
}

4. Password Hashing Utilities

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

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

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

5. Token Settings

from datetime import datetime, timedelta
from jose import JWTError, jwt

# Secret key should be stored securely (e.g., env variable)
SECRET_KEY = "a3f5c2e7d8b9c0d1e2f3a4b5c6d7e8f9"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

6. Create Access Token

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

7. OAuth2 Dependency

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi import Depends, HTTPException, status

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_user(db, username: str) -> Optional[UserInDB]:
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)
    return None

def authenticate_user(fake_db, username: str, password: str) -> Optional[UserInDB]:
    user = get_user(fake_db, username)
    if not user or not verify_password(password, user.hashed_password):
        return None
    return user

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, username)
    if user is None:
        raise credentials_exception
    return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

8. Token Endpoint

@app.post("/token", response_model=dict)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, 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 {"access_token": access_token, "token_type": "bearer"}

9. Protecting Routes

@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

10. Running the Application

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

Visit http://localhost:8000/docs to see the automatically generated OpenAPI docs. The /token endpoint appears under the “Authorize” button, allowing you to test protected routes directly from the UI.

Best Practices for Production‑Ready JWT OAuth2

  • Store secrets securely: Use environment variables, secret managers, or vault services instead of hard‑coding SECRET_KEY.
  • Short‑lived access tokens: Keep the expiration time low (e.g., 15 minutes) and issue refresh tokens if you need long‑term sessions.
  • Implement token revocation: Maintain a blocklist (e.g., Redis) for compromised tokens or support logout flows.
  • Use HTTPS: Always serve your API over TLS to protect bearer tokens in transit.
  • Validate audience and issuer: When integrating with third‑party identity providers, check aud and iss claims.
  • Rate limit authentication endpoints: Prevent brute‑force attacks on /token with tools like slowapi or external API gateways.

Extending the Example: Refresh Tokens and OAuth2 Scopes

While the password grant is sufficient for many internal APIs, larger ecosystems often require refresh tokens and scoped access. Below is a concise outline of how to add these features.

Refresh Token Flow

  1. When issuing the access token, also generate a long‑lived refresh token (e.g., 7 days).
  2. Store the refresh token hash in a database linked to the user.
  3. Create a new endpoint /refresh that accepts the refresh token, verifies it, and returns a fresh access token.
  4. Invalidate the old refresh token after use to mitigate replay attacks.

Scopes Example

oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={"read": "Read access", "write": "Write access"},
)

# Dependency that checks required scopes
def get_current_user_with_scope(
    security_scopes: SecurityScopes,
    token: str = Depends(oauth2_scheme)
):
    # Decode token, extract "scopes" claim, compare with security_scopes.scopes
    # Raise 403 if missing
    ...

By adding scopes, you can protect fine‑grained endpoints such as /items/ (read

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *