Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing JWT-Based Password Flow Authentication in FastAPI

Tech 1

FastAPI leverages the OAuth2PasswordBearer security scheme to enforce bearer token authentication following the OAuth2 password grant flow. This approach requires configuring a dependency that extracts tokens from the Authorization header, validating credentials at a dedicated login route, and issuing JSON Web Tokens (JWTs) via third-party libraries like python-jose and cryptography.

Token Extraction Setup

Initialize the security scheme by specfiying the endpoint responsible for token issuance:

from fastapi.security import OAuth2PasswordBearer

token_handler = OAuth2PasswordBearer(tokenUrl="/auth/session")

Inject this scheme into protected routes using FastAPI's dependency injection system. The framework will automatically parse the Bearer <token> header and pass the extracted string to your handler:

@app.get("/secure-resource")
async def fetch_data(credentials: str = Depends(token_handler)):
    return {"status": "success", "received_token_prefix": credentials[:8] + "..."}

Credential Verification & Token Issuance

The authentication endpoint must accept form data containing username and password. FastAPI provides OAuth2PasswordRequestForm to parse these fields compliant with the OAuth2 specification. Always install python-multipart to handle multipart/form-data requests:

pip install python-multipart passlib[bcrypt] python-jose[cryptography]

Password hashing should occur securely. The passlib library paired with bcrypt handles salting and verification efficiently. During registration or setup, hash passwords before storage. At login, verify plaintext inputs against stored hashes:

from passlib.context import CryptContext

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

def validate_credentials(plaintext: str, ciphertext: str) -> bool:
    return pwd_cipher.verify(plaintext, ciphertext)

Successful authentication triggers JWT creation. Utilize jwt.encode() from python-jose to sign payloads with a secret key and expiration timestamp:

import jwt as jose_encode
from datetime import datetime, timedelta, timezone

SECRET_KEY = "a3f8b9c2d1e0f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0"
ALGORITHM = "HS256"
ACCESS_TOKEN_TTL = timedelta(minutes=45)

def construct_jws(user_identifier: str) -> str:
    payload = {"uid": user_identifier, "iat": datetime.now(timezone.utc)}
    payload["exp"] = datetime.now(timezone.utc) + ACCESS_TOKEN_TTL
    return jose_encode.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

Combine verification and issuance into the login route:

from fastapi import HTTPException, status

class UserProfile(BaseModel):
    username: str
    hashed_secret: str

# Placeholder for actual database logic
def fetch_account(username: str) -> UserProfile | None:
    pass

@app.post("/auth/session")
async def authenticate(form: OAuth2PasswordRequestForm = Depends()):
    acct = fetch_account(form.username)
    
    if not acct or not validate_credentials(form.password, acct.hashed_secret):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication failed",
            headers={"WWW-Authenticate": "Bearer"},
        )
        
    jws_token = construct_jws(acct.username)
    return {"access_token": jws_token, "token_type": "bearer"}

Decoding & Route Protection

Extracting the username from an incoming token requires decoding and validation. Wrap this logic in a reusable dependency to maintain clean route handlers:

from jose import JWTError, jwt as jose_decode

def resolve_identity(jwt_string: str = Depends(token_handler)) -> UserProfile:
    auth_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or malformed signature",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    try:
        claims = jose_decode.decode(jwt_string, SECRET_KEY, algorithms=[ALGORITHM])
        subject = claims.get("uid")
        if not subject:
            raise auth_exception
    except JWTError:
        raise auth_exception
        
    current_acct = fetch_account(subject)
    if not current_acct or not getattr(current_acct, "active", True):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access restricted")
        
    return current_acct

Apply this dependency to restrict access to authorized accounts only:

@app.get("/dashboard/metrics")
async def view_metrics(current: UserProfile = Depends(resolve_identity)):
    return {"owner": current.username, "view": "allowed"}

Request Processing Middleware

Interceptors execute around every request lifecycle phase. Decorate async functions with @app.middleware("http") to hook into pre-processing and post-processing stages:

import time

@app.middleware("http")
async def track_latency(request: Request, next_handler):
    begin_timestamp = time.monotonic()
    response = await next_handler(request)
    elapsed_ms = (time.monotonic() - begin_timestamp) * 1000
    response.headers["X-Duration-MS"] = f"{elapsed_ms:.2f}"
    return response

Place timing calculations before and after invoking next_handler() to measure total latency without bolcking internal logic execution.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.