Tutorial · 35 min read

Python Authentication with H33:
From Setup to Production

From zero to post-quantum secure. Build authentication the right way in Python — starting with classical best practices in Flask and Django, then upgrading to biometric and post-quantum auth with H33's API. Complete, runnable code at every step.

5 min
Setup
~50µs
Per Auth
1.2M
Auth/sec
PQ
Secure

Table of Contents

  1. Why Authentication Matters in 2026
  2. Setting Up Your Python Project
  3. Password Hashing: bcrypt, Argon2id, and PBKDF2
  4. Flask Authentication with Flask-Login
  5. Django's Built-In Authentication System
  6. JWT Authentication with PyJWT
  7. OAuth 2.0 and OpenID Connect Integration
  8. Session Management and Security
  9. Rate Limiting and Brute-Force Protection
  10. Multi-Factor Authentication
  11. Post-Quantum Readiness with the H33 API
  12. Security Best Practices and Common Pitfalls
  13. Production Deployment Considerations

Authentication is the single most critical piece of any web application. Get it wrong, and nothing else matters — your database, your business logic, your user trust, all compromised. Yet the majority of Python tutorials stop at werkzeug.security.generate_password_hash() and call it a day. That was adequate in 2020. It is not adequate in 2026, when quantum computers are advancing from laboratory curiosities toward practical cryptanalytic threats.

This tutorial takes a different approach. We start with the fundamentals — password hashing, session management, JWTs, OAuth — because you need to understand classical authentication before you can appreciate why it needs to evolve. Then we layer on biometric authentication and post-quantum cryptography through H33's API. By the end, you will have a production-grade authentication system that is secure against both classical and quantum adversaries.

Every code example in this tutorial is complete and runnable. No pseudo-code, no hand-waving. Copy, paste, and build.

1. Why Authentication Matters in 2026

Authentication vulnerabilities remain the most exploited attack vector on the internet. The OWASP Top 10 (2021) lists Broken Access Control as the number one risk, and Identification and Authentication Failures at number seven. But these rankings understate the problem because authentication failures are a precondition for nearly every other category — injection, SSRF, and security misconfiguration attacks all become trivial once an attacker has authenticated as a privileged user.

The Threat Landscape

The Quantum Clock Is Ticking

NIST estimates that cryptographically relevant quantum computers could arrive by the early 2030s. Every JWT you sign today with RS256 (RSA) or ES256 (ECDSA) will be forgeable by a sufficiently powerful quantum computer running Shor's algorithm. If your tokens have long lifetimes or your signed data has long-term value, you are already vulnerable to harvest-now-decrypt-later attacks.

The good news: building authentication correctly in Python is not difficult. It just requires discipline, the right libraries, and a willingness to move beyond passwords-only. Let us start building.

2. Setting Up Your Python Project

We will build a complete Flask application with user registration, login, JWT-based sessions, and eventually H33 post-quantum authentication. The same principles apply to Django and FastAPI — we will show framework-specific examples throughout.

Shell Project initialization
# Create and enter project directory
mkdir h33-auth-tutorial && cd h33-auth-tutorial

# Create virtual environment (always isolate dependencies)
python3 -m venv venv
source venv/bin/activate

# Install core dependencies
pip install flask flask-login flask-sqlalchemy flask-wtf

# Install authentication dependencies
pip install bcrypt argon2-cffi PyJWT cryptography flask-limiter

# Install H33 SDK (post-quantum auth)
pip install h33-sdk

# Install production dependencies
pip install gunicorn python-dotenv redis

Here is what each package does:

Create the project structure:

Shell Directory structure
mkdir -p app/{routes,models,services,middleware}
touch app/__init__.py app/config.py .env .env.example run.py

Set up the environment file:

Env .env
# Server
FLASK_ENV=development
SECRET_KEY=your-256-bit-secret-replace-in-production

# JWT Configuration
JWT_SECRET_KEY=a-separate-secret-for-jwt-tokens
JWT_ACCESS_EXPIRY=900          # 15 minutes in seconds
JWT_REFRESH_EXPIRY=604800      # 7 days in seconds

# H33 API (get yours at h33.ai/get-api-key)
H33_API_KEY=your_h33_api_key_here
H33_API_URL=https://api.h33.ai/v1

# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/authdb

# Redis (for rate limiting and session store)
REDIS_URL=redis://localhost:6379/0

Now create the Flask application factory:

Python app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from dotenv import load_dotenv
import os

load_dotenv()

db = SQLAlchemy()
login_manager = LoginManager()
csrf = CSRFProtect()
limiter = Limiter(key_func=get_remote_address)

