StormShadow308's picture
Deploy RICS Gradio UI with ZeroGPU-safe generation
6143b69
Raw
History Blame Contribute Delete
5.27 kB
"""Auth primitives: password hashing, JWT, and a small file-based user store.
Prefers ``passlib`` / ``python-jose`` when installed (see requirements.txt) and
falls back to stdlib implementations (PBKDF2-HMAC + a hand-rolled HS256 token) so
the backend runs and tests pass without those optional dependencies.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import time
from pathlib import Path
from backend.config import settings
# ── password hashing ─────────────────────────────────────────────────────────
try:
import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("ascii")
def verify_password(password: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("ascii"))
except Exception: # noqa: BLE001
return False
except ImportError: # bcrypt unavailable β†’ PBKDF2 fallback
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 120_000)
return f"pbkdf2_sha256$120000${salt.hex()}${dk.hex()}"
def verify_password(password: str, hashed: str) -> bool:
try:
algo, iters, salt_hex, dk_hex = hashed.split("$")
except ValueError:
return False
if algo != "pbkdf2_sha256":
return False
dk = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), bytes.fromhex(salt_hex), int(iters)
)
return hmac.compare_digest(dk.hex(), dk_hex)
# ── JWT ──────────────────────────────────────────────────────────────────────
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _b64url_decode(data: str) -> bytes:
pad = "=" * (-len(data) % 4)
return base64.urlsafe_b64decode(data + pad)
try:
from jose import jwt as _jose_jwt
def create_token(tenant_id: str) -> str:
payload = {
"sub": tenant_id,
"exp": int(time.time()) + settings.jwt_expiry_minutes * 60,
}
return _jose_jwt.encode(
payload, settings.jwt_secret, algorithm=settings.jwt_algorithm
)
def decode_token(token: str) -> dict:
return _jose_jwt.decode(
token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]
)
except Exception: # noqa: BLE001 - python-jose unavailable β†’ HS256 fallback
def create_token(tenant_id: str) -> str:
header = _b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
payload = _b64url(
json.dumps(
{
"sub": tenant_id,
"exp": int(time.time()) + settings.jwt_expiry_minutes * 60,
}
).encode()
)
signing_input = f"{header}.{payload}".encode()
sig = hmac.new(
settings.jwt_secret.encode(), signing_input, hashlib.sha256
).digest()
return f"{header}.{payload}.{_b64url(sig)}"
def decode_token(token: str) -> dict:
try:
header_b64, payload_b64, sig_b64 = token.split(".")
except ValueError as exc:
raise ValueError("Malformed token") from exc
signing_input = f"{header_b64}.{payload_b64}".encode()
expected = hmac.new(
settings.jwt_secret.encode(), signing_input, hashlib.sha256
).digest()
if not hmac.compare_digest(expected, _b64url_decode(sig_b64)):
raise ValueError("Invalid signature")
payload = json.loads(_b64url_decode(payload_b64))
if int(payload.get("exp", 0)) < int(time.time()):
raise ValueError("Token expired")
return payload
# ── user store ───────────────────────────────────────────────────────────────
def _users_path() -> Path:
p = settings.data_dir_path / "users.json"
p.parent.mkdir(parents=True, exist_ok=True)
return p
def _load_users() -> dict[str, str]:
path = _users_path()
if not path.is_file():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception: # noqa: BLE001
return {}
def _save_users(users: dict[str, str]) -> None:
_users_path().write_text(json.dumps(users), encoding="utf-8")
def register_user(tenant_id: str, password: str) -> bool:
"""Create a tenant. Returns False if it already exists."""
users = _load_users()
if tenant_id in users:
return False
users[tenant_id] = hash_password(password)
_save_users(users)
return True
def authenticate(tenant_id: str, password: str) -> bool:
users = _load_users()
hashed = users.get(tenant_id)
return bool(hashed and verify_password(password, hashed))