04/16 Upload codebase

This commit is contained in:
2026-04-16 08:27:07 -04:00
parent 91be5c1f5c
commit 457016b8d4
43 changed files with 6241 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
import uuid
import time
from datetime import datetime, timedelta
from functools import wraps
import jwt
from flask import current_app, request, g, jsonify
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError
def hash_auth_token(auth_hash: str) -> str:
"""Hash the client-derived PBKDF2 auth_hash with Argon2id before storing."""
ph = PasswordHasher(
time_cost=current_app.config['ARGON2_TIME_COST'],
memory_cost=current_app.config['ARGON2_MEMORY_COST'],
parallelism=current_app.config['ARGON2_PARALLELISM'],
)
return ph.hash(auth_hash)
def verify_auth_token(auth_hash: str, stored_hash: str) -> bool:
ph = PasswordHasher()
try:
return ph.verify(stored_hash, auth_hash)
except (VerifyMismatchError, VerificationError, InvalidHashError):
return False
def generate_tokens(user_id: int) -> dict:
"""Return access_token and refresh_token JWTs, each with a unique jti."""
now = datetime.utcnow()
secret = current_app.config['JWT_SECRET_KEY']
access_payload = {
'sub': str(user_id),
'type': 'access',
'jti': str(uuid.uuid4()),
'iat': now,
'exp': now + current_app.config['JWT_ACCESS_TOKEN_EXPIRES'],
}
refresh_payload = {
'sub': str(user_id),
'type': 'refresh',
'jti': str(uuid.uuid4()),
'iat': now,
'exp': now + current_app.config['JWT_REFRESH_TOKEN_EXPIRES'],
}
return {
'access_token': jwt.encode(access_payload, secret, algorithm='HS256'),
'refresh_token': jwt.encode(refresh_payload, secret, algorithm='HS256'),
}
def generate_mfa_token(user_id: int) -> str:
"""Short-lived (5-min) single-use token issued after password but before TOTP."""
now = datetime.utcnow()
payload = {
'sub': str(user_id),
'type': 'mfa',
'jti': str(uuid.uuid4()),
'iat': now,
'exp': now + timedelta(minutes=5),
}
return jwt.encode(payload, current_app.config['JWT_SECRET_KEY'], algorithm='HS256')
def decode_token(token: str, expected_type: str = 'access', check_blacklist: bool = True) -> dict:
"""Decode and validate a JWT. Raises jwt.PyJWTError on any failure."""
secret = current_app.config['JWT_SECRET_KEY']
payload = jwt.decode(token, secret, algorithms=['HS256'])
if payload.get('type') != expected_type:
raise jwt.InvalidTokenError('Wrong token type')
if check_blacklist:
from app.models.token_blacklist import TokenBlacklist
jti = payload.get('jti')
if jti and TokenBlacklist.is_blacklisted(jti):
raise jwt.InvalidTokenError('Token has been revoked')
return payload
def blacklist_token(token: str, token_type: str) -> None:
"""Add a JWT's jti to the blacklist. Silently ignores invalid tokens."""
try:
payload = decode_token(token, expected_type=token_type, check_blacklist=False)
jti = payload.get('jti')
if not jti:
return
exp = payload.get('exp')
expires_at = datetime.utcfromtimestamp(exp) if exp else datetime.utcnow() + timedelta(days=7)
from app.models.token_blacklist import TokenBlacklist
from app import db
# Avoid duplicate if already blacklisted
if not TokenBlacklist.query.filter_by(jti=jti).first():
entry = TokenBlacklist(
jti=jti,
user_id=int(payload.get('sub', 0)),
expires_at=expires_at,
)
db.session.add(entry)
db.session.commit()
# Opportunistic cleanup — runs in same transaction context
TokenBlacklist.cleanup_expired()
except Exception:
pass # Never let blacklisting errors break the logout flow
def require_jwt(f):
"""Decorator: validates Bearer token and sets g.current_user_id."""
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid Authorization header'}), 401
token = auth_header[7:]
try:
payload = decode_token(token, expected_type='access')
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.PyJWTError:
return jsonify({'error': 'Invalid token'}), 401
g.current_user_id = int(payload['sub'])
return f(*args, **kwargs)
return decorated