def create_app():
    app = Flask(__name__)

    # Configuration
    app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
    app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL")
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

    # Session security
    app.config["SESSION_COOKIE_HTTPONLY"] = True
    app.config["SESSION_COOKIE_SECURE"] = True
    app.config["SESSION_COOKIE_SAMESITE"] = "Lax"

    # Initialize extensions
    db.init_app(app)
    login_manager.init_app(app)
    csrf.init_app(app)
    limiter.init_app(app)

    # Register blueprints
    from app.routes.auth import auth_bp
    from app.routes.protected import protected_bp
    app.register_blueprint(auth_bp, url_prefix="/api/auth")
    app.register_blueprint(protected_bp, url_prefix="/api")

    return app
Why an Application Factory?

The factory pattern (create_app()) is a Flask best practice for production applications. It allows you to create multiple instances for testing, prevents circular imports, and makes configuration injection straightforward. Every serious Flask deployment uses this pattern.

3. Password Hashing: bcrypt, Argon2id, and PBKDF2

Storing passwords in plaintext is an unforgivable sin. Storing them with MD5 or SHA-256 is only marginally better — general-purpose hash functions are designed to be fast, which makes them easy to brute-force with GPUs. You need a password hashing function: a deliberately slow, memory-hard function designed specifically to make offline cracking expensive.

Python has three solid options. Here is how they compare:

Algorithm Memory-Hard GPU Resistant Time per Hash Recommendation
PBKDF2-SHA256 No Weak ~200ms (600k rounds) Django default, adequate
bcrypt Partial Moderate ~250ms (cost=12) Good, well-established
Argon2id Yes Strong ~300ms (default) Best choice for new projects

Argon2id: The Recommended Choice

Argon2 won the Password Hashing Competition (PHC) in 2015. Argon2id is the hybrid variant that resists both side-channel attacks (like timing attacks) and GPU-based cracking. It is the recommendation of OWASP, NIST (SP 800-63B), and the security community at large.

Python app/services/password.py
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError

# Configure Argon2id with OWASP-recommended parameters
ph = PasswordHasher(
    time_cost=3,          # Number of iterations
    memory_cost=65536,     # 64 MB of memory
    parallelism=4,         # 4 threads
    hash_len=32,           # 256-bit output
    salt_len=16,           # 128-bit salt
)

def hash_password(password: str) -> str:
    """Hash a password using Argon2id.
    Returns a PHC-format string containing algorithm,
    parameters, salt, and hash. Store this entire string."""
    return ph.hash(password)

def verify_password(stored_hash: str, password: str) -> bool:
    """Verify a password against a stored Argon2id hash."""
    try:
        return ph.verify(stored_hash, password)
    except (VerifyMismatchError, VerificationError):
        return False

def needs_rehash(stored_hash: str) -> bool:
    """Check if hash was created with outdated parameters.
    Call after successful verification. If True, rehash
    the password with current parameters and update DB."""
    return ph.check_needs_rehash(stored_hash)

bcrypt: The Established Alternative

bcrypt has been the go-to password hash since 1999. It uses a cost factor (number of rounds) that can be increased over time as hardware gets faster. A cost factor of 12 is the current minimum recommendation.

Python app/services/password_bcrypt.py
import bcrypt

def hash_password(password: str) -> str:
    """Hash a password using bcrypt with cost factor 12."""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
    return hashed.decode("utf-8")

def verify_password(stored_hash: str, password: str) -> bool:
    """Verify a password against a stored bcrypt hash."""
    return bcrypt.checkpw(
        password.encode("utf-8"),
        stored_hash.encode("utf-8")
    )

PBKDF2: Django's Default

Django ships with PBKDF2-SHA256 as its default password hasher. It is not memory-hard, which makes it more vulnerable to GPU-based cracking than Argon2id or bcrypt. However, Django makes it straightforward to upgrade:

Python settings.py (Django)
# Upgrade Django's password hashing to Argon2id
# pip install argon2-cffi django[argon2]
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
# Django automatically rehashes with Argon2id on next login
Never Roll Your Own

Do not use hashlib.sha256() or hashlib.pbkdf2_hmac() directly for password hashing unless you deeply understand salt generation, iteration counts, and timing-safe comparison. Use argon2-cffi or bcrypt — they handle all of this correctly.

4. Flask Authentication with Flask-Login

Flask-Login manages user sessions for you — tracking who is logged in, protecting routes that require authentication, and handling the remember-me cookie. Let us build a complete registration and login system.

The User Model

Python app/models/user.py
from app import db, login_manager
from flask_login import UserMixin
from app.services.password import hash_password, verify_password, needs_rehash
from datetime import datetime, timezone

