Table of Contents
- Why Authentication Matters in 2026
- Setting Up Your Python Project
- Password Hashing: bcrypt, Argon2id, and PBKDF2
- Flask Authentication with Flask-Login
- Django's Built-In Authentication System
- JWT Authentication with PyJWT
- OAuth 2.0 and OpenID Connect Integration
- Session Management and Security
- Rate Limiting and Brute-Force Protection
- Multi-Factor Authentication
- Post-Quantum Readiness with the H33 API
- Security Best Practices and Common Pitfalls
- 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
- Credential stuffing — Automated tools test billions of leaked username/password pairs against your login endpoint. Over 15 billion credentials are publicly available from historical breaches. If your users reuse passwords (and most do), you are exposed.
- Phishing and social engineering — Even strong passwords fail when users hand them to convincing fake login pages. SMS-based 2FA codes are interceptable via SIM swapping.
- Session hijacking — Stolen session tokens allow attackers to impersonate users without ever knowing their password. XSS vulnerabilities are the primary vector.
- Brute-force attacks — Without rate limiting, attackers can attempt millions of password guesses per hour against your API.
- Harvest-now, decrypt-later — Nation-state actors are recording encrypted traffic today, planning to decrypt it once quantum computers are powerful enough to break RSA and ECC. Your authentication tokens and TLS sessions are targets.
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.
# 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:
flask— Lightweight web framework. The foundation of our API.flask-login— Session-based user authentication management for Flask.flask-sqlalchemy— SQLAlchemy ORM integration. Manages user records.bcrypt— Password hashing using the bcrypt KDF. C-backed, fast.argon2-cffi— The winner of the Password Hashing Competition. Stronger than bcrypt for new deployments.PyJWT— Signs and verifies JSON Web Tokens.flask-limiter— Rate limiting middleware backed by Redis or in-memory storage.h33-sdk— H33's Python SDK for post-quantum authentication and biometric verification.
Create the project 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:
# 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:
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
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.
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.
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:
# 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
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
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
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.
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
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"
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
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
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
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.
pip install authlib httpx
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"}, )
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("/")
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.
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.
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
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:
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.
pip install pyotp qrcode[pil]
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
@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"})
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
pip install h33-sdk
Initialize the Client
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.
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
@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:
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
FastAPI Async Integration
If you are using FastAPI, take advantage of async calls to H33's API for maximum throughput:
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, }
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
@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
# 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:
# 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
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.
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.
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.
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.
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
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 →