class User(UserMixin, db.Model):
    __tablename__ = "users"

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    password_hash = db.Column(db.String(512), nullable=False)
    totp_secret = db.Column(db.String(32), nullable=True)
    mfa_enabled = db.Column(db.Boolean, default=False)
    failed_login_count = db.Column(db.Integer, default=0)
    locked_until = db.Column(db.DateTime, nullable=True)
    created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
    last_login = db.Column(db.DateTime, nullable=True)

    def set_password(self, password: str):
        self.password_hash = hash_password(password)

    def check_password(self, password: str) -> bool:
        if not verify_password(self.password_hash, password):
            return False
        # Transparent rehash on successful login
        if needs_rehash(self.password_hash):
            self.password_hash = hash_password(password)
            db.session.commit()
        return True

    def is_locked(self) -> bool:
        if self.locked_until and self.locked_until > datetime.now(timezone.utc):
            return True
        return False

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

Registration and Login Routes

Python app/routes/auth.py
from flask import Blueprint, request, jsonify
from flask_login import login_user, logout_user, login_required
from app import db, limiter
from app.models.user import User
from datetime import datetime, timedelta, timezone
import re

auth_bp = Blueprint("auth", __name__)

def validate_password_strength(password: str) -> tuple:
    if len(password) < 12:
        return False, "Password must be at least 12 characters"
    if not re.search(r"[A-Z]", password):
        return False, "Must contain an uppercase letter"
    if not re.search(r"[0-9]", password):
        return False, "Must contain a digit"
    return True, ""

@auth_bp.route("/register", methods=["POST"])
@limiter.limit("5 per minute")
def register():
    data = request.get_json()
    email = data.get("email", "").strip().lower()
    password = data.get("password", "")

    valid, msg = validate_password_strength(password)
    if not valid:
        return jsonify({"error": msg}), 400

    if User.query.filter_by(email=email).first():
        # Don't reveal whether email exists
        return jsonify({"message": "Check your email to confirm"}), 201

    user = User(email=email)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    return jsonify({"message": "Check your email to confirm"}), 201

@auth_bp.route("/login", methods=["POST"])
@limiter.limit("10 per minute")
def login():
    data = request.get_json()
    email = data.get("email", "").strip().lower()
    password = data.get("password", "")

    user = User.query.filter_by(email=email).first()

    if not user or not user.check_password(password):
        if user:
            user.failed_login_count += 1
            if user.failed_login_count >= 5:
                user.locked_until = datetime.now(timezone.utc) + timedelta(minutes=15)
            db.session.commit()
        return jsonify({"error": "Invalid email or password"}), 401

    if user.is_locked():
        return jsonify({"error": "Account temporarily locked"}), 423

    user.failed_login_count = 0
    user.locked_until = None
    user.last_login = datetime.now(timezone.utc)
    db.session.commit()

    login_user(user, remember=data.get("remember", False))
    return jsonify({"message": "Login successful"}), 200

Security Details in the Login Route

Notice the defensive patterns: rate limiting on the endpoint (10 requests/minute), account lockout after 5 failed attempts, constant-time responses that do not reveal whether an email exists, failure counter reset on successful login, and email normalization with .strip().lower(). These are not optional in production.

5. Django's Built-In Authentication System

Django ships with a complete authentication system out of the box. Unlike Flask, where you assemble components, Django provides the User model, login/logout views, password hashing, session management, and permissions. For many projects, you should use Django's auth and focus your energy on hardening it rather than replacing it.

Custom User Model

Always create a custom user model from the start, even if you have nothing to add yet. Migrating away from Django's default auth.User after data exists in production is painful.

Python accounts/models.py (Django)
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    email = models.EmailField(unique=True)
    mfa_enabled = models.BooleanField(default=False)
    totp_secret = models.CharField(max_length=32, blank=True, null=True)
    failed_login_count = models.IntegerField(default=0)
    locked_until = models.DateTimeField(blank=True, null=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username"]

    class Meta:
        db_table = "users"

Django Security Settings

Python settings.py (security section)
AUTH_USER_MODEL = "accounts.User"

SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_AGE = 1800            # 30 minutes
SESSION_ENGINE = "django.contrib.sessions.backends.cache"

CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True

SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
X_FRAME_OPTIONS = "DENY"
Flask vs Django: Which to Choose?

Use Django when you need a full-featured admin panel, ORM, migrations, and auth out of the box. Use Flask when you need microservice-level control, or you are building a pure API with a separate frontend. Use FastAPI when async performance is critical and you want automatic OpenAPI documentation. All three work with H33's post-quantum authentication API.

6. JWT Authentication with PyJWT

JSON Web Tokens (JWTs) are the dominant pattern for stateless authentication in APIs. A JWT encodes user claims into a signed token that the client sends with each request. The server verifies the signature without hitting a database — enabling horizontal scaling because any server instance can validate any token.

But JWTs are also the source of more security vulnerabilities than any other auth pattern. Here is how to implement them correctly.

Token Service

Python app/services/tokens.py
import jwt
import os
from datetime import datetime, timedelta, timezone
from typing import Optional
import secrets

JWT_SECRET = os.getenv("JWT_SECRET_KEY")
JWT_ALGORITHM = "HS256"
ACCESS_EXPIRY = int(os.getenv("JWT_ACCESS_EXPIRY", 900))
REFRESH_EXPIRY = int(os.getenv("JWT_REFRESH_EXPIRY", 604800))

def create_access_token(user_id: int, roles: list = None) -> str:
    """Create a short-lived access token."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user_id),
        "type": "access",
        "roles": roles or [],
        "iat": now,
        "exp": now + timedelta(seconds=ACCESS_EXPIRY),
        "jti": secrets.token_hex(16),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def create_refresh_token(user_id: int) -> str:
    """Create a long-lived refresh token."""
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user_id),
        "type": "refresh",
        "iat": now,
        "exp": now + timedelta(seconds=REFRESH_EXPIRY),
        "jti": secrets.token_hex(16),
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def decode_token(token: str) -> Optional[dict]:
    """Decode and validate a JWT. Returns None on failure."""
    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM],  # Whitelist algorithms
            options={
                "require": ["sub", "exp", "type", "jti"],
                "verify_exp": True,
            }
        )
        return payload
    except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
        return None

JWT Middleware

Python app/middleware/jwt_required.py
from functools import wraps
from flask import request, jsonify, g
from app.services.tokens import decode_token

def jwt_required(f):
    """Decorator: requires a valid JWT access token."""
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")

        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing Authorization header"}), 401

        token = auth_header[7:]
        payload = decode_token(token)

        if not payload or payload.get("type") != "access":
            return jsonify({"error": "Invalid or expired token"}), 401

        g.user_id = int(payload["sub"])
        g.user_roles = payload.get("roles", [])
        g.token_jti = payload["jti"]

        return f(*args, **kwargs)
    return decorated
The Five Deadly JWT Sins

1. Algorithm confusion — Always specify algorithms=["HS256"] in decode. Never accept "none". 2. No expiration — Tokens without exp live forever. 3. Storing in localStorage — XSS can steal them. Use httpOnly cookies instead. 4. Huge payloads — JWTs are sent with every request; keep claims minimal. 5. No revocation strategy — You need a token blacklist (Redis) or short-lived tokens with refresh rotation.

7. OAuth 2.0 and OpenID Connect Integration

OAuth 2.0 lets your users sign in with existing accounts — Google, GitHub, Microsoft — instead of creating yet another password. OpenID Connect (OIDC) is the identity layer on top of OAuth 2.0 that provides standardized user profile information. For Python applications, Authlib is the most complete and well-maintained library.

Shell Install Authlib
pip install authlib httpx
Python app/services/oauth.py
from authlib.integrations.flask_client import OAuth
from flask import current_app

oauth = OAuth()

def init_oauth(app):
    oauth.init_app(app)

    # Google OIDC
    oauth.register(
        name="google",
        client_id=app.config["GOOGLE_CLIENT_ID"],
        client_secret=app.config["GOOGLE_CLIENT_SECRET"],
        server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
        client_kwargs={"scope": "openid email profile"},
    )

    # GitHub OAuth 2.0
    oauth.register(
        name="github",
        client_id=app.config["GITHUB_CLIENT_ID"],
        client_secret=app.config["GITHUB_CLIENT_SECRET"],
        access_token_url="https://github.com/login/oauth/access_token",
        authorize_url="https://github.com/login/oauth/authorize",
        api_base_url="https://api.github.com/",
        client_kwargs={"scope": "user:email"},
    )
Python app/routes/oauth.py
from flask import Blueprint, redirect, url_for, session, jsonify
from flask_login import login_user
from app.services.oauth import oauth
from app.models.user import User
from app import db

oauth_bp = Blueprint("oauth", __name__)

@oauth_bp.route("/login/google")
def google_login():
    redirect_uri = url_for("oauth.google_callback", _external=True)
    return oauth.google.authorize_redirect(redirect_uri)

@oauth_bp.route("/callback/google")
def google_callback():
    token = oauth.google.authorize_access_token()
    user_info = token.get("userinfo")

    if not user_info or not user_info.get("email_verified"):
        return jsonify({"error": "Email not verified"}), 400

    email = user_info["email"].lower()

    # Find or create user
    user = User.query.filter_by(email=email).first()
    if not user:
        user = User(email=email)
        user.set_password(secrets.token_urlsafe(32))  # Random password
        db.session.add(user)
        db.session.commit()

    login_user(user)
    return redirect("/")
OAuth Security Essentials

Always verify the state parameter to prevent CSRF attacks. Always validate that the email is verified before trusting it. Use PKCE (Proof Key for Code Exchange) for public clients. Never store OAuth tokens in localStorage — use server-side sessions or httpOnly cookies. The Authlib library handles most of these automatically when configured correctly.

8. Session Management and Security

Sessions are the mechanism by which your server remembers who a user is between requests. Whether you use Flask's signed cookies, Django's server-side sessions, or JWTs, session management is where most authentication implementations fail in production. The attacks are well-understood: session fixation, session hijacking, and session replay.

Server-Side Sessions with Redis

Flask's default sessions store data in signed cookies on the client. This is convenient but limited — cookie size limits, no server-side revocation, and the session data is visible (though not modifiable) to the client. For production, use server-side sessions backed by Redis.

Python app/config.py
from redis import Redis
import os

class ProductionConfig:
    SECRET_KEY = os.getenv("SECRET_KEY")
    SESSION_TYPE = "redis"
    SESSION_REDIS = Redis.from_url(os.getenv("REDIS_URL"))
    SESSION_PERMANENT = False
    SESSION_USE_SIGNER = True
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_SAMESITE = "Lax"
    PERMANENT_SESSION_LIFETIME = 1800  # 30 minutes

Session Security Checklist

Must Have

  • httpOnly cookies — Prevents JavaScript from reading session tokens. Blocks XSS-based session theft.
  • Secure flag — Cookies transmitted only over HTTPS. Prevents network sniffing.
  • SameSite=Lax — Prevents CSRF attacks by restricting cross-origin cookie sending.
  • Short session lifetime — 15-30 minutes for sensitive applications. Use sliding windows.

Should Have

  • Session regeneration — Issue a new session ID after login to prevent fixation attacks.
  • Idle timeout — Expire sessions after inactivity, not just absolute time.
  • Concurrent session limits — Limit active sessions per user. Alert on anomalous logins.
  • IP/UA binding — Invalidate sessions when the client fingerprint changes dramatically.
Python Session regeneration after login
from flask import session

def regenerate_session():
    """Regenerate session ID after authentication state change.
    Prevents session fixation attacks."""
    old_data = dict(session)
    session.clear()
    session.update(old_data)
    session.modified = True

# Call after login_user() in your login route
login_user(user)
regenerate_session()

9. Rate Limiting and Brute-Force Protection

Without rate limiting, an attacker can attempt millions of login attempts per hour against your API. Even with Argon2id hashing, you need to limit the rate at which guesses can be submitted. Defense in depth means multiple layers: application-level rate limiting, account lockout, and network-level protection.

Flask-Limiter Configuration

Python app/__init__.py (rate limiter config)
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"],
    storage_uri=os.getenv("REDIS_URL", "memory://"),
)

# Apply stricter limits to auth endpoints
@auth_bp.route("/login", methods=["POST"])
@limiter.limit("10 per minute")
@limiter.limit("3 per minute", key_func=lambda: request.get_json().get("email", ""))
def login():
    # Per-IP AND per-email rate limiting
    ...

Progressive Delay Strategy

Instead of hard lockouts, which can be used for denial-of-service against legitimate users, implement progressive delays that make brute-force attacks impractical without completely blocking access:

Python app/services/brute_force.py
import redis
import time
import os

r = redis.from_url(os.getenv("REDIS_URL"))

def check_brute_force(identifier: str) -> tuple:
    """Check if login attempt should be delayed.
    Returns (allowed: bool, wait_seconds: int)."""
    key = f"login_attempts:{identifier}"
    attempts = r.get(key)

    if attempts is None:
        return True, 0

    attempts = int(attempts)

    # Progressive delay: 0, 1, 2, 4, 8, 16, 32, 60 seconds
    if attempts < 3:
        return True, 0
    elif attempts < 10:
        delay = min(2 ** (attempts - 3), 60)
        return True, delay
    else:
        return False, 900  # 15-minute lockout after 10 failures

def record_attempt(identifier: str, success: bool):
    key = f"login_attempts:{identifier}"
    if success:
        r.delete(key)
    else:
        pipe = r.pipeline()
        pipe.incr(key)
        pipe.expire(key, 900)  # Reset counter after 15 minutes
        pipe.execute()

Defense in Depth: Layered Rate Limiting

Layer 1: Network-level (NGINX/CloudFront) — blocks obvious floods before they reach your application. Layer 2: Application-level (Flask-Limiter) — per-IP and per-email limits. Layer 3: Account-level (progressive delay) — per-account lockout with exponential backoff. Layer 4: CAPTCHA after repeated failures — reCAPTCHA v3 or hCaptcha for suspected bots.

10. Multi-Factor Authentication

Passwords alone are not sufficient. Multi-factor authentication (MFA) requires users to prove their identity with two or more independent factors: something they know (password), something they have (TOTP device), and something they are (biometrics). TOTP (Time-based One-Time Password, RFC 6238) is the most widely deployed second factor and works with Google Authenticator, Authy, and 1Password.

Shell Install TOTP dependencies
pip install pyotp qrcode[pil]
Python app/services/mfa.py
import pyotp
import qrcode
import io
import base64

def generate_totp_secret() -> str:
    """Generate a new TOTP secret for a user."""
    return pyotp.random_base32()

def get_totp_uri(secret: str, email: str) -> str:
    """Generate the otpauth:// URI for QR code scanning."""
    totp = pyotp.TOTP(secret)
    return totp.provisioning_uri(name=email, issuer_name="YourApp")

def generate_qr_code(uri: str) -> str:
    """Generate a base64-encoded QR code image."""
    img = qrcode.make(uri)
    buffer = io.BytesIO()
    img.save(buffer, format="PNG")
    return base64.b64encode(buffer.getvalue()).decode()

def verify_totp(secret: str, code: str) -> bool:
    """Verify a TOTP code with a 30-second window."""
    totp = pyotp.TOTP(secret)
    return totp.verify(code, valid_window=1)  # Allows 1 step drift

MFA Enrollment Endpoint

Python app/routes/mfa.py
@auth_bp.route("/mfa/setup", methods=["POST"])
@login_required
def setup_mfa():
    """Generate TOTP secret and return QR code."""
    secret = generate_totp_secret()
    uri = get_totp_uri(secret, current_user.email)
    qr_base64 = generate_qr_code(uri)

    # Store secret temporarily in session until verified
    session["pending_totp_secret"] = secret

    return jsonify({
        "qr_code": f"data:image/png;base64,{qr_base64}",
        "secret": secret,  # For manual entry
    })

@auth_bp.route("/mfa/verify", methods=["POST"])
@login_required
def verify_mfa_setup():
    """Verify the user can generate valid TOTP codes."""
    code = request.get_json().get("code", "")
    secret = session.get("pending_totp_secret")

    if not secret or not verify_totp(secret, code):
        return jsonify({"error": "Invalid code"}), 400

    # Persist the secret and enable MFA
    current_user.totp_secret = secret
    current_user.mfa_enabled = True
    db.session.commit()
    session.pop("pending_totp_secret", None)

    return jsonify({"message": "MFA enabled successfully"})
Beyond TOTP: WebAuthn and Biometrics

TOTP is a solid second factor, but it is still phishable — an attacker with a convincing fake login page can capture both the password and the TOTP code in real time. For phishing-resistant MFA, consider WebAuthn/FIDO2 (hardware security keys) or biometric authentication through H33's API. Biometric verification via fully homomorphic encryption ensures the biometric template never leaves the encrypted domain.

11. Post-Quantum Readiness with the H33 API

Everything we have built so far — password hashing, JWTs, OAuth, sessions, MFA — protects against today's threats. But quantum computers running Shor's algorithm will break the RSA and ECC cryptography that underlies TLS, JWT signatures, and OAuth token exchange. H33's API provides a drop-in upgrade path: post-quantum authentication backed by FHE, zero-knowledge proofs, and CRYSTALS-Dilithium signatures — all in a single API call that completes in approximately 50 microseconds per authentication.

Install the H33 Python SDK

Shell Install H33 SDK
pip install h33-sdk

Initialize the Client

Python app/services/h33_client.py
import os
from h33 import H33Client

# Initialize the H33 client with your API key
h33 = H33Client(
    api_key=os.getenv("H33_API_KEY"),
    base_url=os.getenv("H33_API_URL", "https://api.h33.ai/v1"),
)

# Async client for FastAPI / async Flask
from h33 import AsyncH33Client

async_h33 = AsyncH33Client(
    api_key=os.getenv("H33_API_KEY"),
    base_url=os.getenv("H33_API_URL", "https://api.h33.ai/v1"),
)

Post-Quantum Biometric Enrollment

H33's biometric authentication processes biometric templates entirely within FHE (Fully Homomorphic Encryption). The raw biometric data never leaves the encrypted domain — the server performs matching on ciphertexts. This is provably private: even H33's own infrastructure cannot see the biometric template.

Python app/routes/h33_auth.py
from flask import Blueprint, request, jsonify
from app.services.h33_client import h33
from app.middleware.jwt_required import jwt_required

h33_bp = Blueprint("h33", __name__)

@h33_bp.route("/enroll", methods=["POST"])
@jwt_required
def enroll_biometric():
    """Enroll a user's biometric template with H33.
    The template is encrypted client-side before transmission."""
    data = request.get_json()
    biometric_template = data.get("template")

    if not biometric_template:
        return jsonify({"error": "Missing biometric template"}), 400

    result = h33.biometric.enroll(
        user_id=str(g.user_id),
        template=biometric_template,
        modality="face",  # or "fingerprint", "voice"
    )

    return jsonify({
        "enrolled": result.success,
        "enrollment_id": result.enrollment_id,
        "pq_secured": True,
    })

Post-Quantum Authentication

Python app/routes/h33_auth.py (continued)
@h33_bp.route("/verify", methods=["POST"])
@jwt_required
def verify_biometric():
    """Verify a biometric template against enrolled data.
    All matching happens on encrypted ciphertexts (FHE)."""
    data = request.get_json()

    result = h33.biometric.verify(
        user_id=str(g.user_id),
        template=data.get("template"),
    )

    return jsonify({
        "verified": result.match,
        "confidence": result.confidence,
        "latency_us": result.latency_us,  # ~50 microseconds
        "pq_attestation": result.attestation,
    })

Post-Quantum Token Signing

Replace your classical JWT signing with H33's Dilithium-backed token attestation. This protects your tokens against future quantum attacks:

Python app/services/pq_tokens.py
from app.services.h33_client import h33
import json
import base64

def create_pq_signed_token(user_id: int, claims: dict) -> dict:
    """Create a post-quantum signed authentication token.
    Uses Dilithium signatures via H33's API."""
    payload = json.dumps({
        "sub": str(user_id),
        **claims,
    }).encode()

    result = h33.sign(
        payload=base64.b64encode(payload).decode(),
        algorithm="dilithium3",
    )

    return {
        "token": result.signature,
        "payload": base64.b64encode(payload).decode(),
        "algorithm": "dilithium3",
        "pq_secure": True,
    }

def verify_pq_token(token: str, payload: str) -> bool:
    """Verify a post-quantum signed token."""
    result = h33.verify(
        payload=payload,
        signature=token,
        algorithm="dilithium3",
    )
    return result.verified

H33 Auth Performance

FHE biometric match (32-user batch)~1,375µs
Dilithium attestation~240µs
Per-auth latency~50µs
Sustained throughput~1.2M auth/sec

FastAPI Async Integration

If you are using FastAPI, take advantage of async calls to H33's API for maximum throughput:

Python fastapi_h33.py
from fastapi import FastAPI, Depends, HTTPException
from app.services.h33_client import async_h33

app = FastAPI()

@app.post("/api/auth/verify")
async def verify_pq(user_id: str, template: str):
    """Async post-quantum biometric verification."""
    result = await async_h33.biometric.verify(
        user_id=user_id,
        template=template,
    )

    if not result.match:
        raise HTTPException(status_code=401, detail="Verification failed")

    return {
        "verified": True,
        "latency_us": result.latency_us,
        "pq_attestation": result.attestation,
    }
Migration Path: Classical to Post-Quantum

You do not need to rip out your existing auth system. The recommended migration path is: Step 1 — Add H33 as an additional verification layer alongside your existing JWTs. Step 2 — Enroll users in biometric auth progressively (on next login). Step 3 — Switch token attestation from HS256/RS256 to Dilithium. Step 4 — Deprecate password-only authentication. Each step is independently deployable. See our crypto agility guide for the full migration playbook.

12. Security Best Practices and Common Pitfalls

After building hundreds of authentication systems, these are the mistakes we see most often. Every item on this list has caused a real-world data breach.

The Pitfalls

Pitfall Impact Fix
Plaintext passwords in logs Critical Never log request bodies on auth endpoints. Scrub passwords from error tracking.
Timing oracle on login High Return identical responses for "user not found" and "wrong password". Use constant-time comparison.
JWT in localStorage Critical XSS steals tokens. Use httpOnly cookies with Secure and SameSite flags.
No CSRF protection High Use Flask-WTF or Django's CSRF middleware. Always validate the CSRF token on state-changing requests.
Weak secrets in .env Critical Generate secrets with python -c "import secrets; print(secrets.token_hex(32))". Never commit .env to git.
No rate limiting High Flask-Limiter or Django-Ratelimit on all auth endpoints. Back with Redis for distributed deployments.
Password reset via GET High Reset tokens must be single-use, short-lived (15 min), and validated via POST.
Missing HTTPS enforcement Critical HSTS header with a one-year max-age. Redirect all HTTP to HTTPS. Set Secure flag on all cookies.

Security Headers Checklist

Python Security headers middleware
@app.after_request
def set_security_headers(response):
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
    response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
    return response

Dependency Security

Shell Audit your dependencies regularly
# Check for known vulnerabilities
pip install pip-audit
pip-audit

# Pin all dependencies with hashes
pip install pip-tools
pip-compile --generate-hashes requirements.in

# Use Dependabot or Renovate for automated updates

13. Production Deployment Considerations

Development authentication and production authentication are fundamentally different. Here is what changes when you deploy to production.

WSGI Server

Never use Flask's built-in development server in production. Use Gunicorn with a production-grade worker configuration:

Shell Production launch command
# Production Gunicorn config
gunicorn \
  --workers 4 \
  --worker-class gthread \
  --threads 2 \
  --bind 0.0.0.0:8000 \
  --timeout 30 \
  --access-logfile - \
  --error-logfile - \
  --max-requests 10000 \
  --max-requests-jitter 1000 \
  "app:create_app()"

Production Checklist

Secrets Management

Store secrets in AWS Secrets Manager, HashiCorp Vault, or environment variables injected at deploy time. Never hardcode secrets. Never commit .env files. Rotate JWT signing keys on a regular schedule.

Database Security

Use connection pooling (PgBouncer or SQLAlchemy pool). Enable SSL for database connections. Use a read replica for authentication queries in high-traffic scenarios. Run database migrations separately from application deployment.

Monitoring and Alerting

Log every authentication event: successful logins, failed attempts, password resets, MFA enrollments. Alert on anomalies: spike in failed logins, logins from new geographies, concurrent sessions. Never log passwords or tokens.

HTTPS Everywhere

Terminate TLS at your load balancer (ALB, CloudFront, Nginx). Set HSTS headers with a one-year max-age and preload directive. Use TLS 1.3 minimum. Consider post-quantum TLS for forward secrecy against quantum adversaries.

Backup and Recovery

Automated daily backups of user credential databases. Test recovery procedures quarterly. If using H33 for biometric auth, enrolled templates are stored in H33's infrastructure — no biometric backup is required on your side.

Django Production Settings

Python settings/production.py
import os

DEBUG = False
ALLOWED_HOSTS = [os.getenv("DOMAIN")]
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")

# Database with SSL
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.getenv("DB_NAME"),
        "USER": os.getenv("DB_USER"),
        "PASSWORD": os.getenv("DB_PASSWORD"),
        "HOST": os.getenv("DB_HOST"),
        "OPTIONS": {"sslmode": "require"},
        "CONN_MAX_AGE": 600,
    }
}

# Cache-backed sessions
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": os.getenv("REDIS_URL"),
    }
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"

# Logging: capture auth events, never log credentials
LOGGING = {
    "version": 1,
    "handlers": {
        "console": {"class": "logging.StreamHandler"},
    },
    "loggers": {
        "django.security": {
            "handlers": ["console"],
            "level": "WARNING",
        },
    },
}

Conclusion

Authentication in Python is a solved problem at the classical level — the libraries exist, the patterns are well-documented, and the pitfalls are known. What is not solved is the quantum threat. Every JWT signed with RS256, every TLS handshake using ECDHE, every password hash protected by a classical key derivation function is on a clock. The question is not whether quantum computers will break these primitives, but when.

The migration path is clear: start with solid classical foundations (Argon2id, short-lived JWTs, server-side sessions, MFA), then layer on post-quantum protection through H33's API. You do not need to rip and replace. You need to add a layer. One API call. Approximately 50 microseconds of additional latency. And your authentication system is secure against both today's attackers and tomorrow's quantum computers.

For more on the cryptographic foundations, see our guides on Fully Homomorphic Encryption, Zero-Knowledge Proofs, and CRYSTALS-Dilithium signatures. For framework-specific tutorials, see the Node.js authentication tutorial, the Go authentication tutorial, and the Rust authentication tutorial.

Ready to Go Quantum-Secure?

Start protecting your users with post-quantum authentication today. 10,000 free API calls/month, no credit card required.

Get Free API Key →

Build With Post-Quantum Security

Enterprise-grade FHE, ZKP, and post-quantum cryptography. One API call. Sub-millisecond latency.

Get Free API Key → Read the Docs
Free tier · 10,000 API calls/month · No credit card required
Verify It Yourself